多標籤(組)運算

1、概述

標籤是精細化運營必不可少的工具,常見的使用場景有標籤推送,千人千面的廣告展現等。在實際的業務中,標籤每每是經過交併差非運算組合在一塊兒使用,好比:標籤組合是 A ∪ B ∩ C,須要判斷用戶在不在這個集合中。php

以千人千面展現廣告爲例,咱們會有這樣的需求:node

  1. (美甲師或者美甲店主)且參與了開店計劃的廣州用戶展現A廣告。
  2. (美甲師或者美甲店主)且參與了開店計劃的深圳用戶展現B廣告。

標籤說明:這裏的標籤都是用戶標籤,英文標籤:美甲師( identity_1)、美甲店主( identity_2)、參與了開店計劃( shop_setup_user)、廣州( guangzhou)、深圳( shenzhen)。數據庫

2、實現思路

首先,從需求能夠得出廣告展現的標籤表達式: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*shenzhenide

分析:一個用戶包含多個標籤,判斷「一個用戶」是否存在「一個標籤運算的集合」中,從而來展現廣告,其核心就是:判斷一個並集集合與另外一個(多個運算的)集合的交集關係。函數

1. 表達式分析

表達式含義

結合「交併差非」的含義,以及(除了「!」)符號左右結合運算的原理,能夠明確符號鏈接左右兩個的標籤(表達式)的含義:工具

  1. 由「+」鏈接的兩個標籤(表達式)是或的關係,只要有一個與用戶的標籤有交集即爲true。
  2. 由「*」連接的兩個標籤(表達式)是交的關係,左右兩個都與用戶的標籤有交集才爲true。
  3. 由「-」連接的兩個標籤(表達式)是交的關係,左邊與用戶的標籤有交集且右邊與用戶的標籤沒有交集,才爲true。
  4. 「!」比較特殊,它是使得其後跟着的標籤(表達式)相反。

轉成二叉樹

理清楚含義之後,能夠看出只要用遞歸的方式對其左右運算,就能夠獲得「用戶是否在標籤表達式」集合裏的結果。左右運算的一個很合適的數據結構就是二叉樹,大體思路就是:測試

  1. 將表達式轉成二叉樹
  2. 遞歸二叉樹判斷

image.png

2. 表達式解析

關於表達式的解析,與基本的四則運算表達式解析基本一致,只不過咱們的含義不同,以及沒有符號的優先級區別。

a. 中綴表達式與後綴表達式

中綴表達式就是常說的算數表達式,好比: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+/*+

能夠看出轉化規則是,按順序讀取字符:

  1. 遇到操做數,寫入output。
  2. 遇到(+-*/,寫入操做符棧中。
  3. 遇到),從非空的操做符棧,中彈出一項;若項不爲(,則寫至輸出,若項爲(,則退出循環。
  4. 循環讀取結束後,將操做符棧逐個彈出拼在output後便可。
代碼實現(PHP)
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] => *
)

b. 後綴表達式轉二叉樹

分析:根據後綴表達式的含義,符合表示前面兩個元素的運算。所以在遍歷時,能夠利用一個棧去暫存標籤表達式,當遍歷到符號,就彈出兩個標籤做爲其運算的左右元素,造成一個新的節點放回到棧中,如此循環就能造成一個完整的二叉樹。

//轉後綴表達式的方法
...


//基礎節點
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] =>
        )

)

3. 判斷標籤組是否包含用戶

回顧一下符號的含義:

  1. 由「+」鏈接的兩個標籤(表達式)是或的關係,只要有一個與用戶的標籤有交集即爲true。
  2. 由「*」連接的兩個標籤(表達式)是交的關係,左右兩個都與用戶的標籤有交集才爲true。
  3. 由「-」連接的兩個標籤(表達式)是交的關係,左邊與用戶的標籤有交集且右邊與用戶的標籤沒有交集,才爲true。
  4. 「!」比較特殊,它是使得其後跟着的標籤(表達式)相反。

說明:

  1. 這裏函數傳入參數設計爲「用戶標籤」和上一步構成的「樹」。
  2. 「用戶標籤」是個數組。
  3. 判斷邏輯先簡單判斷是否存在於「用戶標籤」數組中。

實現

//接上面的代碼
//...

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)

3、場景擴展

在實際的業務中,標籤組合會更加複雜。除了「標籤」與「標籤」組合,還可會有「標籤」與「標籤組」,「用戶標籤」與「設備標籤」。下面談談這些需求如何支持。

1. 標籤與標籤組互相嵌套

標籤組實質也是經過標籤的運算組合在一塊兒,舉個例子:
標籤組1:Atag1+Atag2*Atag3
標籤組2:Btag4-[標籤組1]
結果:Btag4-(Atag1+Atag2*Atag3)

2. 多種類型的標籤組合運算

假若有用戶標籤與設備標籤組合,目前沒作過這樣的需求哈,若是要作能夠考慮isContained的參數用一個「包含用戶標籤數組和設備標籤數組的對象」代替數組,而後標籤表達式中的標籤帶上前綴:用戶標籤(u|)、設備標籤(d|)。
舉個例子:
標籤表達式:(u|identity_1+u|identity_2)*u|shop_setup_user*d|guangzhou
判斷時,根據前綴來選擇使用用戶標籤仍是設備標籤作判斷。

4、結語

除了「判斷標籤組是否包含用戶」這個需求,還有另一個需求也很經常使用:「判斷標籤表達式包含多少用戶」,這個需求除了邏輯還涉及到數據庫的設計,實現方案跟實際場景也有關係,就不在這裏討論啦。

以上的代碼段爲縮減版,可能存在問題哈,若有錯漏望指正。

相關文章
相關標籤/搜索