標籤是精細化運營必不可少的工具,常見的使用場景有標籤推送,千人千面的廣告展現等。在實際的業務中,標籤每每是經過交併差非運算組合在一塊兒使用,好比:標籤組合是 A ∪ B ∩ C
,須要判斷用戶在不在這個集合中。php
以千人千面展現廣告爲例,咱們會有這樣的需求:node
標籤說明:這裏的標籤都是用戶標籤,英文標籤:美甲師( identity_1
)、美甲店主( identity_2
)、參與了開店計劃( shop_setup_user
)、廣州( guangzhou
)、深圳( shenzhen
)。數據庫
首先,從需求能夠得出廣告展現的標籤表達式:express
A 廣告: (identity_1 ∪ identity_2) ∩ shop_setup_user ∩ guangzhou
B 廣告: (identity_1 ∪ identity_2) ∩ shop_setup_user ∩ shenzhen
數組
爲了方便表示「交併差非」全部運算,將「交併差非」分別用「*+-!」表示,其中運算沒有優先級區別,因而上面的表達式能夠寫成:數據結構
A 廣告: (identity_1+identity_2)*shop_setup_user*guangzhou
B 廣告: (identity_1+identity_2)*shop_setup_user*shenzhen
ide
分析:一個用戶包含多個標籤,判斷「一個用戶」是否存在「一個標籤運算的集合」中,從而來展現廣告,其核心就是:判斷一個並集集合與另外一個(多個運算的)集合的交集關係。函數
結合「交併差非」的含義,以及(除了「!」)符號左右結合運算的原理,能夠明確符號鏈接左右兩個的標籤(表達式)的含義:工具
理清楚含義之後,能夠看出只要用遞歸的方式對其左右運算,就能夠獲得「用戶是否在標籤表達式」集合裏的結果。左右運算的一個很合適的數據結構就是二叉樹,大體思路就是:測試
關於表達式的解析,與基本的四則運算表達式解析基本一致,只不過咱們的含義不同,以及沒有符號的優先級區別。
中綴表達式就是常說的算數表達式,好比:1+2*3/(2+1)
。後綴表達式(也叫逆波蘭表示法)就是運算符在運算數以後的表達式,好比上述的表達式寫成:12321+/*+
。也但是實現去掉括號的做用。轉化過程,會用到棧去保存運算符號。
讀取的字符 | 分解中綴表達式 | 求後綴表達式(output) | 棧中內容 |
---|---|---|---|
1 | 1 | 1 | |
+ | 1+ | 1 | + |
2 | 1+2 | 12 | + |
* | 1+2* | 12 | +* |
3 | 1+2*3 | 123 | +* |
/ | 1+2*3/ | 123 | +*/ |
( | 1+2*3/( | 123 | +*/( |
2 | 1+2*3/(2 | 1232 | +*/( |
+ | 1+2*3/(2+ | 1232 | +*/(+ |
1 | 1+2*3/(2+1 | 12321 | +*/(+ |
) | 1+2*3/(2+1) | 12321+ | +*/( |
1+2*3/(2+1) | 12321+ | +*/ | |
1+2*3/(2+1) | 12321+/ | +* | |
1+2*3/(2+1) | 12321+/* | + | |
1+2*3/(2+1) | 12321+/*+ |
能夠看出轉化規則是,按順序讀取字符:
(+-*/
,寫入操做符棧中。)
,從非空的操做符棧,中彈出一項;若項不爲(
,則寫至輸出,若項爲(
,則退出循環。function expressionToSuffixExpressionArray($expression) { $charArray = array_reverse(str_split($expression)); $operationArray = []; $output = []; while (($c = array_pop($charArray)) != '') { if (in_array($c, ['(', '+', '-', '*', '/'])) { array_push($operationArray, $c); } elseif (in_array($c, [')'])) { while ($op = array_pop($operationArray)) { if ($op == '(') { break; } array_push($output, $op); } } else { array_push($output, $c); } } return array_merge($output, $operationArray); } //測試代碼 $expression = '3*(2+1)'; $result = expressionToSuffixExpressionArray($expression); echo "expression: {$expression}" . PHP_EOL; print_r($result);
輸出:
expression: 3*(2+1) Array ( [0] => 3 [1] => 2 [2] => 1 [3] => + [4] => * )
基礎的表達式解析實現了,針對咱們的標籤表達式(多個字符組成一個標籤),以及去掉「/」,加上「!」的邏輯,稍做修改:
function expressionToSuffixExpressionArray($expression) { $charArray = array_reverse(str_split($expression)); $operationArray = []; $output = []; $expression = ''; while (($c = array_pop($charArray)) != '') { if (in_array($c, ['(', '+', '-', '*'])) { if (!empty($expression)) { array_push($output, $expression); $expression = ''; } array_push($operationArray, $c); } elseif (in_array($c, [')'])) { if (!empty($expression)) { array_push($output, $expression); $expression = ''; } while ($op = array_pop($operationArray)) { if ($op == '(') { break; } array_push($output, $op); } } elseif (in_array($c, ['!'])) { if (!empty($expression)) { array_push($output, $expression); $expression = ''; } array_push($output, $c); } else { $expression .= $c; } } return array_merge($output, $operationArray); } //測試代碼 $expression = '(identity_1+identity_2)*shop_setup_user*guangzhou'; $result = expressionToSuffixExpressionArray($expression); echo "expression: {$expression}" . PHP_EOL; print_r($result);
輸出:
expression: (identity_1+identity_2)*shop_setup_user*guangzhou Array ( [0] => identity_1 [1] => identity_2 [2] => + [3] => shop_setup_user [4] => guangzhou [5] => * [6] => * )
分析:根據後綴表達式的含義,符合表示前面兩個元素的運算。所以在遍歷時,能夠利用一個棧去暫存標籤表達式,當遍歷到符號,就彈出兩個標籤做爲其運算的左右元素,造成一個新的節點放回到棧中,如此循環就能造成一個完整的二叉樹。
//轉後綴表達式的方法 ... //基礎節點 class TreeNode { public static function create(string $root = '') { return [ 'root' => $root, 'left' => '', 'right' => '', 'opposite' => false, ]; } } //後綴表達式數組轉成二叉樹 function suffixExpressionArrayToBinaryTree($suffixExpressionArray) { $stack = []; $suffixExpressionArray = array_reverse($suffixExpressionArray); while ($item = array_pop($suffixExpressionArray)) { if (in_array($item, ['+', '-', '*'])) { $node = TreeNode::create($item); $node['right'] = array_pop($stack); $left = array_pop($stack); if ($left['root'] == '!') { $node['right']['opposite'] = true; $node['left'] = array_pop($stack); } else { $node['left'] = $left; } array_push($stack, $node); } else { array_push($stack, TreeNode::create($item)); } } return $stack; } //測試代碼 $expression = '(identity_1+identity_2)*shop_setup_user*guangzhou'; $result = expressionToSuffixExpressionArray($expression); echo "expression: {$expression}" . PHP_EOL; print_r($result); $tree = suffixExpressionArrayToBinaryTree($result); print_r($tree);
輸出:
Array ( [0] => Array ( [root] => * [left] => Array ( [root] => + [left] => Array ( [root] => identity_1 [left] => [right] => [opposite] => ) [right] => Array ( [root] => identity_2 [left] => [right] => [opposite] => ) [opposite] => ) [right] => Array ( [root] => * [left] => Array ( [root] => shop_setup_user [left] => [right] => [opposite] => ) [right] => Array ( [root] => guangzhou [left] => [right] => [opposite] => ) [opposite] => ) [opposite] => ) )
回顧一下符號的含義:
- 由「+」鏈接的兩個標籤(表達式)是或的關係,只要有一個與用戶的標籤有交集即爲true。
- 由「*」連接的兩個標籤(表達式)是交的關係,左右兩個都與用戶的標籤有交集才爲true。
- 由「-」連接的兩個標籤(表達式)是交的關係,左邊與用戶的標籤有交集且右邊與用戶的標籤沒有交集,才爲true。
- 「!」比較特殊,它是使得其後跟着的標籤(表達式)相反。
說明:
實現
//接上面的代碼 //... function isContained(array $userTags, array $rootNode): bool { $result = false; if (in_array($rootNode['root'], ['+', '-', '*'])) { switch ($rootNode['root']) { case '+': $result = (isContained($userTags, $rootNode['left']) || isContained( $userTags, $rootNode['right'] )); break; case '-': $result = ((isContained( $userTags, $rootNode['left'] ) === true) && (isContained( $userTags, $rootNode['right'] ) === false)); break; case '*': $result = (isContained($userTags, $rootNode['left']) && isContained( $userTags, $rootNode['right'] )); break; } } else { $result = in_array($rootNode['root'], $userTags); } if ($rootNode['opposite']) { $result = !$result; } return $result; } //測試代碼 //$tree 是上一步的tree $userTags1 = ['tag1', 'tag2', 'identity_1', 'guangzhou', 'shop_setup_user']; $result1 = isContained($userTags1, $tree[0]); $userTags2 = ['tag1', 'tag2', 'identity_2', 'shop_setup_user']; $result2 = isContained($userTags2, $tree[0]); $userTags3 = ['tag1', 'tag2', 'identity_3', 'guangzhou', 'shop_setup_user']; $result3 = isContained($userTags3, $tree[0]); var_dump($result1, $result2, $result3);
輸出:
bool(true) bool(false) bool(false)
在實際的業務中,標籤組合會更加複雜。除了「標籤」與「標籤」組合,還可會有「標籤」與「標籤組」,「用戶標籤」與「設備標籤」。下面談談這些需求如何支持。
標籤組實質也是經過標籤的運算組合在一塊兒,舉個例子:
標籤組1:Atag1+Atag2*Atag3
標籤組2:Btag4-[標籤組1]
結果:Btag4-(Atag1+Atag2*Atag3)
假若有用戶標籤與設備標籤組合,目前沒作過這樣的需求哈,若是要作能夠考慮isContained的參數用一個「包含用戶標籤數組和設備標籤數組的對象」代替數組,而後標籤表達式中的標籤帶上前綴:用戶標籤(u|
)、設備標籤(d|
)。
舉個例子:
標籤表達式:(u|identity_1+u|identity_2)*u|shop_setup_user*d|guangzhou
判斷時,根據前綴來選擇使用用戶標籤仍是設備標籤作判斷。
除了「判斷標籤組是否包含用戶」這個需求,還有另一個需求也很經常使用:「判斷標籤表達式包含多少用戶」,這個需求除了邏輯還涉及到數據庫的設計,實現方案跟實際場景也有關係,就不在這裏討論啦。
以上的代碼段爲縮減版,可能存在問題哈,若有錯漏望指正。