六種機率資料結構的詳細解釋及應用場景

PetterLiu發表於2024-10-23

image

1/ Bloom Filter

  • 用途: 測試一個元素是否可能在一個集合中。
  • 原理: Bloom Filter 使用多個雜湊函式將元素對映到一個位陣列上。如果所有對應的位都被設定為1,則認為該元素可能在集合中。
  • 優點: 非常節省空間,因為不需要儲存實際的元素,只需儲存點陣圖資訊。
  • 應用: 在資料庫查詢最佳化、網頁快取過濾、網路路由器中快速判斷是否轉發資料包等場合都有應用。

image

Bloom Filter在IP白名單

Bloom Filter 在 IP 白名單的應用場景主要是為了快速判斷一個 IP 地址是否屬於已知的白名單集合。由於 Bloom Filter 具有高效的儲存和查詢特性,它非常適合用於頻繁查詢的大規模資料集。例如,在網路防火牆或安全裝置中,需要快速判斷一個請求的來源 IP 是否屬於預先定義好的白名單。

下面是一個使用 Guava 庫實現 Bloom Filter 的示例程式碼,用於 IP 白名單檢查:

示例程式碼

首先,確保你已經在專案中新增了 Guava 庫的依賴。如果你使用的是 Maven,可以在 pom.xml 檔案中新增如下依賴:

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>31.0.1-jre</version>
</dependency>
然後,你可以使用以下 Java 程式碼來實現 IP 白名單的檢查:
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnel;

import java.net.InetAddress;
import java.net.UnknownHostException;

public class IPWhiteListExample {

    public static void main(String[] args) {
        // 建立一個BloomFilter例項,預期插入5000個元素,期望誤報率為0.01
        BloomFilter<InetAddress> ipBloomFilter = BloomFilter.create(
                new Funnel<InetAddress>() {
                    @Override
                    public void funnel(InetAddress from, com.google.common.hash.Hasher into) {
                        into.putInt(from.getAddress().length);
                        into.putBytes(from.getAddress());
                    }
                }, 5000, 0.01);

        // 新增一些IP地址到BloomFilter
        addIPToFilter(ipBloomFilter, "192.168.1.1");
        addIPToFilter(ipBloomFilter, "192.168.1.2");
        addIPToFilter(ipBloomFilter, "192.168.1.3");

        // 檢查IP地址是否存在於白名單中
        checkIPInWhiteList(ipBloomFilter, "192.168.1.1"); // 應該返回true
        checkIPInWhiteList(ipBloomFilter, "192.168.1.4"); // 可能返回true或false,取決於誤報率
    }

    private static void addIPToFilter(BloomFilter<InetAddress> filter, String ipAddress) {
        try {
            InetAddress inetAddress = InetAddress.getByName(ipAddress);
            filter.put(inetAddress);
        } catch (UnknownHostException e) {
            e.printStackTrace();
        }
    }

    private static void checkIPInWhiteList(BloomFilter<InetAddress> filter, String ipAddress) {
        try {
            InetAddress inetAddress = InetAddress.getByName(ipAddress);
            boolean mightBePresent = filter.mightContain(inetAddress);
            System.out.println("IP Address " + ipAddress + ": " + (mightBePresent ? "Might be in whitelist" : "Not in whitelist"));
        } catch (UnknownHostException e) {
            e.printStackTrace();
        }
    }
}
解釋
  1. 建立 BloomFilter 例項:我們建立了一個 BloomFilter,預期插入 5000 個 IP 地址,期望的誤報率為 0.01。
  2. 定義 Funnel:這裡定義了一個 Funnel 介面實現,用於將 IP 地址轉換為可雜湊的形式。Funnel 介面允許我們指定如何將物件轉換為原始型別以便進行雜湊。
  3. 新增 IP 到 BloomFilter:我們透過 addIPToFilter 方法將一些 IP 地址新增到 BloomFilter 中。
  4. 檢查 IP 是否在白名單中:透過 checkIPInWhiteList 方法,我們可以檢查一個 IP 地址是否可能存在於白名單中。

這種方法非常適合用於需要快速判斷 IP 地址是否合法的場景,例如在網路防火牆、負載均衡器或其他需要頻繁進行 IP 地址驗證的應用中。


在 Redis 中實現 Bloom Filter 可以利用 Redis 的 BitMap 資料型別或者 String 型別來儲存位陣列。Redis 本身並沒有直接提供 Bloom Filter 的實現,但是可以透過手動構建位陣列來模擬 Bloom Filter 的行為。

下面是如何使用 Redis 來實現一個 Bloom Filter,用於 IP 白名單過濾的場景:

步驟 1:安裝 Redis 並連線

確保你已經安裝了 Redis,並且有一個可用的 Redis 例項。同時,你需要一個 Redis 客戶端庫來與 Redis 互動。在 Java 中,可以使用 Jedis 或 Lettuce 庫。這裡我們將使用 Jedis。

新增 Jedis 依賴

如果你使用 Maven,可以在 pom.xml 檔案中新增如下依賴:

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>4.4.0</version>
</dependency>
步驟 2:編寫程式碼

接下來,編寫 Java 程式碼來實現 Bloom Filter,並用於 IP 白名單檢查。

示例程式碼
import redis.clients.jedis.Jedis;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.stream.IntStream;

public class RedisBloomFilterExample {

    private static final int BLOOM_FILTER_SIZE = 10000; // 位陣列大小
    private static final int HASH_FUNCTIONS_COUNT = 5; // 雜湊函式的數量

    public static void main(String[] args) {
        // 連線 Redis
        Jedis jedis = new Jedis("localhost", 6379); // 替換為實際的 Redis 地址和埠
        
        // 清空 Redis 資料庫(僅用於演示)
        jedis.flushDB();

        // 新增 IP 地址到 Bloom Filter
        addIPToFilter(jedis, "192.168.1.1");
        addIPToFilter(jedis, "192.168.1.2");
        addIPToFilter(jedis, "192.168.1.3");

        // 檢查 IP 地址是否在白名單中
        checkIPInWhiteList(jedis, "192.168.1.1"); // 應該返回 true
        checkIPInWhiteList(jedis, "192.168.1.4"); // 可能返回 true 或 false,取決於誤報率
    }

    private static void addIPToFilter(Jedis jedis, String ipAddress) {
        byte[] ipBytes = ipToByteArray(ipAddress);
        IntStream.range(0, HASH_FUNCTIONS_COUNT).forEach(i -> {
            int index = hash(ipBytes, i) % BLOOM_FILTER_SIZE;
            jedis.setbit("bloomfilter", index, true);
        });
    }

    private static void checkIPInWhiteList(Jedis jedis, String ipAddress) {
        byte[] ipBytes = ipToByteArray(ipAddress);
        boolean mightBePresent = IntStream.range(0, HASH_FUNCTIONS_COUNT)
                .allMatch(i -> jedis.getbit("bloomfilter", hash(ipBytes, i) % BLOOM_FILTER_SIZE));
        System.out.println("IP Address " + ipAddress + ": " + (mightBePresent ? "Might be in whitelist" : "Not in whitelist"));
    }

    private static byte[] ipToByteArray(String ipAddress) {
        try {
            return InetAddress.getByName(ipAddress).getAddress();
        } catch (UnknownHostException e) {
            throw new RuntimeException(e);
        }
    }

    private static int hash(byte[] data, int seed) {
        int hash = 0;
        MessageDigest md = null;
        try {
            md = MessageDigest.getInstance("MD5");
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        }
        md.update(data);
        byte[] digest = md.digest();
        hash = seed * digest[0] & 0xFF;
        return Math.abs(hash);
    }
}
注意事項
  • 雜湊函式:這裡的雜湊函式使用 MD5 訊息摘要演算法,但在實際應用中可以根據需要選擇其他雜湊演算法。
  • 位陣列大小和雜湊函式數量BLOOM_FILTER_SIZEHASH_FUNCTIONS_COUNT 的選擇會影響 Bloom Filter 的誤報率和儲存效率。你可以根據實際需求調整這兩個引數。
  • 誤報率:Bloom Filter 存在一定的誤報率,因此在判斷 IP 地址是否在白名單中時,需要考慮這一點。

透過上述步驟,你可以在 Redis 中實現一個 Bloom Filter,並用於 IP 白名單過濾的場景。這種方法特別適合需要快速查詢大量 IP 地址的應用場景。


2/ Cuckoo Filter:

  • 用途: 類似於Bloom Filter,測試一個元素是否可能在一個集合中。
  • 原理: Cuckoo Filter 使用類似於Cuckoo Hashing的技術來儲存元素的指紋(通常是雜湊值的一部分),並允許刪除操作。
  • 優點: 支援刪除操作,並且在某些情況下效能優於Bloom Filter。
  • 應用: 在資料庫系統中用於快速查詢以及在網路環境中進行快速過濾等。

image

Cuckoo Filter 使用指紋來儲存元素的一部分資訊,並使用 Cuckoo Hashing 來解決衝突。在電商系統中,Cuckoo Filter 可以用於多種場景,如快取鍵管理、商品推薦系統中的去重、使用者行為分析等。

應用場景示例
  1. 快取鍵管理:在電商系統的快取機制中,可以使用 Cuckoo Filter 來儲存已存在的快取鍵,從而快速判斷一個新鍵是否已經存在於快取中,避免不必要的快取查詢。

  2. 商品推薦系統中的去重:在推薦系統中,可以使用 Cuckoo Filter 來儲存已推薦的商品 ID,確保不會重複推薦相同的產品。

  3. 使用者行為分析:在使用者行為跟蹤和分析中,可以使用 Cuckoo Filter 來記錄使用者的瀏覽歷史記錄,從而快速判斷使用者是否瀏覽過某個特定的商品。

使用 Java 編寫 Cuckoo Filter 示例

在 Java 中,可以使用第三方庫來實現 Cuckoo Filter,比如 cuckoofilter 庫。下面是一個使用 cuckoofilter 庫的示例程式碼,展示如何在電商系統中使用 Cuckoo Filter 進行快取鍵管理。

步驟 1:新增依賴

首先,確保你在專案中新增了 cuckoofilter 庫的依賴。如果你使用 Maven,可以在 pom.xml 檔案中新增如下依賴:

<dependency>
    <groupId>com.github.xiaoymin</groupId>
    <artifactId>cuckoofilter</artifactId>
    <version>1.1.0</version>
</dependency>
步驟 2:編寫程式碼

接下來,編寫 Java 程式碼來實現 Cuckoo Filter,並用於快取鍵管理的場景。

import com.github.xiaoymin.cuckoofilter.CuckooFilter;
import com.github.xiaoymin.cuckoofilter.CuckooFilterBuilder;
import com.github.xiaoymin.cuckoofilter.CuckooFilterPolicy;
import com.github.xiaoymin.cuckoofilter.CuckooFilterType;

import java.nio.charset.StandardCharsets;
import java.util.Arrays;

public class CuckooFilterDemo {

    public static void main(String[] args) {
        // 建立 Cuckoo Filter 例項,預期插入 10000 個元素,誤報率為 0.01
        CuckooFilter<String> cuckooFilter = new CuckooFilterBuilder<String>()
                .type(CuckooFilterType.LFU)
                .policy(CuckooFilterPolicy.DOUBLE_HASHING)
                .expectedInsertions(10000)
                .fpp(0.01)
                .build();

        // 新增一些快取鍵到 Cuckoo Filter
        addCacheKeyToFilter(cuckooFilter, "product:123");
        addCacheKeyToFilter(cuckooFilter, "product:456");
        addCacheKeyToFilter(cuckooFilter, "product:789");

        // 檢查快取鍵是否存在於 Cuckoo Filter 中
        checkCacheKeyInFilter(cuckooFilter, "product:123"); // 應該返回 true
        checkCacheKeyInFilter(cuckooFilter, "product:000"); // 可能返回 true 或 false,取決於誤報率
    }

    private static void addCacheKeyToFilter(CuckooFilter<String> filter, String cacheKey) {
        byte[] keyBytes = cacheKey.getBytes(StandardCharsets.UTF_8);
        filter.insert(keyBytes);
    }

    private static void checkCacheKeyInFilter(CuckooFilter<String> filter, String cacheKey) {
        byte[] keyBytes = cacheKey.getBytes(StandardCharsets.UTF_8);
        boolean mightBePresent = filter.mightContain(keyBytes);
        System.out.println("Cache Key " + cacheKey + ": " + (mightBePresent ? "Might be in cache" : "Not in cache"));
    }
}
透過上述示例,你可以看到如何使用 cuckoofilter 庫在 Java 中實現 Cuckoo Filter,並將其應用於電商系統的快取鍵管理場景。Cuckoo Filter 的優勢在於支援刪除操作,並且具有較高的精確度,非常適合需要快速判斷元素是否存在的情況。

3/ HyperLogLog:

  • 用途: 計算資料流中的唯一元素數量。
  • 原理: HyperLogLog 使用特殊的雜湊函式和統計方法來估計不同元素的數量。
  • 優點: 非常節省記憶體,對於大資料集尤其有用。
  • 應用: 在大資料分析領域用於計算唯一訪問者數(如網站UV)、網路流量分析等。


應用場景示例
  1. 唯一訪問者統計:可以使用 HyperLogLog 來估計每天或每小時的獨立訪客數(UV)。
  2. 唯一商品點選統計:可以用來估算某段時間內被點選過的不同商品的數量。
  3. 唯一搜尋關鍵詞統計:可以用來統計一段時間內使用者搜尋的不同關鍵詞的數量。
使用 Java 編寫 HyperLogLog 示例

在 Java 中,可以使用第三方庫來實現 HyperLogLog。一個常用的庫是 google/guava,它提供了 HyperLogLog 的實現。下面是一個使用 Guava 庫的示例程式碼,展示如何在電商系統中使用 HyperLogLog 來統計獨立訪客數(UV)。

接下來,編寫 Java 程式碼來實現 HyperLogLog,並用於統計獨立訪客數的場景

import com.google.common.primitives.Ints;
import com.google.common.primitives.UnsignedLong;
import com.google.common.hash.HyperLogLog;
import com.google.common.hash.HyperLogLogCounter;

import java.util.UUID;

public class HyperLogLogExample {

    public static void main(String[] args) {
        // 建立 HyperLogLog 例項,預期誤差率大約為 2%
        HyperLogLog hyperLogLog = HyperLogLog.newCounterBuilder()
                .withExpectedNumElements(100000)
                .withFpp(0.02)
                .build();

        // 模擬獨立訪客數
        int numberOfVisitors = 100000;
        for (int i = 0; i < numberOfVisitors; i++) {
            String visitorId = UUID.randomUUID().toString(); // 模擬每個訪客的唯一識別符號
            hyperLogLog.offer(UnsignedLong.fromIntBits(visitorId.hashCode()));
        }

        // 輸出估計的獨立訪客數
        System.out.println("Estimated number of unique visitors: " + hyperLogLog.count());
    }
}
透過上述示例,你可以看到如何使用 Guava 庫在 Java 中實現 HyperLogLog,並將其應用於電商系統的獨立訪客數統計場景。HyperLogLog 的優點在於它可以非常節省記憶體,同時提供一個足夠準確的基數估計,非常適合需要處理大規模資料集並且對精確度要求不是極高的場景。例如,在實時監控、流量統計、日誌分析等領域都有廣泛的應用。

Redis 自版本 2.8.9 起引入了 HyperLogLog 資料結構,專門用於估計集合中的不同元素數量。HyperLogLog 在 Redis 中的實現非常高效,特別適合於統計獨立訪客(UV)、唯一關鍵詞等場景。
示例:使用 Redis 的 HyperLogLog 統計 UV
import redis.clients.jedis.Jedis;

public class RedisHyperLogLogExample {

    public static void main(String[] args) {
        Jedis jedis = new Jedis("localhost", 6379);
        jedis.flushDB(); // 清空資料庫(僅用於演示)

        // 假設這是來自使用者的請求,我們記錄每個使用者的唯一識別符號
        String[] userIds = {"user1", "user2", "user3", "user1", "user4"};

        // 將使用者識別符號新增到 HyperLogLog 中
        for (String userId : userIds) {
            jedis.pfAdd("unique_visitors", userId);
        }

        // 獲取估計的獨立訪客數
        long estimatedUniqueVisitors = jedis.pfCount("unique_visitors");
        System.out.println("Estimated number of unique visitors: " + estimatedUniqueVisitors);
    }
}

4/ Count-Min Sketch:

  • 用途: 估計資料流中事件的頻率。
  • 原理: 使用多個雜湊函式將元素對映到二維陣列(或計數矩陣)中的位置,並增加計數器。
  • 優點: 節省空間,可以快速得到近似的頻率資訊。
  • 應用: 在網路監控中用來跟蹤流量模式,在搜尋引擎中估算關鍵詞頻率,在資料庫系統中進行聚合查詢加速等。
應用場景示例
  1. 商品點選頻率統計:可以使用 CMS 來統計哪些商品被點選最多,幫助進行商品推薦或廣告投放。
  2. 使用者行為模式分析:可以用來分析使用者對於特定商品類別的偏好,幫助改進商品分類或個性化推薦。
  3. 熱門關鍵詞統計:可以用來統計使用者搜尋中最常出現的關鍵詞,幫助最佳化搜尋引擎或內容推薦。
使用 Java 編寫 Count-Min Sketch 示例

為了實現 Count-Min Sketch,在 Java 中可以自行實現 CMS 的邏輯,或者使用第三方庫。這裡我們提供一個簡單的 CMS 實現示例。

步驟 1:定義 CMS 類

首先,我們需要定義一個 CMS 類,用於初始化矩陣和雜湊函式。

import java.util.Arrays;

public class CountMinSketch {
    private int width;
    private int depth;
    private int[][] matrix;
    private HashFunction[] hashFunctions;

    public CountMinSketch(int width, int depth) {
        this.width = width;
        this.depth = depth;
        this.matrix = new int[depth][width];
        this.hashFunctions = new HashFunction[depth];

        // 初始化雜湊函式
        for (int i = 0; i < depth; i++) {
            hashFunctions[i] = new HashFunction(width);
        }
    }

    public void update(String item, int increment) {
        for (HashFunction f : hashFunctions) {
            int index = f.hash(item);
            matrix[f.getIndex()][index] += increment;
        }
    }

    public int estimate(String item) {
        int minEstimate = Integer.MAX_VALUE;
        for (HashFunction f : hashFunctions) {
            int index = f.hash(item);
            minEstimate = Math.min(minEstimate, matrix[f.getIndex()][index]);
        }
        return minEstimate;
    }

    private class HashFunction {
        private int a;
        private int b;
        private int m;

        public HashFunction(int width) {
            this.a = (int) (Math.random() * width);
            this.b = (int) (Math.random() * width);
            this.m = width;
        }

        public int hash(String item) {
            return ((a * item.hashCode() + b) % m + m) % m; // 防止負數
        }

        public int getIndex() {
            return Arrays.asList(this).indexOf(this);
        }
    }
}
步驟 2:使用 CMS 類

接下來,編寫 Java 程式碼來使用上面定義的 CMS 類,並用於統計電商系統中的商品點選頻率。

public class CountMinSketchExample {

    public static void main(String[] args) {
        // 建立 Count-Min Sketch 例項
        CountMinSketch cms = new CountMinSketch(1000, 5);

        // 模擬商品點選事件
        String[] products = {"ProductA", "ProductB", "ProductC"};
        for (String product : products) {
            for (int i = 0; i < 1000; i++) {
                cms.update(product, 1); // 更新 CMS 計數器
            }
        }

        // 輸出估計的商品點選次數
        System.out.println("Estimated clicks for ProductA: " + cms.estimate("ProductA"));
        System.out.println("Estimated clicks for ProductB: " + cms.estimate("ProductB"));
        System.out.println("Estimated clicks for ProductC: " + cms.estimate("ProductC"));
    }
}
透過上述示例,你可以看到如何在 Java 中實現 Count-Min Sketch,並將其應用於電商系統中的商品點選頻率統計場景。Count-Min Sketch 的主要優勢在於能夠以較小的記憶體消耗提供元素頻率的近似估計,適用於需要快速統計大量資料的情況。不過需要注意的是,CMS 提供的是估計值,並非精確值,因此在需要精確統計的情況下可能不適合。

Redis 沒有直接支援 Count-Min Sketch,但可以使用 Redis 的雜湊表(Hashes)或有序集合(Sorted Sets)來實現 CMS 的矩陣和雜湊函式邏輯。

示例:使用 Redis 的 Hashes 模擬 Count-Min Sketch
import redis.clients.jedis.Jedis;

public class RedisCountMinSketchExample {

    public static void main(String[] args) {
        Jedis jedis = new Jedis("localhost", 6379);
        jedis.flushDB(); // 清空資料庫(僅用於演示)

        // 假設 CMS 的寬度為 1000,深度為 5
        int width = 1000;
        int depth = 5;
        String prefix = "cms_layer_";

        // 模擬商品點選事件
        String[] products = {"ProductA", "ProductB", "ProductC"};
        for (String product : products) {
            for (int i = 0; i < 1000; i++) {
                updateCountMinSketch(jedis, product, 1, width, depth, prefix);
            }
        }

        // 輸出估計的商品點選次數
        System.out.println("Estimated clicks for ProductA: " + estimateFrequency(jedis, "ProductA", width, depth, prefix));
        System.out.println("Estimated clicks for ProductB: " + estimateFrequency(jedis, "ProductB", width, depth, prefix));
        System.out.println("Estimated clicks for ProductC: " + estimateFrequency(jedis, "ProductC", width, depth, prefix));
    }

    private static void updateCountMinSketch(Jedis jedis, String item, int increment, int width, int depth, String prefix) {
        for (int layer = 0; layer < depth; layer++) {
            int index = hash(item, layer, width);
            String key = prefix + layer;
            jedis.hincrBy(key, String.valueOf(index), increment);
        }
    }

    private static long estimateFrequency(Jedis jedis, String item, int width, int depth, String prefix) {
        long minEstimate = Long.MAX_VALUE;
        for (int layer = 0; layer < depth; layer++) {
            int index = hash(item, layer, width);
            String key = prefix + layer;
            long value = jedis.hget(key, String.valueOf(index)) == null ? 0 : Long.parseLong(jedis.hget(key, String.valueOf(index)));
            minEstimate = Math.min(minEstimate, value);
        }
        return minEstimate;
    }

    private static int hash(String item, int layer, int modulo) {
        int a = layer;
        int b = layer * 2;
        return ((a * item.hashCode() + b) % modulo + modulo) % modulo; // 防止負數
    }
}

5/ MinHash:

  • 用途: 估計兩個集合之間的相似度。
  • 原理: MinHash 對集合中的元素應用雜湊函式,產生簽名,透過比較簽名來估計Jaccard相似度。
  • 優點: 在處理大規模資料集時效率高,可以有效地計算相似性。
  • 應用: 在文件檢索系統中檢測重複文件,在推薦系統中計算使用者興趣相似度,在反垃圾郵件系統中檢測垃圾郵件叢集等。
使用 MinHash 進行商品推薦

在這個例子中,我們將展示如何使用 MinHash 來檢測使用者購物籃中的商品相似性,並據此進行商品推薦。

步驟 1:定義 MinHash 類

首先,我們需要定義一個 MinHash 類來實現 MinHash 的邏輯。

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

public class MinHash {

    private List<Integer> signatureMatrix;
    private int numPermutations;

    public MinHash(int numPermutations) {
        this.numPermutations = numPermutations;
        this.signatureMatrix = new ArrayList<>();
        for (int i = 0; i < numPermutations; i++) {
            signatureMatrix.add(Integer.MAX_VALUE);
        }
    }

    public void updateSignature(List<Integer> shingleIds) {
        Random rand = new Random();
        for (int i = 0; i < numPermutations; i++) {
            int a = rand.nextInt(numPermutations);
            int b = rand.nextInt(numPermutations);
            for (int shingleId : shingleIds) {
                int hashedValue = (a * shingleId + b) % numPermutations;
                if (hashedValue < signatureMatrix.get(i)) {
                    signatureMatrix.set(i, hashedValue);
                }
            }
        }
    }

    public List<Integer> getSignature() {
        return signatureMatrix;
    }
}
步驟 2:建立商品資料模型

接著,我們需要建立一個簡單的資料模型來表示商品和使用者的購物籃。

import java.util.HashSet;
import java.util.Set;

public class ShoppingCart {

    private Set<Integer> items;

    public ShoppingCart() {
        this.items = new HashSet<>();
    }

    public void addItem(int itemId) {
        items.add(itemId);
    }

    public Set<Integer> getItems() {
        return items;
    }
}
步驟 3:使用 MinHash 進行商品推薦

現在,我們可以編寫一個主程式來使用 MinHash 來檢測使用者購物籃中的商品相似性,並據此進行商品推薦。

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class MinHashExample {

    public static void main(String[] args) {
        // 建立 MinHash 例項
        int numPermutations = 100;
        MinHash minHashUser1 = new MinHash(numPermutations);
        MinHash minHashUser2 = new MinHash(numPermutations);

        // 建立使用者購物籃
        ShoppingCart shoppingCartUser1 = new ShoppingCart();
        ShoppingCart shoppingCartUser2 = new ShoppingCart();

        // 模擬使用者購物籃資料
        shoppingCartUser1.addItem(1);
        shoppingCartUser1.addItem(2);
        shoppingCartUser1.addItem(3);
        shoppingCartUser1.addItem(4);

        shoppingCartUser2.addItem(2);
        shoppingCartUser2.addItem(3);
        shoppingCartUser2.addItem(5);

        // 轉換購物籃資料為 Shingle ID 列表
        List<Integer> shingleIdsUser1 = new ArrayList<>(shoppingCartUser1.getItems());
        List<Integer> shingleIdsUser2 = new ArrayList<>(shoppingCartUser2.getItems());

        // 更新 MinHash 簽名
        minHashUser1.updateSignature(shingleIdsUser1);
        minHashUser2.updateSignature(shingleIdsUser2);

        // 計算 Jaccard 相似度
        double jaccardSimilarity = calculateJaccardSimilarity(minHashUser1.getSignature(), minHashUser2.getSignature());
        System.out.println("Jaccard Similarity between User 1 and User 2: " + jaccardSimilarity);

        // 根據相似性推薦商品
        recommendProductsBasedOnSimilarity(shoppingCartUser1, shoppingCartUser2);
    }

    private static double calculateJaccardSimilarity(List<Integer> sig1, List<Integer> sig2) {
        int matches = 0;
        for (int i = 0; i < sig1.size(); i++) {
            if (sig1.get(i).equals(sig2.get(i))) {
                matches++;
            }
        }
        return (double) matches / sig1.size();
    }

    private static void recommendProductsBasedOnSimilarity(ShoppingCart user1, ShoppingCart user2) {
        Set<Integer> user1Items = user1.getItems();
        Set<Integer> user2Items = user2.getItems();

        // 找出使用者 2 擁有但使用者 1 沒有的商品
        user2Items.removeAll(user1Items);
        System.out.println("Recommended products for User 1 based on User 2's basket:");
        user2Items.forEach(itemId -> System.out.println("Item ID: " + itemId));
    }
}
程式碼解釋
  1. 定義 MinHash 類

    • updateSignature 方法用於更新 MinHash 簽名矩陣。
    • getSignature 方法用於獲取簽名矩陣。
  2. 建立商品資料模型

    • ShoppingCart 類用於表示使用者的購物籃,其中包含使用者購買的商品 ID。
  3. 使用 MinHash 進行商品推薦

    • 建立兩個使用者的購物籃,並填充一些商品 ID。
    • 將購物籃資料轉換為 Shingle ID 列表,並更新 MinHash 簽名。
    • 計算兩個使用者的 Jaccard 相似度。
    • 根據相似性推薦商品,找出使用者 2 擁有但使用者 1 沒有的商品,並推薦給使用者 1。

透過上述示例,你可以看到如何在 Java 中實現 MinHash,並將其應用於電商系統中的商品推薦場景。MinHash 的主要優點在於它能夠有效地處理大資料集,並快速估計集合之間的相似性,這對於推薦系統來說是非常有用的特性。在實際應用中,還可以結合 LSH (Locality Sensitive Hashing) 技術來進一步提高相似性檢測的效率。

Redis 可以用來儲存 MinHash 簽名,例如使用字串型別來儲存簽名,或者使用雜湊表來儲存多個簽名。

示例:使用 Redis 的 Hashes 模擬 MinHash
import redis.clients.jedis.Jedis;

public class RedisMinHashExample {

    public static void main(String[] args) {
        Jedis jedis = new Jedis("localhost", 6379);
        jedis.flushDB(); // 清空資料庫(僅用於演示)

        // 假設 MinHash 的寬度為 100
        int width = 100;
        String prefix = "min_hash_";

        // 模擬使用者購物籃資料
        String[] itemsUser1 = {"item1", "item2", "item3"};
        String[] itemsUser2 = {"item2", "item3", "item4"};

        // 更新 MinHash 簽名
        updateMinHash(jedis, "user1", itemsUser1, width, prefix);
        updateMinHash(jedis, "user2", itemsUser2, width, prefix);

        // 計算 Jaccard 相似度
        double jaccardSimilarity = calculateJaccardSimilarity(jedis, "user1", "user2", width, prefix);
        System.out.println("Jaccard Similarity between User 1 and User 2: " + jaccardSimilarity);
    }

    private static void updateMinHash(Jedis jedis, String user, String[] items, int width, String prefix) {
        String key = prefix + user;
        for (int i = 0; i < width; i++) {
            int minIndex = Integer.MAX_VALUE;
            for (String item : items) {
                int index = hash(item, i, width);
                minIndex = Math.min(minIndex, index);
            }
            jedis.hset(key, String.valueOf(i), String.valueOf(minIndex));
        }
    }

    private static double calculateJaccardSimilarity(Jedis jedis, String user1, String user2, int width, String prefix) {
        String key1 = prefix + user1;
        String key2 = prefix + user2;
        int matches = 0;
        for (int i = 0; i < width; i++) {
            String val1 = jedis.hget(key1, String.valueOf(i));
            String val2 = jedis.hget(key2, String.valueOf(i));
            if (val1 != null && val2 != null && val1.equals(val2)) {
                matches++;
            }
        }
        return (double) matches / width;
    }

    private static int hash(String item, int layer, int modulo) {
        int a = layer;
        int b = layer * 2;
        return ((a * item.hashCode() + b) % modulo + modulo) % modulo; // 防止負數
    }
}

6/ Skip List:

  • 用途: 提供了一種有序的資料結構,支援快速查詢、插入和刪除操作。
  • 原理: Skip List 是一種基於連結串列的資料結構,它透過多層連結列表實現跳躍機制,每一層都會跳過一定數量的節點。
  • 優點: 相比平衡樹更容易實現,同時提供了對數級別的效能。
  • 應用: 在記憶體管理中實現高效的資料排序,在資料庫管理系統中提供索引功能等。
示例:使用 Skip List 進行商品評分排序

在這個例子中,我們將展示如何使用跳躍列表來實現商品評分的排序功能。我們將會使用 Java 生態中的 ConcurrentSkipListMap 類來實現這個功能。

import java.util.Comparator;
import java.util.concurrent.ConcurrentSkipListMap;

public class SkipListExample {

    public static void main(String[] args) {
        // 建立一個帶有自定義比較器的 ConcurrentSkipListMap
        ConcurrentSkipListMap<ProductRatingPair, Product> productRatingMap = new ConcurrentSkipListMap<>(new Comparator<ProductRatingPair>() {
            @Override
            public int compare(ProductRatingPair o1, ProductRatingPair o2) {
                return Integer.compare(o1.getRating(), o2.getRating()); // 按照評分降序排列
            }
        });

        // 模擬商品及其評分資料
        Product product1 = new Product("Product A", 4);
        Product product2 = new Product("Product B", 3);
        Product product3 = new Product("Product C", 5);
        Product product4 = new Product("Product D", 2);

        // 新增商品到跳躍列表
        productRatingMap.put(new ProductRatingPair(product1.getName(), product1.getRating()), product1);
        productRatingMap.put(new ProductRatingPair(product2.getName(), product2.getRating()), product2);
        productRatingMap.put(new ProductRatingPair(product3.getName(), product3.getRating()), product3);
        productRatingMap.put(new ProductRatingPair(product4.getName(), product4.getRating()), product4);

        // 輸出所有商品按評分排序後的結果
        System.out.println("Sorted Products by Rating:");
        for (ProductRatingPair key : productRatingMap.keySet()) {
            Product product = productRatingMap.get(key);
            System.out.println(product.getName() + " with rating " + product.getRating());
        }

        // 查詢評分大於等於 3 的商品
        System.out.println("\nProducts with rating >= 3:");
        for (ProductRatingPair key : productRatingMap.subMap(new ProductRatingPair("", 3), true, new ProductRatingPair("", 5), true).keySet()) {
            Product product = productRatingMap.get(key);
            System.out.println(product.getName() + " with rating " + product.getRating());
        }
    }

    static class Product {
        private String name;
        private int rating;

        public Product(String name, int rating) {
            this.name = name;
            this.rating = rating;
        }

        public String getName() {
            return name;
        }

        public int getRating() {
            return rating;
        }
    }

    static class ProductRatingPair {
        private String productName;
        private int rating;

        public ProductRatingPair(String productName, int rating) {
            this.productName = productName;
            this.rating = rating;
        }

        public String getProductName() {
            return productName;
        }

        public int getRating() {
            return rating;
        }
    }
}
透過上述示例,你可以看到如何使用 Java 生態中的 ConcurrentSkipListMap 類來實現跳躍列表,並將其應用於電商系統中的商品評分排序場景。跳躍列表的優點在於它提供了高效的查詢、插入和刪除操作,並且在併發環境下也能保證執行緒安全。這對於需要頻繁更新和查詢商品評分的電商系統來說是非常有用的。

Redis 使用了跳躍列表(Skip List)作為其有序集合(Sorted Set)的底層實現之一。當元素數量較少時,Redis 使用字典(雜湊表)來儲存有序集合;當元素數量增加到一定程度時,Redis 會自動切換到跳躍列表來儲存有序集合,以便更有效地支援範圍查詢和排序。在電商系統中,有序集合(Sorted Set)可以被廣泛應用於需要根據某個分數(score)對元素進行排序的場景。


總結

以上每種資料結構都有其獨特的使用場景和優勢,作為工程師瞭解它們可以讓你在面對特定問題時做出更好的技術選擇。


今天先到這兒,希望對雲原生,技術領導力, 企業管理,系統架構設計與評估,團隊管理, 專案管理, 產品管理,資訊保安,團隊建設 有參考作用 , 您可能感興趣的文章:
構建創業公司突擊小團隊
國際化環境下系統架構演化
微服務架構設計
影片直播平臺的系統架構演化
微服務與Docker介紹
Docker與CI持續整合/CD
網際網路電商購物車架構演變案例
網際網路業務場景下訊息佇列架構
網際網路高效研發團隊管理演進之一
訊息系統架構設計演進
網際網路電商搜尋架構演化之一
企業資訊化與軟體工程的迷思
企業專案化管理介紹
軟體專案成功之要素
人際溝通風格介紹一
精益IT組織與分享式領導
學習型組織與企業
企業創新文化與等級觀念
組織目標與個人目標
初創公司人才招聘與管理
人才公司環境與企業文化
企業文化、團隊文化與知識共享
高效能的團隊建設
專案管理溝通計劃
構建高效的研發與自動化運維
某大型電商雲平臺實踐
網際網路資料庫架構設計思路
IT基礎架構規劃方案一(網路系統規劃)
餐飲行業解決方案之客戶分析流程
餐飲行業解決方案之採購戰略制定與實施流程
餐飲行業解決方案之業務設計流程
供應鏈需求調研CheckList
企業應用之效能實時度量系統演變

如有想了解更多軟體設計與架構, 系統IT,企業資訊化, 團隊管理 資訊,請關注我的微信訂閱號:

image_thumb2_thumb_thumb_thumb_thumb[1]

作者:Petter Liu
出處:http://www.cnblogs.com/wintersun/
本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連線,否則保留追究法律責任的權利。 該文章也同時釋出在我的獨立部落格中-Petter Liu Blog。

相關文章