441 lines
13 KiB
Plaintext
441 lines
13 KiB
Plaintext
{
|
|
"cells": [
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "f37ff61d-dfe1-4b65-a74d-efe317bbfade",
|
|
"metadata": {},
|
|
"source": [
|
|
"# Data Structures: Trees, Graphs, and Tries"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "c12455f1-b6e7-4822-8b20-0da02e8e7a11",
|
|
"metadata": {},
|
|
"source": [
|
|
"We saw that linked lists use nodes linked in a linear fashion.\n",
|
|
"\n",
|
|
"Each node had a \"next\" (and possibly a reference to \"prev\").\n",
|
|
"\n",
|
|
"We can use this same idea with additional links to create **Trees**.\n",
|
|
"\n",
|
|
"We'll start with a classic **binary search tree**.\n",
|
|
"\n",
|
|
"Each node has a value, and up to two children, \"left\" and \"right\".\n",
|
|
"\n",
|
|
"Data is stored in the tree such that when a new node is added, if it is less than the current value of a node, it should be stored to the left, and if it is greater, it should be stored to the right.\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 5,
|
|
"id": "012f2a24-f0cc-47ef-a572-3c0825f27850",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"class Node:\n",
|
|
" def __init__(self, value, left=None, right=None):\n",
|
|
" self.value = value\n",
|
|
" self.left = left\n",
|
|
" self.right = right\n",
|
|
"\n",
|
|
" def __str__(self):\n",
|
|
" return f\"({self.value}, {self.left}, {self.right})\"\n",
|
|
"\n",
|
|
"\n",
|
|
"class BST:\n",
|
|
" def __init__(self, iterable=None):\n",
|
|
" self.root = None\n",
|
|
" if iterable:\n",
|
|
" for item in iterable:\n",
|
|
" self.add_item(item)\n",
|
|
"\n",
|
|
" def add_item(self, newval):\n",
|
|
" # special case: first item\n",
|
|
" if self.root is None:\n",
|
|
" self.root = Node(newval)\n",
|
|
" else:\n",
|
|
" parent = self.root\n",
|
|
" # traverse until we find room in the tree\n",
|
|
" while True:\n",
|
|
" if newval < parent.value:\n",
|
|
" if parent.left:\n",
|
|
" parent = parent.left\n",
|
|
" else:\n",
|
|
" parent.left = Node(newval)\n",
|
|
" break\n",
|
|
" else:\n",
|
|
" if parent.right:\n",
|
|
" parent = parent.right\n",
|
|
" else:\n",
|
|
" parent.right = Node(newval)\n",
|
|
" break\n",
|
|
"\n",
|
|
"\n",
|
|
"def print_infix(node):\n",
|
|
" \"\"\"prints items in sorted order\"\"\"\n",
|
|
" if node.left:\n",
|
|
" print_infix(node.left)\n",
|
|
" print(node.value)\n",
|
|
" if node.right:\n",
|
|
" print_infix(node.right)\n",
|
|
" "
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "c833bb06-4b02-4f50-bab9-ab1d30092144",
|
|
"metadata": {},
|
|
"source": [
|
|
"Tree traversal is inherently recursive, so we'll use a recursive function to print the tree in sorted order.\n",
|
|
"\n",
|
|
"Most tree algorithms will operate on the left & right subtrees the same way, so we can write a recursive function that takes a node and calls itself on the left & right subtrees."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 7,
|
|
"id": "4f652851-2fbd-42c1-adb3-cb352657bb5a",
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"Bear\n",
|
|
"Fox\n",
|
|
"Rabbit\n",
|
|
"Raccoon\n",
|
|
"Wolf\n"
|
|
]
|
|
}
|
|
],
|
|
"source": [
|
|
"tree = BST()\n",
|
|
"tree.add_item(\"Fox\")\n",
|
|
"tree.add_item(\"Wolf\")\n",
|
|
"tree.add_item(\"Bear\")\n",
|
|
"tree.add_item(\"Raccoon\")\n",
|
|
"tree.add_item(\"Rabbit\")\n",
|
|
"print_infix(tree.root)\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "44e92462-532b-4340-8c9b-52314870677f",
|
|
"metadata": {},
|
|
"source": [
|
|
"#### Aside: defaultdict\n",
|
|
"\n",
|
|
"```python\n",
|
|
"# common pattern:\n",
|
|
"if key not in dct:\n",
|
|
" dct[key] = []\n",
|
|
"dct[key].append(element)\n",
|
|
"```\n",
|
|
"\n",
|
|
"We can instead use `collections.defaultdict`:"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 6,
|
|
"id": "a06805c2-75ae-46e3-9916-f4ee8cca6080",
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"{1, 2, 3}\n",
|
|
"defaultdict(<function <lambda> at 0x111069120>, {'newkey': {1, 2, 3}})\n",
|
|
"defaultdict(<function <lambda> at 0x111069120>, {'newkey': {1, 2, 3, 4}})\n"
|
|
]
|
|
}
|
|
],
|
|
"source": [
|
|
"from collections import defaultdict\n",
|
|
"\n",
|
|
"# give defaultdict a function that it will use to generate missing keys\n",
|
|
"dd = defaultdict(lambda: {1, 2, 3})\n",
|
|
"\n",
|
|
"print(dd[\"newkey\"])\n",
|
|
"print(dd)\n",
|
|
"\n",
|
|
"dd[\"newkey\"].add(4) # can add to set without ensuring it exists\n",
|
|
"print(dd)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "ba2d9950-e5cf-4d11-9307-f1fa37f01e67",
|
|
"metadata": {},
|
|
"source": [
|
|
"## Graphs\n",
|
|
"\n",
|
|
"![](https://www.simplilearn.com/ice9/free_resources_article_thumb/Graph%20Data%20Structure%20-%20Soni/what-is-graphs-in-data-structure.png)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 13,
|
|
"id": "323a6e33-76b7-41b7-97b9-e4dfa04d6bb4",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"class Graph:\n",
|
|
" def __init__(self):\n",
|
|
" # create a dictionary where every string maps to a set of strings\n",
|
|
" self.edges = defaultdict(set)\n",
|
|
"\n",
|
|
" def add_edge(self, node1, node2):\n",
|
|
" # add in both directions, could alter for directed graph\n",
|
|
" self.edges[node1].add(node2)\n",
|
|
" self.edges[node2].add(node1)\n",
|
|
"\n",
|
|
" def find_path(self, from_node, to_node, seen=None):\n",
|
|
" if not seen:\n",
|
|
" seen = set()\n",
|
|
"\n",
|
|
" if to_node in self.edges[from_node]:\n",
|
|
" return (from_node, to_node)\n",
|
|
" else:\n",
|
|
" for sibling in self.edges[from_node] - seen:\n",
|
|
" return (from_node,) + self.find_path(\n",
|
|
" sibling, to_node, seen | set(sibling)\n",
|
|
" )\n",
|
|
" # return self.find_path("
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 14,
|
|
"id": "867f5186-e308-4d2f-9766-713ecb352f99",
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"data": {
|
|
"text/plain": [
|
|
"('A', 'D', 'B')"
|
|
]
|
|
},
|
|
"execution_count": 14,
|
|
"metadata": {},
|
|
"output_type": "execute_result"
|
|
}
|
|
],
|
|
"source": [
|
|
"g = Graph()\n",
|
|
"g.add_edge(\"A\", \"D\")\n",
|
|
"g.add_edge(\"B\", \"D\")\n",
|
|
"g.find_path(\"A\", \"B\")"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 17,
|
|
"id": "a9acaa28-8a24-41c5-86c0-4342365533f6",
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"data": {
|
|
"text/plain": [
|
|
"('A', 'B', 'A', 'D', 'E')"
|
|
]
|
|
},
|
|
"execution_count": 17,
|
|
"metadata": {},
|
|
"output_type": "execute_result"
|
|
}
|
|
],
|
|
"source": [
|
|
"g = Graph()\n",
|
|
"g.add_edge(\"A\", \"B\")\n",
|
|
"g.add_edge(\"B\", \"C\")\n",
|
|
"g.add_edge(\"C\", \"D\")\n",
|
|
"g.add_edge(\"D\", \"E\")\n",
|
|
"g.add_edge(\"A\", \"D\")\n",
|
|
"g.find_path(\"A\", \"E\")"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "615230a9-5f7c-47c9-9d5e-83d37081bc47",
|
|
"metadata": {},
|
|
"source": [
|
|
"### Discussion\n",
|
|
"\n",
|
|
"* Graphs & Trees in the real world?\n",
|
|
"* Alternate implementations?\n",
|
|
" * NetworkX"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "45d51cd1-9d2b-40dd-95b2-f51f11b49b9e",
|
|
"metadata": {},
|
|
"source": [
|
|
"## Tries\n",
|
|
"\n",
|
|
"Usually pronounced \"try\" to differentiate it from trees.\n",
|
|
"\n",
|
|
"A **trie** is a data structure that stores data associated with string keys similar to a dictionary in many ways. (Python `dict`s are a different data structure: **hash tables**.)\n",
|
|
"\n",
|
|
"A **trie** is a specialized data structure, particularly useful for partial matching of strings. The way the data is stored enables efficient lookup of all strings that start with a given prefix, as well as \"fuzzy search\" where some characters don't match.\n",
|
|
"\n",
|
|
"Each node in a **trie** contains:\n",
|
|
"\n",
|
|
"- an fixed-size array of children\n",
|
|
"- a value\n",
|
|
"\n",
|
|
"Let's imagine a simplified version of a **trie** that can only store string keys with the letters \"a\", \"b\", \"c\", and \"d\".\n",
|
|
"\n",
|
|
"So keys \"a\", \"ba\", \"dddddd\", and \"abcdabcdaabcad\" would all be valid.\n",
|
|
"\n",
|
|
"Now, instead of `linked_list.next` or `tree_node.left`, we will have four children, so we'll store them in a tuple:"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 18,
|
|
"id": "1ed75b91-54fe-4650-965d-ef26507788b9",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"\n",
|
|
"class TrieNode:\n",
|
|
" def __init__(self, value=None):\n",
|
|
" self.value = value\n",
|
|
" self.children = [None, None, None, None]\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "4ad2dec4-dc42-48bd-a144-d5c61d60dfdb",
|
|
"metadata": {},
|
|
"source": [
|
|
"Notice that we **do not store the key**!\n",
|
|
"\n",
|
|
"```python\n",
|
|
"trie = Trie()\n",
|
|
"trie[\"a\"] = 1\n",
|
|
"```\n",
|
|
"\n",
|
|
"Represents a tree with a single key \"a\". The node \"X\" is the 0th child of the root node. It would have no children set, and a value of `1`.\n",
|
|
"\n",
|
|
"```\n",
|
|
" root\n",
|
|
" / \\\\\\\n",
|
|
" X\n",
|
|
"//\\\\\n",
|
|
"```\n",
|
|
"Let's look at a trie where someone has also set `trie[\"aba\"] = 100`\n",
|
|
"\n",
|
|
"\n",
|
|
"```\n",
|
|
" root\n",
|
|
" / \\\\\\\n",
|
|
" X \n",
|
|
" /|\\\\\n",
|
|
" Y\n",
|
|
" /\\\\\\\n",
|
|
" Z \n",
|
|
" //\\\\\n",
|
|
"```\n",
|
|
"\n",
|
|
"Each node has four children, the 0th child being associated with the value \"a\", the 1st with \"b\", and so on.\n",
|
|
"\n",
|
|
"- X is the same as before `value=1`. It now has a child node \"Y\" in 1st position, associated with \"b\". \n",
|
|
"- Y has no `value` set, because it only exists to build out the tree in this case. It has a child at \"a\" position (0).\n",
|
|
"- Z is at a terminal position and would have `value=100`. Since the path from the root is \"aba\" that is the key associated with the value."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "3c397fb5-0503-4d8e-a13d-58b6d5cd6943",
|
|
"metadata": {},
|
|
"source": [
|
|
"### Lookup Algorithm\n",
|
|
"\n",
|
|
"Traversing the tree is done by a simple recursive algorithm:\n",
|
|
"\n",
|
|
"- if there are more letters in the key: convert the next one to an index and traverse to that child node\n",
|
|
"- if there are no more letters: the current node is the destination\n",
|
|
"\n",
|
|
"The correct behavior when encountering a child node that does not (yet) exist depends on the nature of the traversal:\n",
|
|
"\n",
|
|
"In a lookup (such as `__getitem__`) the key in question must not be in the **trie**.\n",
|
|
"If a value was being set, the node should be created.\n",
|
|
"\n",
|
|
"### Note/Project Hint\n",
|
|
"\n",
|
|
"`value=None` will create problems in practice, because you should be able to set `trie[\"abc\"] = None` and not have it treat it as if the data was deleted.\n",
|
|
"\n",
|
|
"Instead, you will probably want to use different values for unset variables. It is common to make a \"sentinel\" class for this, a class that is used to create a unique value (like `None` being the sole instance of `NoneType`.).\n",
|
|
"\n",
|
|
"```python\n",
|
|
"class DefaultColor:\n",
|
|
" \"\"\" Used as a sentinel class. \"\"\"\n",
|
|
"\n",
|
|
"def set_background(color=DefaultColor):\n",
|
|
" \"\"\"\n",
|
|
" This function exists to set the background color.\n",
|
|
" (In reality, to demonstrate a time when you might treat None and an unset value differently.)\n",
|
|
" \n",
|
|
" If color is set to None, the background will be transparent.\n",
|
|
" If color is not set, the background will default to the user's choice.\n",
|
|
" \"\"\"\n",
|
|
" if color is DefaultColor:\n",
|
|
" ...\n",
|
|
"```\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "4aef9799-2f13-4c7c-ae62-ac8e6ed22ca3",
|
|
"metadata": {},
|
|
"source": [
|
|
"### Trie Complexity\n",
|
|
"\n",
|
|
"Trie traversal complexity is `O(m)` where **m** is the length of the key strings. \n",
|
|
"\n",
|
|
"This in practice would likely be much lower than **n**, the number of words in the data.\n",
|
|
"\n",
|
|
"### Discussion\n",
|
|
"\n",
|
|
"- How would prefix lookup work?\n",
|
|
"- Wildcards?"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"id": "0939902d-b50b-4073-92ab-4df03bc1b6fb",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": []
|
|
}
|
|
],
|
|
"metadata": {
|
|
"kernelspec": {
|
|
"display_name": "Python 3 (ipykernel)",
|
|
"language": "python",
|
|
"name": "python3"
|
|
},
|
|
"language_info": {
|
|
"codemirror_mode": {
|
|
"name": "ipython",
|
|
"version": 3
|
|
},
|
|
"file_extension": ".py",
|
|
"mimetype": "text/x-python",
|
|
"name": "python",
|
|
"nbconvert_exporter": "python",
|
|
"pygments_lexer": "ipython3",
|
|
"version": "3.10.15"
|
|
}
|
|
},
|
|
"nbformat": 4,
|
|
"nbformat_minor": 5
|
|
}
|