From c413bb82a4896968fa21c40947d726831c990e4e Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sat, 6 Jul 2024 23:36:33 +0100 Subject: [PATCH 1/8] posts: gap-buffer: add insertion --- content/posts/2024-07-06-gap-buffer/index.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/content/posts/2024-07-06-gap-buffer/index.md b/content/posts/2024-07-06-gap-buffer/index.md index ace8fd9..b23f21a 100644 --- a/content/posts/2024-07-06-gap-buffer/index.md +++ b/content/posts/2024-07-06-gap-buffer/index.md @@ -121,3 +121,22 @@ def grow(self, capacity: int) -> None: self._buf = new_buf self._gap_end += added_capacity ``` + +### Insertion + +Inserting text at the cursor's position means filling up the gap in the middle +of the buffer. To do so we must first make sure that the gap is big enough, or +grow the buffer accordingly. + +Then inserting the text is simply a matter of copying its characters in place, +and moving the start of the gap further right. + +```python +def insert(self, val: str) -> None: + # Ensure we have enouh space to insert the whole string + if len(val) > self.gap_length: + self.grow(max(self.capacity * 2, self.string_length + len(val))) + # Fill the gap with the given string + self._buf[self._gap_start : self._gap_start + len(val)] = val + self._gap_start += len(val) +``` From 6a1c074e3240e1d8312c5fa9fa7f91589c7e0f4e Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sat, 6 Jul 2024 23:36:46 +0100 Subject: [PATCH 2/8] posts: gap-buffer: add deletion --- content/posts/2024-07-06-gap-buffer/index.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/content/posts/2024-07-06-gap-buffer/index.md b/content/posts/2024-07-06-gap-buffer/index.md index b23f21a..9ca44ea 100644 --- a/content/posts/2024-07-06-gap-buffer/index.md +++ b/content/posts/2024-07-06-gap-buffer/index.md @@ -140,3 +140,22 @@ def insert(self, val: str) -> None: self._buf[self._gap_start : self._gap_start + len(val)] = val self._gap_start += len(val) ``` + +### Deletion + +Removing text from the buffer simply expands the gap in the corresponding +direction, shortening the string's prefix/suffix. This makes it very cheap. + +The methods are named after the `backspace` and `delete` keys on the keyboard. + +```python +def backspace(self, dist: int = 1) -> None: + assert dist <= self.prefix_length + # Extend gap to the left + self._gap_start -= dist + +def delete(self, dist: int = 1) -> None: + assert dist <= self.suffix_length + # Extend gap to the right + self._gap_end += dist +``` From f0b3c778623d7d3fbf42a02a608456307c8870f2 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sat, 6 Jul 2024 23:41:31 +0100 Subject: [PATCH 3/8] posts: gap-buffer: add movement --- content/posts/2024-07-06-gap-buffer/index.md | 30 ++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/content/posts/2024-07-06-gap-buffer/index.md b/content/posts/2024-07-06-gap-buffer/index.md index 9ca44ea..763628d 100644 --- a/content/posts/2024-07-06-gap-buffer/index.md +++ b/content/posts/2024-07-06-gap-buffer/index.md @@ -159,3 +159,33 @@ def delete(self, dist: int = 1) -> None: # Extend gap to the right self._gap_end += dist ``` + +### Moving the cursor + +Moving the cursor along the buffer will shift letters from one side of the gap +to the other, moving them accross from prefix to suffix and back. + +I find Python's list slicing not quite as elegant to read as a `memmove`, though +it does make for a very small and efficient implementation. + +```python +def left(self, dist: int = 1) -> None: + assert dist <= self.prefix_length + # Shift the needed number of characters from end of prefix to start of suffix + self._buf[self._gap_end - dist : self._gap_end] = self._buf[ + self._gap_start - dist : self._gap_start + ] + # Adjust indices accordingly + self._gap_start -= dist + self._gap_end -= dist + +def right(self, dist: int = 1) -> None: + assert dist <= self.suffix_length + # Shift the needed number of characters from start of suffix to end of prefix + self._buf[self._gap_start : self._gap_start + dist] = self._buf[ + self._gap_end : self._gap_end + dist + ] + # Adjust indices accordingly + self._gap_start += dist + self._gap_end += dist +``` From 987078068f8eacfa65014c5f7075cf7c1e2df721 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 14 Jul 2024 17:53:25 +0100 Subject: [PATCH 4/8] posts: add bloom-filter --- .../posts/2024-07-14-bloom-filter/index.md | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 content/posts/2024-07-14-bloom-filter/index.md diff --git a/content/posts/2024-07-14-bloom-filter/index.md b/content/posts/2024-07-14-bloom-filter/index.md new file mode 100644 index 0000000..98cfc1e --- /dev/null +++ b/content/posts/2024-07-14-bloom-filter/index.md @@ -0,0 +1,26 @@ +--- +title: "Bloom Filter" +date: 2024-07-14T17:46:40+01:00 +draft: false # I don't care for draft mode, git has branches for that +description: "Probably cool" +tags: + - algorithms + - data structures + - python +categories: + - programming +series: +- Cool algorithms +favorite: false +disable_feed: false +--- + +The [_Bloom Filter_][wiki] is a probabilistic data structure for set membership. + +The filter can be used as an inexpensive first step when querying the actual +data is quite costly (e.g: as a first check for expensive cache lookups or large +data seeks). + +[wiki]: https://en.wikipedia.org/wiki/Bloom_filter + + From aea5587742f1198411e1b81d757f4a4a976f0a4f Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 14 Jul 2024 17:54:59 +0100 Subject: [PATCH 5/8] posts: bloom-filter: add presentation --- content/posts/2024-07-14-bloom-filter/index.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/content/posts/2024-07-14-bloom-filter/index.md b/content/posts/2024-07-14-bloom-filter/index.md index 98cfc1e..0a82882 100644 --- a/content/posts/2024-07-14-bloom-filter/index.md +++ b/content/posts/2024-07-14-bloom-filter/index.md @@ -24,3 +24,16 @@ data seeks). [wiki]: https://en.wikipedia.org/wiki/Bloom_filter + +## What does it do? + +A _Bloom Filter_ can be understood as a hash-set which can either tell you: + +* An element is _not_ part of the set. +* An element _may be_ part of the set. + +More specifically, one can tweak the parameters of the filter to make it so that +the _false positive_ rate of membership is quite low. + +I won't be going into those calculations here, but they are quite trivial to +compute, or one can just look up appropriate values for their use case. From dda444bdc0c305366803df8ec30b039294bfead3 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 14 Jul 2024 17:55:15 +0100 Subject: [PATCH 6/8] posts: bloom-filter: add construction --- .../posts/2024-07-14-bloom-filter/index.md | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/content/posts/2024-07-14-bloom-filter/index.md b/content/posts/2024-07-14-bloom-filter/index.md index 0a82882..547d50f 100644 --- a/content/posts/2024-07-14-bloom-filter/index.md +++ b/content/posts/2024-07-14-bloom-filter/index.md @@ -37,3 +37,28 @@ the _false positive_ rate of membership is quite low. I won't be going into those calculations here, but they are quite trivial to compute, or one can just look up appropriate values for their use case. + +## Implementation + +I'll be using Python, which has the nifty ability of representing bitsets +through its built-in big integers quite easily. + +We'll be assuming a `BIT_COUNT` of 64 here, but the implementation can easily be +tweaked to use a different number, or even change it at construction time. + +### Representation + +A `BloomFilter` is just a set of bits and a list of hash functions. + +```python +BIT_COUNT = 64 + +class BloomFilter[T]: + _bits: int + _hash_functions: list[Callable[[T], int]] + + def __init__(self, hash_functions: list[Callable[[T], int]]) -> None: + # Filter is initially empty + self._bits = 0 + self._hash_functions = hash_functions +``` From 0030310952f98e09a980c8964a638067ad74fba6 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 14 Jul 2024 17:55:33 +0100 Subject: [PATCH 7/8] posts: bloom-filter: add insertion --- content/posts/2024-07-14-bloom-filter/index.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/content/posts/2024-07-14-bloom-filter/index.md b/content/posts/2024-07-14-bloom-filter/index.md index 547d50f..1d593a7 100644 --- a/content/posts/2024-07-14-bloom-filter/index.md +++ b/content/posts/2024-07-14-bloom-filter/index.md @@ -62,3 +62,18 @@ class BloomFilter[T]: self._bits = 0 self._hash_functions = hash_functions ``` + +### Inserting a key + +To add an element to the filter, we take the output from each hash function and +use that to set a bit in the filter. This combination of bit will identify the +element, which we can use for lookup later. + +```python +def insert(self, val: T) -> None: + # Iterate over each hash + for f in self._hash_functions: + n = f(val) % BIT_COUNT + # Set the corresponding bit + self._bit |= 1 << n +``` From 5e3ba4fb04e1509b0e43d75c5551f1f0c2fd5e2d Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 14 Jul 2024 17:56:33 +0100 Subject: [PATCH 8/8] posts: bloom-filter: add lookup --- content/posts/2024-07-14-bloom-filter/index.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/content/posts/2024-07-14-bloom-filter/index.md b/content/posts/2024-07-14-bloom-filter/index.md index 1d593a7..93107d4 100644 --- a/content/posts/2024-07-14-bloom-filter/index.md +++ b/content/posts/2024-07-14-bloom-filter/index.md @@ -77,3 +77,21 @@ def insert(self, val: T) -> None: # Set the corresponding bit self._bit |= 1 << n ``` + +### Querying a key + +Because the _Bloom Filter_ does not actually store its elements, but some +derived data from hashing them, it can only definitely say if an element _does +not_ belong to it. Otherwise, it _may_ be part of the set, and should be checked +against the actual underlying store. + +```python +def may_contain(self, val: T) -> bool: + for f in self._hash_functions: + n = f(val) % BIT_COUNT + # If one of the bits is unset, the value is definitely not present + if not (self._bit & (1 << n)): + return False + # All bits were matched, `val` is likely to be part of the set + return True +```