├── .github └── FUNDING.yml ├── .gitignore ├── 163_course.png ├── LICENSE ├── Makefile ├── README.md ├── docs ├── 00_课程简介之笨方法学算法 │ └── why_and_how_to_learn.md ├── 01_抽象数据类型和面向对象编程 │ ├── ADT_OOP.md │ └── bag_adt.py ├── 02_数组和列表 │ ├── array_and_list.md │ ├── array_and_list.py │ └── list.png ├── 03_链表 │ ├── double_link_list.py │ ├── linked_list.md │ ├── linked_list.py │ └── lru_cache.py ├── 04_队列 │ ├── array_queue.png │ ├── array_queue.py │ ├── deque.py │ ├── queue.md │ └── queue.py ├── 05_栈 │ ├── stack.md │ └── stack.py ├── 06_算法分析 │ ├── big_o.md │ └── function_growth.png ├── 07_哈希表 │ ├── hashtable.md │ ├── hashtable.py │ ├── insert_hash.png │ ├── insert_hash_chaining.png │ ├── quadratic_hash.png │ └── quadratic_result.png ├── 08_字典 │ ├── dict.md │ └── dict_adt.py ├── 09_集合 │ ├── set.md │ ├── set.png │ └── set_adt.py ├── 10_递归 │ ├── fact.png │ ├── hanoi.gif │ ├── hanoi_four_disks.png │ ├── hanoi_tower.png │ ├── print_rec.png │ ├── recursion.md │ └── recursion.py ├── 11_线性查找与二分查找 │ ├── search.md │ └── search.py ├── 12_基本排序算法 │ ├── basic_sort.md │ └── basic_sort.py ├── 13_高级排序算法 │ ├── advanced_sorting.md │ ├── merge_sort.md │ ├── merge_sort.py │ ├── merge_sort_merge.png │ ├── merge_sort_recursion_tree.png │ ├── merge_sort_split.png │ ├── merge_sorted_array.png │ ├── merge_sorted_array_2.png │ ├── partition.png │ ├── quick_sort.md │ ├── quick_sort.png │ ├── quicksort.py │ ├── quicksort_worst.png │ ├── test.sh │ └── tn.png ├── 14_树与二叉树 │ ├── binary_tree.png │ ├── binary_tree_level.png │ ├── btree.py │ ├── complete_binary_tree.png │ ├── family_tree.png │ ├── full_binary_tree.png │ ├── perfect_binary_tree.png │ ├── preorder.png │ ├── tree.md │ └── tree.png ├── 15_堆与堆排序 │ ├── heap.png │ ├── heap_and_heapsort.md │ ├── heap_and_heapsort.py │ ├── heap_array.png │ ├── lfu.py │ ├── min_heap.py │ ├── siftdown.png │ ├── siftup.png │ └── topk.py ├── 163_course.png ├── 16_优先级队列 │ ├── priority_queue.md │ └── priority_queue.py ├── 17_二叉查找树 │ ├── binary_search_tree.md │ ├── bst.png │ ├── bst.py │ ├── bst_insert.png │ ├── bst_remove_leaf.png │ ├── bst_remove_node_with_one_child.png │ ├── bst_search.png │ ├── bst_worstcase.png │ ├── find_successor.png │ ├── predecessor_successor.png │ └── remove_interior_replace.png ├── 18_图与图的遍历 │ ├── bfs.png │ ├── graph.md │ ├── graph.py │ ├── graph_rep.png │ └── graph_road.png ├── 19_python内置常用算法和数据结构 │ └── builtins.md ├── 20_面试指南 │ └── interview.md └── index.md ├── mkdocs.yml ├── requirements.txt └── 剑指offer ├── 03_FindInPartiallySortedMatrix(二维数组中的查找).py ├── 04_ReplaceBlank(替换空格).py ├── 05_PrintListInReversedOrder(从尾到头打印链表).py ├── 06_ConstructBinaryTree(重建二叉树).py ├── 07_QueueWithTwoStacks(用两个栈实现队列).py ├── 08_MinNumberInRotatedArray(旋转数组最小数字).py ├── 10_NumberOf1InBinary(二进制中1的个数).py ├── 12_Print1ToMaxOfNDigits(打印1到最大的n位数).py ├── 13_DeleteNodeInList(O1时间删除链表节点).py ├── 14_ReorderArray(调整奇偶顺序).py ├── 15_KthNodeFromEnd(链表倒数第k个节点).py ├── 16_ReverseList(翻转链表).py ├── 17_MergeSortedLists(合并两个有序链表).py ├── 18_SubstructureInTree(树的子结构).py ├── 19_MirrorOfBinaryTree(二叉树镜像).py ├── 20_PrintMatrix(螺旋矩阵).py ├── 21_MinInStack(包含min 函数的栈).py ├── 22_StackPushPopOrder(栈的压入弹出序列).py ├── 23_BfsTree(层序从上往下打印二叉树).py ├── 24_SquenceOfBST(二叉搜索树的后序遍历序列).py ├── 25_PathInTree(二叉树和为某一个值).py ├── 27_ConvertBinarySearchTree(二叉搜索树转成双向链表).py ├── 28_StringPermutation(字符串全排列).py ├── 29_MoreThanHalfNumber(数组中出现次数超过一半的数字).py ├── 30_KLeastNumbers(最小的 k 个数).py ├── 31_GreatestSumOfSubarrays(连续子数组最大和).py ├── 32_NumberOf1(从1到n整数中1出现的次数).py ├── 33_SortArrayForMinNumber(把数组排成最小的数).py ├── 34_UglyNumber(丑数).py ├── 35_FirstNotRepeatingChar(第一个只出现一次的字符).py ├── 36_InversePairs(数组中的逆序对).py ├── 37_FirstCommonNodesInLists(两个链表的第一个公共结点).py ├── 38_NumberOfK(数字在排序数组中出现的次数).py ├── 39_1_TreeDepth(二叉树深度).py ├── 40_NumbersAppearOnce(数组中只出现一次的数字).py ├── 41_1_TwoNumbersWithSum(和为s的两个数字VS和为s的连续正数序列).py └── README.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [PegasusWang] 4 | custom: ["https://www.paypal.me/pegasuswang"] 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # media 2 | *.mp4 3 | *.mov 4 | *.avi 5 | 6 | 7 | # python 8 | __pycache__/ 9 | *.py[cod] 10 | *.pyc 11 | *$py.class 12 | .python-version 13 | 14 | 15 | # vim 16 | .vimrc 17 | .lvimrc 18 | .*.sw[a-z] 19 | *.un~ 20 | Session.vim 21 | 22 | # cache 23 | .cache 24 | .tmp 25 | .idea 26 | 27 | # mkdocs 28 | site/ 29 | 30 | # vscode 31 | .vscode/ 32 | 33 | # pytest 34 | .pytest_cache/ 35 | -------------------------------------------------------------------------------- /163_course.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PegasusWang/python_data_structures_and_algorithms/3469a79c34b6c08ae52797c3974b49fbfa8cca51/163_course.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 PegasusWang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | push: 2 | git push origin master 3 | 4 | serve: 5 | mkdocs serve 6 | 7 | publish: 8 | git push origin master 9 | mkdocs gh-deploy 10 | -------------------------------------------------------------------------------- /docs/00_课程简介之笨方法学算法/why_and_how_to_learn.md: -------------------------------------------------------------------------------- 1 | # 什么是算法和数据结构? 2 | 3 | 你可能会在一些教材上看到这句话: 4 | 5 | 程序 = 算法 + 数据结构 6 | 7 | 算法(Algorithm):是指解题方案的准确而完整的描述,是一系列解决问题的清晰指令,算法代表着用系统的方法描述解决问题的策略机制。也就是说,能够对一定规范的输入,在有限时间内获得所要求的输出。 8 | 9 | 数据结构(Data Structures):是计算机存储和组织数据的一种方式,可以用来高效地处理数据。 10 | 11 | 举个例子:二分查找就是一个非常经典的算法,而二分查找经常需要作用在一个有序数组上。这里二分就是一种折半的算法思想, 12 | 而数组是我们最常用的一种数据结构,支持根据下标快速访问。很多算法需要特定的数据结构来实现,所以经常把它们放到一块讲。 13 | 14 | 实际上,在真正的项目开发中,大部分时间都是 从数据库取数据 -> 数据操作和结构化 -> 返回给前端,在数据操作过程中需要合理地抽象, 15 | 组织、处理数据,如果选用了错误的数据结构,就会造成代码运行低效。这也是我们需要学习算法和数据结构的原因。 16 | 17 | # 笨方法学算法 18 | 这里我们用一种很原始的『笨』方法来学习算法:纸笔模拟。 19 | 20 | - 阅读资料了解算法思想 21 | - 纸笔模拟尝试理解 22 | - 用自己熟悉的编程语言来实现 23 | - 单测 24 | 25 | # 小问题 26 | 27 | - 你还知道哪些经典的算法和数据结构? 28 | - 学习算法你觉得需要哪些预备知识? 29 | - 我们的业务代码开发中会涉及到算法吗? 30 | - 你了解 redis 吗,你知道它有哪几个常用的数据结构吗?你知道它的底层实现方式吗? 31 | -------------------------------------------------------------------------------- /docs/01_抽象数据类型和面向对象编程/ADT_OOP.md: -------------------------------------------------------------------------------- 1 | # Python 一切皆对象 2 | 3 | 举个例子,在 python 中我们经常使用的 list 4 | 5 | ```py 6 | l = list() # 实例化一个 list 对象 l 7 | l.append(1) # 调用 l 的 append 方法 8 | l.append(2) 9 | l.remove(1) 10 | print(len(l)) # 调用对象的 `__len__` 方法 11 | ``` 12 | 13 | 在后面实现新的数据类型时,我们将使用 python 的 class 实现,它包含属性和方法。 14 | 属性一般是使用某种特定的数据类型,而方法一般是对属性的操作。 15 | 这里你只需了解这么多就行了, 我们不会使用继承等特性。 16 | 17 | 18 | # 什么是抽象数据类型 ADT 19 | 20 | 实际上 python 内置的 list 就可以看成一种抽象数据类型。 21 | 22 | ADT: Abstract Data Type,抽象数据类型,我们在组合已有的数据结构来实现一种新的数据类型, ADT 定义了类型的数据和操作。 23 | 24 | 我们以抽象一个背包(Bag) 数据类型来说明,背包是一种容器类型,我们可以给它添加东西,也可以移除东西,并且我们想知道背包里 25 | 有多少东西。于是我们可以定义一个新的数据类型叫做 Bag. 26 | 27 | ```py 28 | class Bag: 29 | """ 背包类型 """ 30 | pass 31 | ``` 32 | 33 | 34 | # 实现一个 Bag ADT 35 | 视频中我们将使用 python 的 class 来实现一个新的容器类型叫做 Bag。 36 | 37 | 38 | # 实现 ADT 我们应该注意什么? 39 | - 如何选用恰当的数据结构作为存储? 40 | - 选取的数据结构能否满足 ADT 的功能需求 41 | - 实现效率如何? 42 | 43 | 44 | # 小问题: 45 | - 你了解 python 的魔术方法吗? 比如 `__len__` ,调用 len(l) 的时候发生了什么? 46 | - 你了解单测吗?我们以后将使用 pytest 运行单元测试,保证我们实现的数据结构和算法是正确的。你可以网上搜索下它的简单用法 47 | 48 | # 延伸阅读: 49 | 50 | [数据结构与算法--ADT](http://www.atjiang.com/data-structures-using-python-ADT/) 51 | 52 | [http://www.nhu.edu.tw/~chun/CS-ch12-Abstract%20Data%20Types.pdf](http://www.nhu.edu.tw/~chun/CS-ch12-Abstract%20Data%20Types.pdf) 53 | -------------------------------------------------------------------------------- /docs/01_抽象数据类型和面向对象编程/bag_adt.py: -------------------------------------------------------------------------------- 1 | # coding: utf8 2 | 3 | 4 | class Bag(object): 5 | 6 | def __init__(self, maxsize=10): 7 | self.maxsize = maxsize 8 | self._items = list() 9 | 10 | def add(self, item): 11 | if len(self) >= self.maxsize: 12 | raise Exception('Full') 13 | self._items.append(item) 14 | 15 | def remove(self, item): 16 | self._items.remove(item) 17 | 18 | def __len__(self): 19 | return len(self._items) 20 | 21 | def __iter__(self): 22 | for item in self._items: 23 | yield item 24 | 25 | 26 | def test_bag(): 27 | bag = Bag() 28 | 29 | bag.add(1) 30 | bag.add(2) 31 | bag.add(3) 32 | 33 | assert len(bag) == 3 34 | 35 | bag.remove(3) 36 | assert len(bag) == 2 37 | 38 | for i in bag: 39 | print(i) 40 | 41 | 42 | if __name__ == '__main__': 43 | test_bag() 44 | -------------------------------------------------------------------------------- /docs/02_数组和列表/array_and_list.md: -------------------------------------------------------------------------------- 1 | # 线性结构 2 | 本节我们从最简单和常用的线性结构开始,并结合 Python 语言本身内置的数据结构和其底层实现方式来讲解。 3 | 虽然本质上数据结构的思想是语言无关的,但是了解 Python 的实现方式有助于你避免一些坑。 4 | 5 | 我们会在代码中注释出操作的时间复杂度。 6 | 7 | 8 | # 数组 array 9 | 10 | 数组是最常用到的一种线性结构,其实 python 内置了一个 array 模块,但是大部人甚至从来没用过它。 11 | Python 的 array 是内存连续、存储的都是同一数据类型的结构,而且只能存数值和字符。 12 | 13 | 我建议你课下看下 array 的文档:https://docs.python.org/2/library/array.html 14 | 15 | 你可能很少会使用到它(我推荐你用 numpy.array),我将在视频里简单介绍下它的使用和工作方式,最常用的还是接下来要说的 list, 16 | 本章最后我们会用 list 来实现一个固定长度、并且支持所有 Python 数据类型的数组 Array. 17 | 18 | 19 | # 列表 list 20 | 如果你学过 C++,list 其实和 C++ STL(标准模板库)中的 vector 很类似,它可能是你的 Python 学习中使用最频繁的数据结构之一。 21 | 这里我们不再去自己实现 list,因为这是个 Python 提供的非常基础的数据类型,我会在视频中讲解它的工作方式和内存分配策略, 22 | 避免使用过程中碰到一些坑。当然如果你有毅力或者兴趣的了解底层是如何实现的,可以看看 cpython 解释器的具体实现。 23 | 24 | 25 | 操作 | 平均时间复杂度 | 26 | --------------------------------------|----------------| 27 | list[index] | O(1) | 28 | list.append | O(1) | 29 | list.insert | O(n) | 30 | list.pop(index), default last element | O(1) | 31 | list.remove | O(n) | 32 | 33 | ![](./list.png) 34 | 35 | # 用 list 实现 Array ADT 36 | 讲完了 list 让我们来实现一个定长的数组 Array ADT,在其他一些语言中,内置的数组结构就是定长的。 37 | 这里我们会使用 list 作为 Array 的一个成员(代理)。具体请参考视频讲解和代码示例,后边我们会使用到这个 Array 类。 38 | 39 | 40 | # 小问题 41 | - 你知道线性结构的查找,删除,访问一个元素的平均时间复杂度吗?(后边我们会介绍这个概念,现在你可以简单地理解为一个操作需要的平均步骤) 42 | - list 内存重新分配的时候为什么要有冗余?不会浪费空间吗? 43 | - 当你频繁的pop list 的第一个元素的时候,会发生什么?如果需要频繁在两头增添元素,你知道更高效的数据结构吗?后边我们会讲到 44 | 45 | 46 | # 延伸阅读 47 | 48 | [Python list implementation](https://www.laurentluce.com/posts/python-list-implementation/) 49 | 50 | [https://github.com/python/cpython/blob/master/Objects/listobject.c](https://github.com/python/cpython/blob/master/Objects/listobject.c) 51 | 52 | 53 | # 勘误 54 | 视频里的 Array.clear 方法有误。应该是 `for i in range(len(self._items))`,已经在后续所有使用到 Array 的代码里修正 55 | -------------------------------------------------------------------------------- /docs/02_数组和列表/array_and_list.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # https://docs.python.org/2/library/array.html 4 | from array import array # python 提供的比较原始的 array 类 5 | 6 | 7 | arr = array('u', 'asdf') 8 | 9 | print(arr[0], arr[1], arr[2], arr[3]) 10 | 11 | 12 | # 实现定长的 Array ADT,省略了边界检查等 13 | 14 | class Array(object): 15 | 16 | def __init__(self, size=32): 17 | self._size = size 18 | self._items = [None] * size 19 | 20 | def __getitem__(self, index): 21 | return self._items[index] 22 | 23 | def __setitem__(self, index, value): 24 | self._items[index] = value 25 | 26 | def __len__(self): 27 | return self._size 28 | 29 | def clear(self, value=None): 30 | for i in range(len(self._items)): 31 | self._items[i] = value 32 | 33 | def __iter__(self): 34 | for item in self._items: 35 | yield item 36 | 37 | 38 | def test_array(): 39 | size = 10 40 | a = Array(size) 41 | a[0] = 1 42 | assert a[0] == 1 43 | assert len(a) == 10 44 | 45 | # py.test array_and_list.py 46 | -------------------------------------------------------------------------------- /docs/02_数组和列表/list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PegasusWang/python_data_structures_and_algorithms/3469a79c34b6c08ae52797c3974b49fbfa8cca51/docs/02_数组和列表/list.png -------------------------------------------------------------------------------- /docs/03_链表/double_link_list.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | class Node(object): 5 | __slots__ = ('value', 'prev', 'next') # save memory 6 | 7 | def __init__(self, value=None, prev=None, next=None): 8 | self.value, self.prev, self.next = value, prev, next 9 | 10 | 11 | class CircularDoubleLinkedList(object): 12 | """循环双端链表 ADT 13 | 多了个循环其实就是把 root 的 prev 指向 tail 节点,串起来 14 | """ 15 | 16 | def __init__(self, maxsize=None): 17 | self.maxsize = maxsize 18 | node = Node() 19 | node.next, node.prev = node, node 20 | self.root = node 21 | self.length = 0 22 | 23 | def __len__(self): 24 | return self.length 25 | 26 | def headnode(self): 27 | return self.root.next 28 | 29 | def tailnode(self): 30 | return self.root.prev 31 | 32 | def append(self, value): # O(1), 你发现一般不用 for 循环的就是 O(1),有限个步骤 33 | if self.maxsize is not None and len(self) >= self.maxsize: 34 | raise Exception('LinkedList is Full') 35 | node = Node(value=value) 36 | tailnode = self.tailnode() or self.root 37 | 38 | tailnode.next = node 39 | node.prev = tailnode 40 | node.next = self.root 41 | self.root.prev = node 42 | self.length += 1 43 | 44 | def appendleft(self, value): 45 | if self.maxsize is not None and len(self) >= self.maxsize: 46 | raise Exception('LinkedList is Full') 47 | node = Node(value=value) 48 | if self.root.next is self.root: # empty 49 | node.next = self.root 50 | node.prev = self.root 51 | self.root.next = node 52 | self.root.prev = node 53 | else: 54 | node.prev = self.root 55 | headnode = self.root.next 56 | node.next = headnode 57 | headnode.prev = node 58 | self.root.next = node 59 | self.length += 1 60 | 61 | def remove(self, node): # O(1),传入node 而不是 value 我们就能实现 O(1) 删除 62 | """remove 63 | :param node # 在 lru_cache 里实际上根据key 保存了整个node: 64 | """ 65 | if node is self.root: 66 | return 67 | else: # 68 | node.prev.next = node.next 69 | node.next.prev = node.prev 70 | self.length -= 1 71 | return node 72 | 73 | def iter_node(self): 74 | if self.root.next is self.root: 75 | return 76 | curnode = self.root.next 77 | while curnode.next is not self.root: 78 | yield curnode 79 | curnode = curnode.next 80 | yield curnode 81 | 82 | def __iter__(self): 83 | for node in self.iter_node(): 84 | yield node.value 85 | 86 | def iter_node_reverse(self): 87 | """相比单链表独有的反序遍历""" 88 | if self.root.prev is self.root: 89 | return 90 | curnode = self.root.prev 91 | while curnode.prev is not self.root: 92 | yield curnode 93 | curnode = curnode.prev 94 | yield curnode 95 | 96 | 97 | def test_double_link_list(): 98 | dll = CircularDoubleLinkedList() 99 | assert len(dll) == 0 100 | 101 | dll.append(0) 102 | dll.append(1) 103 | dll.append(2) 104 | 105 | assert list(dll) == [0, 1, 2] 106 | 107 | assert [node.value for node in dll.iter_node()] == [0, 1, 2] 108 | assert [node.value for node in dll.iter_node_reverse()] == [2, 1, 0] 109 | 110 | headnode = dll.headnode() 111 | assert headnode.value == 0 112 | dll.remove(headnode) 113 | assert len(dll) == 2 114 | assert [node.value for node in dll.iter_node()] == [1, 2] 115 | 116 | dll.appendleft(0) 117 | assert [node.value for node in dll.iter_node()] == [0, 1, 2] 118 | 119 | 120 | if __name__ == '__main__': 121 | test_double_link_list() 122 | -------------------------------------------------------------------------------- /docs/03_链表/linked_list.md: -------------------------------------------------------------------------------- 1 | # 链式结构 2 | 3 | 上一节讲到了支持随机访问的线性结构,这次我们开始讲链式结构, 视频里我会说下这两种结构的区别,然后讲解最常见的单链表和双链表。 4 | 之前在专栏文章[那些年,我们一起跪过的算法题[视频]](https://zhuanlan.zhihu.com/p/35175401)里实现过一个 lru_cache, 5 | 使用到的就是循环双端链表,如果感觉这篇文章有点难理解,我们这里将会循序渐进地来实现。 6 | 后边讲到哈希表的冲突解决方式的时候,我们会再次提到链表。 7 | 8 | 上一节我们分析了 list 的各种操作是如何实现的,如果你还有印象的话,list 9 | 在头部进行插入是个相当耗时的操作(需要把后边的元素一个一个挪个位置)。假如你需要频繁在数组两头增删,list 就不太合适。 10 | 今天我们介绍的链式结构将摆脱这个缺陷,当然了链式结构本身也有缺陷,比如你不能像数组一样随机根据下标访问,你想查找一个元素只能老老实实从头遍历。 11 | 所以嘛,学习和了解数据结构的原理和实现你才能准确地选择到底什么时候该用什么数据结构,而不是瞎选导致代码性能很差。 12 | 13 | 14 | # 单链表 15 | 和线性结构不同,链式结构内存不连续的,而是一个个串起来的,这个时候就需要每个链接表的节点保存一个指向下一个节点的指针。 16 | 这里可不要混淆了列表和链表(它们的中文发音类似,但是列表 list 底层其实还是线性结构,链表才是真的通过指针关联的链式结构)。 17 | 看到指针你也不用怕,这里我们用的 python,你只需要一个简单赋值操作就能实现,不用担心 c 语言里复杂的指针。 18 | 19 | 先来定义一个链接表的节点,刚才说到有一个指针保存下一个节点的位置,我们叫它 next, 当然还需要一个 value 属性保存值 20 | 21 | ```py 22 | class Node(object): 23 | def __init__(self, value, next=None): 24 | self.value = value 25 | self.next = next 26 | ``` 27 | 然后就是我们的单链表 LinkedList ADT: 28 | 29 | ```py 30 | class LinkedList(object): 31 | """ 链接表 ADT 32 | [root] -> [node0] -> [node1] -> [node2] 33 | """ 34 | ``` 35 | 实现我们会在视频中用画图来模拟并且手动代码实现,代码里我们会标识每个步骤的时间复杂度。这里请高度集中精力, 36 | 虽然链表的思想很简单,但是想要正确写对链表的操作代码可不容易,稍不留神就可能丢失一些步骤。 37 | 这里我们还是会用简单的单测来验证代码是否按照预期工作。 38 | 39 | 来看下时间复杂度: 40 | 41 | 链表操作 | 平均时间复杂度 | 42 | ------------------------------|----------------| 43 | linked_list.append(value) | O(1) | 44 | linked_list.appendleft(value) | O(1) | 45 | linked_list.find(value) | O(n) | 46 | linked_list.remove(value) | O(n) | 47 | 48 | 49 | # 双链表 50 | 上边我们亲自实现了一个单链表,但是能看到很明显的问题,单链表虽然 append 是 O(1),但是它的 find 和 remove 都是 O(n)的, 51 | 因为删除你也需要先查找,而单链表查找只有一个方式就是从头找到尾,中间找到才退出。 52 | 这里我之前提到过如果要实现一个 lru 缓存(访问时间最久的踢出),我们需要在一个链表里能高效的删除元素, 53 | 并把它追加到访问表的最后一个位置,这个时候单链表就满足不了了, 54 | 因为缓存在 dict 里查找的时间是 O(1),你更新访问顺序就 O(n)了,缓存就没了优势。 55 | 56 | 这里就要使用到双链表了,相比单链表来说,每个节点既保存了指向下一个节点的指针,同时还保存了上一个节点的指针。 57 | 58 | ```py 59 | class Node(object): 60 | # 如果节点很多,我们可以用 __slots__ 来节省内存,把属性保存在一个 tuple 而不是 dict 里 61 | # 感兴趣可以自行搜索 python __slots__ 62 | __slots__ = ('value', 'prev', 'next') 63 | 64 | def __init__(self, value=None, prev=None, next=None): 65 | self.value, self.prev, self.next = value, prev, next 66 | ``` 67 | 68 | 对, 就多了 prev,有啥优势嘛? 69 | 70 | - 看似我们反过来遍历双链表了。反过来从哪里开始呢?我们只要让 root 的 prev 指向 tail 节点,不就串起来了吗? 71 | - 直接删除节点,当然如果给的是一个值,我们还是需要查找这个值在哪个节点? - 但是如果给了一个节点,我们把它拿掉,直接让它的前后节点互相指过去不就行了?哇欧,删除就是 O(1) 了,两步操作就行啦 72 | 73 | 好,废话不多说,我们在视频里介绍怎么实现一个双链表 ADT。你可以直接在本项目的 `docs/03_链表/double_link_list.py` 找到代码。 74 | 最后让我们看下它的时间复杂度:(这里 CircularDoubleLinkedList 取大写字母缩写为 cdll) 75 | 76 | 循环双端链表操作 | 平均时间复杂度 | 77 | ---------------------------------------|----------------| 78 | cdll.append(value) | O(1) | 79 | cdll.appendleft(value) | O(1) | 80 | cdll.remove(node),注意这里参数是 node | O(1) | 81 | cdll.headnode() | O(1) | 82 | cdll.tailnode() | O(1) | 83 | 84 | 85 | # 小问题: 86 | - 这里单链表我没有实现 insert 方法,你能自己尝试实现吗? insert(value, new_value),我想在某个值之前插入一个值。你同样需要先查找,所以这个步骤也不够高效。 87 | - 你能尝试自己实现个 lru cache 吗?需要使用到我们这里提到的循环双端链表 88 | - 借助内置的 collections.OrderedDict,它有两个方法 popitem 和 move_to_end,我们可以迅速实现一个 LRU cache。请你尝试用 OrderedDict 来实现。 89 | - python 内置库的哪些数据结构使用到了本章讲的链式结构? 90 | 91 | 92 | # 相关阅读 93 | 94 | [那些年,我们一起跪过的算法题- Lru cache[视频]](https://zhuanlan.zhihu.com/p/35175401) 95 | 96 | # 勘误: 97 | 98 | 视频中 LinkedList.remove 方法讲解有遗漏, linked_list.py 文件已经修正,请读者注意。具体请参考 [fix linked_list & add gitigonre](https://github.com/PegasusWang/python_data_structures_and_algorithms/pull/3)。视频最后增加了一段勘误说明。 99 | 100 | # Leetcode 101 | 102 | 反转链表 [reverse-linked-list](https://leetcode.com/problems/reverse-linked-list/) 103 | 104 | 这里有一道关于 LRU 的练习题你可以尝试下。 105 | [LRU Cache](https://leetcode.com/problems/lru-cache/description/) 106 | 107 | 合并两个有序链表 [merge-two-sorted-lists](/https://leetcode.com/problems/merge-two-sorted-lists/submissions/) 108 | -------------------------------------------------------------------------------- /docs/04_队列/array_queue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PegasusWang/python_data_structures_and_algorithms/3469a79c34b6c08ae52797c3974b49fbfa8cca51/docs/04_队列/array_queue.png -------------------------------------------------------------------------------- /docs/04_队列/array_queue.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | # NOTE: 从 array_and_list 第一章拷贝的代码 5 | class Array(object): 6 | 7 | def __init__(self, size=32): 8 | self._size = size 9 | self._items = [None] * size 10 | 11 | def __getitem__(self, index): 12 | return self._items[index] 13 | 14 | def __setitem__(self, index, value): 15 | self._items[index] = value 16 | 17 | def __len__(self): 18 | return self._size 19 | 20 | def clear(self, value=None): 21 | for i in range(len(self._items)): 22 | self._items[i] = value 23 | 24 | def __iter__(self): 25 | for item in self._items: 26 | yield item 27 | 28 | 29 | class FullError(Exception): 30 | pass 31 | 32 | 33 | class ArrayQueue(object): 34 | def __init__(self, maxsize): 35 | self.maxsize = maxsize 36 | self.array = Array(maxsize) 37 | self.head = 0 38 | self.tail = 0 39 | 40 | def push(self, value): 41 | if len(self) >= self.maxsize: 42 | raise FullError('queue full') 43 | self.array[self.head % self.maxsize] = value 44 | self.head += 1 45 | 46 | def pop(self): 47 | value = self.array[self.tail % self.maxsize] 48 | self.tail += 1 49 | return value 50 | 51 | def __len__(self): 52 | return self.head - self.tail 53 | 54 | 55 | def test_queue(): 56 | import pytest # pip install pytest 57 | size = 5 58 | q = ArrayQueue(size) 59 | for i in range(size): 60 | q.push(i) 61 | 62 | with pytest.raises(FullError) as excinfo: # 我们来测试是否真的抛出了异常 63 | q.push(size) 64 | assert 'full' in str(excinfo.value) 65 | 66 | assert len(q) == 5 67 | 68 | assert q.pop() == 0 69 | assert q.pop() == 1 70 | 71 | q.push(5) 72 | 73 | assert len(q) == 4 74 | 75 | assert q.pop() == 2 76 | assert q.pop() == 3 77 | assert q.pop() == 4 78 | assert q.pop() == 5 79 | 80 | assert len(q) == 0 81 | -------------------------------------------------------------------------------- /docs/04_队列/deque.py: -------------------------------------------------------------------------------- 1 | # 留给读者练习,如果实在想不出,在第 5 章栈里 stack.py 会有实现 2 | -------------------------------------------------------------------------------- /docs/04_队列/queue.md: -------------------------------------------------------------------------------- 1 | # 队列和栈 2 | 3 | 前面讲了线性和链式结构,如果你顺利掌握了,下边的队列和栈就小菜一碟了。因为我们会用前两章讲到的东西来实现队列和栈。 4 | 之所以放到一起讲是因为这两个东西很类似,队列是先进先出结构(FIFO, first in first out), 5 | 栈是后进先出结构(LIFO, last in first out)。 6 | 7 | 生活中的数据结构: 8 | 9 | - 队列。没错就是咱平常排队,第一个来的第一个走 10 | 11 | 本章我们详细讲讲常用的队列 12 | 13 | # 队列 Queue 14 | 15 | 这里卖个关子,如果你熟悉了上两节讲的内容,这里你会选取哪个数据结构作为队列的底层存储? 16 | 还记得第一章讲的如何实现 ADT 吗?我视频了说了三个注意事项: 17 | 18 | - 1.如何选用恰当的数据结构作为存储? 19 | - 2.选取的数据结构能否满足 ADT 的功能需求 20 | - 3.实现效率如何? 21 | 22 | 我们先来看看 list 可以不?对照这个三个需求,看看能否满足: 23 | 24 | - 1.我们选择了 list 25 | - 2.看起来队列需要从头删除,向尾部增加元素,也就是 list.pop(0) 和 list.append(element) 26 | - 3.嗯,貌似 list.pop(0) 会导致所有其后所有元素向前移动一个位置,O(n)复杂度。append 平均倒是O(1),但是如果内存不够还要重新分配内存。 27 | 28 | 你看,使用了 list 的话频繁 pop(0) 是非常低效的。(当然list 实现还有另一种方式就是插入用 list.insert(0, item),删除用list.pop()) 29 | 30 | 脑子再转转, 我们第二章实现了 链表 LinkedList,看看能否满足要求: 31 | 32 | - 1.这里选择 LinkedList 33 | - 2.删除头元素 LinkedList.popleft(),追加 append(element)。都可以满足 34 | - 3.哇欧,这两个操作都是 O(1) 的,完美。 35 | 36 | 好, 就用 LinkedList 了,我们开始实现,具体看视频。这次实现我们还将演示自定义异常和测试异常。 37 | 38 | 39 | # 用数组实现队列 40 | 41 | 难道用数组就不能实现队列了吗?其实还是可以的。只不过数组是预先分配固定内存的,所以如果你知道了队列的最大长度,也是 42 | 可以用数组来实现的。 43 | 44 | 想象一下,队列就俩操作,进进出出,一进一出,pop 和 push 操作。 45 | 似乎只要两个下标 head, tail 就可以了。 当我们 push 的时候赋值并且前移 head,pop 的时候前移 tail 就可以了。你可以在纸上 46 | 模拟下试试。列队的长度就是 head-pop,这个长度必须不能大于初始化的最大程度。 47 | 48 | 如果 head 先到了数组末尾咋办?重头来呗,只要我们保证 tail 不会超过 head 就行。 49 | 50 | head = 0,1,2,3,4 ... 0,1,2,3,4 ... 51 | 52 | 重头再来,循环往复,仿佛一个轮回。。。。 53 | 怎么重头来呢?看上边数组的规律你如果还想不起来用取模,估计小学数学是体育老师教的。 54 | 55 | ```py 56 | maxsize = 5 57 | for i in range(100): 58 | print(i % maxsize) 59 | ``` 60 | 61 | ![](./array_queue.png) 62 | 63 | 我们来实现一个空间有限的循环队列。ArrayQueue,它的实现很简单,但是缺点是需要预先知道队列的长度来分配内存。 64 | 65 | 66 | # 双端队列 Double ended Queue 67 | 看了视频相信你已经会实现队列了,你可能还听过双端队列。上边讲到的队列 队头出,尾尾进,我们如果想头部和尾巴都能进能出呢? 68 | 这就是双端队列了,如果你用过 collections.deque 模块,就是这个东西。他能高效在两头操作。 69 | 70 | 假如让你实现你能想起来嘛? 71 | 似乎我们需要一个能 append() appendleft() popleft() pop() 都是 O(1) 的数据结构。 72 | 73 | 上边我们实现 队列的 LinkedList 可以吗?貌似就差一个 pop() 最后边的元素无法实现了。 74 | 对,我们还有双端链表。它有这几个方法: 75 | 76 | - append 77 | - appendleft 78 | - headnode() 79 | - tailnode() 80 | - remove(node) # O(1) 81 | 82 | 啊哈,似乎删除头尾都可以啦,而且都是 O(1) 的,完美。 83 | 交给你一个艰巨的任务,实现双端队列 Deque() ADT。你可以参考前几章的任何代码,挑战一下这个任务,别忘记写单元测试呦。当然如果没想出来也没关系,后边我们实现栈的时候还会用到它,那里我们会实现这个代码。 84 | 85 | 86 | # 思考题 87 | - 你能用 python 的 deque 来实现 queue ADT 吗? 88 | - 哪些经典算法里用到了队列呢? 89 | -------------------------------------------------------------------------------- /docs/04_队列/queue.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from collections import deque 4 | 5 | # NOTE:注意这里是第三章 linked_list.py 里的内容,为了使文件自包含,我直接拷贝过来的 6 | 7 | 8 | class Node(object): 9 | def __init__(self, value=None, next=None): # 这里我们 root 节点默认都是 None,所以都给了默认值 10 | self.value = value 11 | self.next = next 12 | 13 | def __str__(self): 14 | """方便你打出来调试,复杂的代码可能需要断点调试""" 15 | return ''.format(self.value, self.next) 16 | 17 | __repr__ = __str__ 18 | 19 | 20 | class LinkedList(object): 21 | """ 链接表 ADT 22 | [root] -> [node0] -> [node1] -> [node2] 23 | """ 24 | 25 | def __init__(self, maxsize=None): 26 | """ 27 | :param maxsize: int or None, 如果是 None,无限扩充 28 | """ 29 | self.maxsize = maxsize 30 | self.root = Node() # 默认 root 节点指向 None 31 | self.tailnode = None 32 | self.length = 0 33 | 34 | def __len__(self): 35 | return self.length 36 | 37 | def append(self, value): # O(1) 38 | if self.maxsize is not None and len(self) >= self.maxsize: 39 | raise Exception('LinkedList is Full') 40 | node = Node(value) # 构造节点 41 | tailnode = self.tailnode 42 | if tailnode is None: # 还没有 append 过,length = 0, 追加到 root 后 43 | self.root.next = node 44 | else: # 否则追加到最后一个节点的后边,并更新最后一个节点是 append 的节点 45 | tailnode.next = node 46 | self.tailnode = node 47 | self.length += 1 48 | 49 | def appendleft(self, value): 50 | headnode = self.root.next 51 | node = Node(value) 52 | self.root.next = node 53 | node.next = headnode 54 | self.length += 1 55 | 56 | def __iter__(self): 57 | for node in self.iter_node(): 58 | yield node.value 59 | 60 | def iter_node(self): 61 | """遍历 从 head 节点到 tail 节点""" 62 | curnode = self.root.next 63 | while curnode is not self.tailnode: # 从第一个节点开始遍历 64 | yield curnode 65 | curnode = curnode.next # 移动到下一个节点 66 | yield curnode 67 | 68 | def remove(self, value): # O(n) 69 | """ 删除包含值的一个节点,将其前一个节点的 next 指向被查询节点的下一个即可 70 | 71 | :param value: 72 | """ 73 | prevnode = self.root # 74 | curnode = self.root.next 75 | for curnode in self.iter_node(): 76 | if curnode.value == value: 77 | prevnode.next = curnode.next 78 | del curnode 79 | self.length -= 1 80 | return 1 # 表明删除成功 81 | else: 82 | prevnode = curnode 83 | return -1 # 表明删除失败 84 | 85 | def find(self, value): # O(n) 86 | """ 查找一个节点,返回序号,从 0 开始 87 | 88 | :param value: 89 | """ 90 | index = 0 91 | for node in self.iter_node(): # 我们定义了 __iter__,这里就可以用 for 遍历它了 92 | if node.value == value: 93 | return index 94 | index += 1 95 | return -1 # 没找到 96 | 97 | def popleft(self): # O(1) 98 | """ 删除第一个链表节点 99 | """ 100 | if self.root.next is None: 101 | raise Exception('pop from empty LinkedList') 102 | headnode = self.root.next 103 | self.root.next = headnode.next 104 | self.length -= 1 105 | value = headnode.value 106 | del headnode 107 | return value 108 | 109 | def clear(self): 110 | for node in self.iter_node(): 111 | del node 112 | self.root.next = None 113 | self.length = 0 114 | 115 | ###################################################### 116 | # 下边是 Queue 实现 117 | ###################################################### 118 | 119 | 120 | class EmptyError(Exception): 121 | """自定义异常""" 122 | pass 123 | 124 | 125 | class Queue(object): 126 | def __init__(self, maxsize=None): 127 | self.maxsize = maxsize 128 | self._item_link_list = LinkedList() 129 | 130 | def __len__(self): 131 | return len(self._item_link_list) 132 | 133 | def push(self, value): # O(1) 134 | """ 队尾添加元素 """ 135 | return self._item_link_list.append(value) 136 | 137 | def pop(self): 138 | """队列头部删除元素""" 139 | if len(self) <= 0: 140 | raise EmptyError('empty queue') 141 | return self._item_link_list.popleft() 142 | 143 | 144 | def test_queue(): 145 | q = Queue() 146 | q.push(0) 147 | q.push(1) 148 | q.push(2) 149 | 150 | assert len(q) == 3 151 | 152 | assert q.pop() == 0 153 | assert q.pop() == 1 154 | assert q.pop() == 2 155 | 156 | import pytest # pip install pytest 157 | with pytest.raises(EmptyError) as excinfo: # 我们来测试是否真的抛出了异常 158 | q.pop() # 继续调用会抛出异常 159 | assert 'empty queue' == str(excinfo.value) 160 | 161 | 162 | class MyQueue: 163 | """ 164 | 使用 collections.deque 可以迅速实现一个队列 165 | """ 166 | def __init__(self): 167 | self.items = deque() 168 | 169 | def append(self, val): 170 | return self.items.append(val) 171 | 172 | def pop(self): 173 | return self.items.popleft() 174 | 175 | def __len__(self): 176 | return len(self.items) 177 | 178 | def empty(self): 179 | return len(self.items) == 0 180 | 181 | def front(self): 182 | return self.items[0] 183 | -------------------------------------------------------------------------------- /docs/05_栈/stack.md: -------------------------------------------------------------------------------- 1 | # 栈 2 | 3 | 栈这个词实际上在计算机科学里使用很多,除了数据结构外,还有内存里的栈区 (和堆对应),熟悉 C 系语言的话应该不会陌生。 4 | 上一章我们讲到了先进先出 queue,其实用 python 的内置类型 collections.deque 或者我们自己实现的 LinkedList 来实现它都很简单。 5 | 本章我们讲讲 后进先出的栈。 6 | 7 | 生活中的数据结构: 8 | 9 | - 栈。好比在桶里头放盘子,先放的盘子放在了底下,后来的盘子放在上边。你要拿的时候,也是先拿最上边的。 10 | 11 | 栈其实也很简单,因为基础操作就俩,一个 push 和一个 pop,咦,咋和队列一样的? 12 | 确实方法名字一样,但是得到的结果可是不同的。 13 | 14 | 15 | # 栈 ADT 16 | 17 | 上一章我介绍了我们怎样选取恰到的数据结构来实现新的 ADT?你能想到这里我们应该使用之前提到的哪个数据结构来实现吗? 18 | 你的大脑可能开始高(gui)速(su)旋转了,上几章学过的 array, list, deque, LinkedList, CircularDoubleLinkedList, queue 19 | 等在大脑里呼啸而过,这个时候可能已经一脸愁容了,到底该选啥? 20 | 21 | 还用问嘛,当然是时间复杂度最小的啦,大部分情况下空间都是够用的。 22 | 其实你会发现栈比队列还简单,因为它只在顶上操作(想象装着盘子的桶),如果有一种数据结构能方便在尾部增减元素不就满足需求了吗。 23 | 这个时候如果你忘记了,可以翻翻前几章,看看哪个数据结构符合要求。 24 | 25 | 想一下,似乎 CircularDoubleLinkedList 循环双端队列是满足的,因为增删最后一个元素都是 O(1)。 26 | 不过看了下示例代码,似乎没有 pop() 方法,对,因为我已经把实现 deque 作为思考题了。😂 27 | 如果之前你没写出来也没关系,这里我们会再实现它。 28 | 29 | 30 | 视频里我们将借助 CircularDoubleLinkedList 实现 双端队列 Deque ,并且用 Deque 实现 Stack。 31 | 32 | 33 | # Stack over flow 什么鬼? 34 | 嗯,stackoverflow 不是一个程序员问答网站吗?没错。 35 | 函数的临时变量是存储在栈区的,如果你不幸写了一个没有出口的递归函数,就会这个错。不信你试试: 36 | 37 | 38 | ```py 39 | def infinite_fib(n): 40 | return infinite_fib(n-1) + infinite_fib(n-2) 41 | infinite_fib(10) 42 | ``` 43 | 44 | 一大段输出之后就会出现异常: RecursionError: maximum recursion depth exceeded。 45 | 后边会讲到递归,递归是初学者比较难理解的概念,在树的遍历等地方还会看到它。 46 | 47 | 48 | # 数据结构头脑风暴法 49 | 50 | 当我们不知道使用什么数据结构来解决问题的时候,《程序员面试金典》这本书的第六章提到了一种方式叫做『数据结构头脑风暴法』。 51 | 这种笨方法就是快速过一遍数据结构的列表,然后逐一尝试各种数据结构看看哪个最适合。 52 | 53 | 在你实现一个更高级的数据结构的时候,如果脑子没有思路,不妨尝试下这个方法,迅速过一遍你所知道的数据结构,看看哪种最适合。(从每个操作的时间复杂度和空间复杂度分析寻找最优解) 54 | 55 | # 思考题 56 | - 上一章我们用数组实现了队列,其实也能用数组来实现栈,你能自己用数组来实现一个栈的 ADT 吗? 57 | - 实际上借助 python 内置的 list/collections.deque 结构就很容易实现一个栈,请你尝试实现,本章我们全部使用自己编写的数据结构而没用到 python 内置的数据结构。 58 | - 这里我们自己实现了 Deque,你能用 python 内置的 collections.deque 实现栈吗?有轮子能直接用的话看起来就简单多了,这里我们为了学习数据结构的实现就避免了直接使用内置结构 59 | - 哪些经典算法里使用到了栈呢? 60 | 61 | # Leetcode 练习 62 | 63 | https://leetcode.com/problems/implement-queue-using-stacks/ 64 | -------------------------------------------------------------------------------- /docs/05_栈/stack.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # NOTE: 这里拷贝的 double_link_list.py 里的代码 4 | 5 | from collections import deque 6 | 7 | 8 | class Node(object): 9 | 10 | def __init__(self, value=None, prev=None, next=None): 11 | self.value, self.prev, self.next = value, prev, next 12 | 13 | 14 | class CircularDoubleLinkedList(object): 15 | """循环双端链表 ADT 16 | 多了个循环其实就是把 root 的 prev 指向 tail 节点,串起来 17 | """ 18 | 19 | def __init__(self, maxsize=None): 20 | self.maxsize = maxsize 21 | node = Node() 22 | node.next, node.prev = node, node 23 | self.root = node 24 | self.length = 0 25 | 26 | def __len__(self): 27 | return self.length 28 | 29 | def headnode(self): 30 | return self.root.next 31 | 32 | def tailnode(self): 33 | return self.root.prev 34 | 35 | def append(self, value): # O(1), 你发现一般不用 for 循环的就是 O(1),有限个步骤 36 | if self.maxsize is not None and len(self) >= self.maxsize: 37 | raise Exception('LinkedList is Full') 38 | node = Node(value=value) 39 | tailnode = self.tailnode() or self.root 40 | 41 | tailnode.next = node 42 | node.prev = tailnode 43 | node.next = self.root 44 | self.root.prev = node 45 | self.length += 1 46 | 47 | def appendleft(self, value): 48 | if self.maxsize is not None and len(self) >= self.maxsize: 49 | raise Exception('LinkedList is Full') 50 | node = Node(value=value) 51 | if self.root.next is self.root: # empty 52 | node.next = self.root 53 | node.prev = self.root 54 | self.root.next = node 55 | self.root.prev = node 56 | else: 57 | node.prev = self.root 58 | headnode = self.root.next 59 | node.next = headnode 60 | headnode.prev = node 61 | self.root.next = node 62 | self.length += 1 63 | 64 | def remove(self, node): # O(1),传入node 而不是 value 我们就能实现 O(1) 删除 65 | """remove 66 | :param node # 在 lru_cache 里实际上根据key 保存了整个node: 67 | """ 68 | if node is self.root: 69 | return 70 | else: # 71 | node.prev.next = node.next 72 | node.next.prev = node.prev 73 | self.length -= 1 74 | return node 75 | 76 | def iter_node(self): 77 | if self.root.next is self.root: 78 | return 79 | curnode = self.root.next 80 | while curnode.next is not self.root: 81 | yield curnode 82 | curnode = curnode.next 83 | yield curnode 84 | 85 | def __iter__(self): 86 | for node in self.iter_node(): 87 | yield node.value 88 | 89 | def iter_node_reverse(self): 90 | """相比单链表独有的反序遍历""" 91 | if self.root.prev is self.root: 92 | return 93 | curnode = self.root.prev 94 | while curnode.prev is not self.root: 95 | yield curnode 96 | curnode = curnode.prev 97 | yield curnode 98 | 99 | 100 | ############################################################ 101 | # 分割线,下边是本章 内容实现 102 | ############################################################ 103 | 104 | 105 | class Deque(CircularDoubleLinkedList): # 注意这里我们用到了继承,嗯,貌似我说过不会用啥 OOP 特性的,抱歉 106 | 107 | def pop(self): 108 | """删除尾节点""" 109 | if len(self) == 0: 110 | raise Exception('empty') 111 | tailnode = self.tailnode() 112 | value = tailnode.value 113 | self.remove(tailnode) 114 | return value 115 | 116 | def popleft(self): 117 | if len(self) == 0: 118 | raise Exception('empty') 119 | headnode = self.headnode() 120 | value = headnode.value 121 | self.remove(headnode) 122 | return value 123 | 124 | 125 | def test_deque(): 126 | dq = Deque() 127 | dq.append(1) 128 | 129 | dq.append(2) 130 | assert list(dq) == [1, 2] 131 | 132 | dq.appendleft(0) 133 | assert list(dq) == [0, 1, 2] 134 | 135 | dq.pop() 136 | assert list(dq) == [0, 1] 137 | 138 | dq.popleft() 139 | assert list(dq) == [1] 140 | 141 | dq.pop() 142 | assert len(dq) == 0 143 | 144 | 145 | class Stack(object): 146 | def __init__(self): 147 | self.deque = Deque() # 你可以很容易替换为 python 内置的 collections.deque 148 | 149 | def push(self, value): 150 | self.deque.append(value) 151 | 152 | def pop(self): 153 | return self.deque.pop() 154 | 155 | 156 | class Stack2(object): 157 | 158 | def __init__(self): 159 | self._deque = deque() 160 | 161 | def push(self, value): 162 | return self._deque.append(value) 163 | 164 | def pop(self): 165 | return self._deque.pop() 166 | 167 | def empty(self): 168 | return len(self._deque) == 0 169 | 170 | 171 | def test_stack(): 172 | s = Stack() 173 | s.push(0) 174 | s.push(1) 175 | s.push(2) 176 | 177 | assert s.pop() == 2 178 | assert s.pop() == 1 179 | assert s.pop() == 0 180 | 181 | import pytest # pip install pytest 182 | with pytest.raises(Exception) as excinfo: # 我们来测试是否真的抛出了异常 183 | s.pop() 184 | assert 'empty' in str(excinfo.value) 185 | 186 | 187 | if __name__ == '__main__': 188 | test_stack() 189 | -------------------------------------------------------------------------------- /docs/06_算法分析/big_o.md: -------------------------------------------------------------------------------- 1 | # 算法复杂度分析 2 | 前面我们说了很多次时间复杂度是 O(1), O(n) 啥的,并没有仔细讲解这个 O 符号究竟是什么。 3 | 你可以大概理解为操作的次数和数据个数的比例关系。比如 O(1) 就是有限次数操作,O(n) 就是操作正比于你的元素个数。 4 | 这一章我们用更严谨的方式来定义它。 5 | 6 | 7 | # 大 O 表示法 8 | 我们从一个计算矩阵的例子来引入,这里我参考了 [《Data Structures and Algorithms in Python》]( 9 | https://book.douban.com/subject/10607365/) 中给的一个例子: 10 | 11 | 考虑计算一个 n * n 矩阵所有元素的和(如果你不知道矩阵,就理解为一个二维数组): 12 | 13 | $$ 14 | \begin{bmatrix} 15 | 0 & 1 & 2 \\ 16 | 3 & 4 & 5 \\ 17 | 6 & 7 & 8 \\ 18 | \end{bmatrix} 19 | $$ 20 | 21 | 这里列举两种方式: 22 | 23 | ```py 24 | # version1 25 | total_sum = 0 26 | for i in range(n): 27 | row_sum[i] = 0 28 | for j in range(n): 29 | row_sum[i] = row_sum[i] + matrix[i, j] 30 | total_sum = total_sum + matrix[i, j] 31 | 32 | # version2 33 | total_sum = 0 34 | for i in range(n): 35 | row_sum[i] = 0 36 | for j in range(n): 37 | row_sum[i] = row_sum[i] + matrix[i, j] 38 | total_sum = total_sum + row_sum[i] # 注意这里和上边的不同 39 | ``` 40 | 41 | v1 版本的关键操作在 j 循环里,两步加法操作,由于嵌套在第一个循环里,操作步骤是 $ (2n) * n = 2n^2 $。 42 | 43 | v2 版本的 total_sum 只有 n 次操作,它的操作次数是 $ n + n*n = n^2 + n $。 44 | 45 | 46 | 这里你可能还感觉不到它们有多大差别,因为计算机执行的太快了,但是当 n 增长特别快的时候,总的操作次数差距就很明显了: 47 | 48 | n | $ 2n^2 $ | $ n^2 +n $ | 49 | -------|----------------|----------------| 50 | 10 | 200 | 110 | 51 | 100 | 20,000 | 10,100 | 52 | 1000 | 2,000,000 | 1,001,000 | 53 | 10000 | 200,000,000 | 100,010,000 | 54 | 100000 | 20,000,000,000 | 10,000,100,000 | 55 | 56 | 通常我们不太关注每个算法具体执行了多少次,而更关心随着输入规模 n 的增加,算法运行时间将以什么速度增加。为此计算机科学家定义了一个符号, 57 | 用来表示在最糟糕的情况下算法的运行时间,大 O 符号,在数学上称之为渐进上界(《算法导论》)。 58 | 59 | # 如何计算时间复杂度 60 | 上边我们列举了两个版本的计算矩阵和的代码,你看到了两个公式: 61 | 62 | - v1: $ 2n*n = 2n^2 $ 63 | - v2: $ n + n*n = n + n^2 $ 64 | 65 | 当 n 非常大的时候,$ n^2 $ 的数值这里将占主导,我们可以忽略 n 的影响 66 | 67 | - v1: $ 2n*n = 2n^2 $ 68 | - v2: $ n + n*n = n + n^2 \leq 2n^2 $ 69 | 70 | 这里我们可以认为两个算法的时间复杂度均为 $ O(n^2) $ 71 | 72 | # 常用时间复杂度 73 | 这里我们列举一些常用的时间复杂度,按照增长速度排序,日常我们的业务代码中最常用的是指数之前的复杂度,指数和阶乘的增长速度非常快, 74 | 当输入比较大的时候用在业务代码里是不可接受的。 75 | 76 | O | 名称 | 举例 | 77 | ----------|--------------|--------------------| 78 | 1 | 常量时间 | 一次赋值 | 79 | $\log n$ | 对数时间 | 折半查找 | 80 | $n$ | 线性时间 | 线性查找 | 81 | n$\log n$ | 对数线性时间 | 快速排序 | 82 | $n^2$ | 平方 | 两重循环 | 83 | $n^3$ | 立方 | 三重循环 | 84 | $2^n$ | 指数 | 递归求斐波那契数列 | 85 | $n!$ | 阶乘 | 旅行商问题 | 86 | 87 | 88 | # 空间复杂度 89 | 相比时间复杂度,空间复杂度讨论比较少。因为用户老爷等不及,况且现在存储越来越白菜价了,更多时候我们为了提升响应速度宁可多 使用点空间。 90 | 空间复杂度相对好算一些,就是每个元素的空间占用乘以总的元素数,有些算法需要额外的空间存储,有些可以本地解决。 91 | 如果能本地搞定的我们成为 in place 的,原地操作,比如交换一个 数组中的某两个位置的元素。但是有些操作可能就需要申请额外的空间 92 | 来完成算法了,后边我们介绍排序算法的时候会讲到。 93 | 94 | 95 | # 常见复杂度增长趋势图 96 | 为了让你有个直观的感觉,我们来看看一些经典的时间复杂度和对应的增长趋势图,不同函数在输入规模增长的时候很快就会有巨大的增长差异 97 | 98 | ![函数增长趋势图](./function_growth.png) 99 | 100 | 101 | # 时间换空间,空间换时间 102 | 有一些时候时间和空间两者不可兼得,我们会牺牲其中之一来换取另一个。 103 | 104 | 空间换时间:比如典型的就是 python 中的集合(后面会讲到它的实现原理),虽然它比较浪费空间,但是却能用 O(1) 105 | 的时间复杂度来判重。 106 | 107 | 时间换空间:当我们空间不够用,典型的就是缓存失效算法,我们不可能缓存下无限容量的数据,就会使用一些缓存淘汰算法来保证空间可用。 108 | 109 | 110 | # 思考题 111 | - 回头看看前几章我们讲到的数据结构,以及每个操作的时间复杂度,你能理解了吗? 112 | - 二分查找是针对有序元素的一种经典的查找算法,你知道的它的时间复杂度吗?你能简单证明下吗。 113 | - 斐波那契数列你肯定很熟悉,它的公式是 F(n) = F(n-1) + F(n-2),你知道计算一个斐波那契数 F(n) 114 | 的时间复杂度吗?你会用数学公式证明吗? 115 | - 你能指出时间和空间权衡的例子吗?往往很多高效的数据结构能同时兼顾时间和空间复杂度,但是有时候我们却得做出一定的权衡 116 | 117 | 118 | # 参考资料 119 | 如果你对数学感兴趣,建议你阅读《算法导论》『函数的增长』这一节 和《Data Structures and Algorithms in Python》第4章。 120 | 121 | 122 | (本章我用了 [MathJax](https://www.zybuluo.com/codeep/note/163962) 来书写一些简单的数学公式,使用 "$"包含起来的就是数学公式) 123 | -------------------------------------------------------------------------------- /docs/06_算法分析/function_growth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PegasusWang/python_data_structures_and_algorithms/3469a79c34b6c08ae52797c3974b49fbfa8cca51/docs/06_算法分析/function_growth.png -------------------------------------------------------------------------------- /docs/07_哈希表/hashtable.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 从数组和列表章复制的代码 4 | 5 | 6 | class Array(object): 7 | 8 | def __init__(self, size=32, init=None): 9 | self._size = size 10 | self._items = [init] * size 11 | 12 | def __getitem__(self, index): 13 | return self._items[index] 14 | 15 | def __setitem__(self, index, value): 16 | self._items[index] = value 17 | 18 | def __len__(self): 19 | return self._size 20 | 21 | def clear(self, value=None): 22 | for i in range(len(self._items)): 23 | self._items[i] = value 24 | 25 | def __iter__(self): 26 | for item in self._items: 27 | yield item 28 | 29 | 30 | class Slot(object): 31 | """定义一个 hash 表数组的槽(slot 这里指的就是数组的一个位置) 32 | hash table 就是一个 数组,每个数组的元素(也叫slot槽)是一个对象,对象包含两个属性 key 和 value。 33 | 34 | 注意,一个槽有三种状态,看你能否想明白。相比链接法解决冲突,探查法删除一个 key 的操作稍微复杂。 35 | 1.从未使用 HashMap.UNUSED。此槽没有被使用和冲突过,查找时只要找到 UNUSED 就不用再继续探查了 36 | 2.使用过但是 remove 了,此时是 HashMap.EMPTY,该探查点后边的元素仍然可能是有key的,需要继续查找 37 | 3.槽正在使用 Slot 节点 38 | """ 39 | 40 | def __init__(self, key, value): 41 | self.key, self.value = key, value 42 | 43 | 44 | class HashTable(object): 45 | 46 | UNUSED = None # 没被使用过 47 | EMPTY = Slot(None, None) # 使用却被删除过 48 | 49 | def __init__(self): 50 | self._table = Array(8, init=HashTable.UNUSED) # 保持 2*i 次方 51 | self.length = 0 52 | 53 | @property 54 | def _load_factor(self): 55 | # load_factor 超过 0.8 重新分配 56 | return self.length / float(len(self._table)) 57 | 58 | def __len__(self): 59 | return self.length 60 | 61 | def _hash(self, key): 62 | return abs(hash(key)) % len(self._table) 63 | 64 | def _find_key(self, key): 65 | """ 66 | 解释一个 slot 为 UNUSED 和 EMPTY 的区别 67 | 因为使用的是二次探查的方式,假如有两个元素 A,B 冲突了,首先A hash 得到是 slot 下标5,A 放到了第5个槽,之后插入 B 因为冲突了,所以继续根据二次探查方式放到了 slot8。 68 | 然后删除 A,槽 5 被置为 EMPTY。然后我去查找 B,第一次 hash 得到的是 槽5,但是这个时候我还是需要第二次计算 hash 才能找到 B。但是如果槽是 UNUSED 我就不用继续找了,我认为 B 就是不存在的元素。这个就是 UNUSED 和 EMPTY 的区别。 69 | """ 70 | origin_index = index = self._hash(key) # origin_index 判断是否又走到了起点,如果查找一圈了都找不到则无此元素 71 | _len = len(self._table) 72 | while self._table[index] is not HashTable.UNUSED: 73 | if self._table[index] is HashTable.EMPTY: # 注意如果是 EMPTY,继续寻找下一个槽 74 | index = (index * 5 + 1) % _len 75 | if index == origin_index: 76 | break 77 | continue 78 | if self._table[index].key == key: # 找到了key 79 | return index 80 | else: 81 | index = (index * 5 + 1) % _len # 没有找到继续找下一个位置 82 | if index == origin_index: 83 | break 84 | 85 | return None 86 | 87 | def _find_slot_for_insert(self, key): 88 | index = self._hash(key) 89 | _len = len(self._table) 90 | while not self._slot_can_insert(index): # 直到找到一个可以用的槽 91 | index = (index * 5 + 1) % _len 92 | return index 93 | 94 | def _slot_can_insert(self, index): 95 | return (self._table[index] is HashTable.EMPTY or self._table[index] is HashTable.UNUSED) 96 | 97 | def __contains__(self, key): # in operator,实现之后可以使用 in 操作符判断 98 | index = self._find_key(key) 99 | return index is not None 100 | 101 | def add(self, key, value): 102 | if key in self: # update 103 | index = self._find_key(key) 104 | self._table[index].value = value 105 | return False 106 | else: 107 | index = self._find_slot_for_insert(key) 108 | self._table[index] = Slot(key, value) 109 | self.length += 1 110 | if self._load_factor >= 0.8: 111 | self._rehash() 112 | return True 113 | 114 | def _rehash(self): 115 | old_table = self._table 116 | newsize = len(self._table) * 2 117 | self._table = Array(newsize, HashTable.UNUSED) 118 | 119 | self.length = 0 120 | 121 | for slot in old_table: 122 | if slot is not HashTable.UNUSED and slot is not HashTable.EMPTY: 123 | index = self._find_slot_for_insert(slot.key) 124 | self._table[index] = slot 125 | self.length += 1 126 | 127 | def get(self, key, default=None): 128 | index = self._find_key(key) 129 | if index is None: 130 | return default 131 | else: 132 | return self._table[index].value 133 | 134 | def remove(self, key): 135 | index = self._find_key(key) 136 | if index is None: 137 | raise KeyError() 138 | value = self._table[index].value 139 | self.length -= 1 140 | self._table[index] = HashTable.EMPTY 141 | return value 142 | 143 | def __iter__(self): 144 | for slot in self._table: 145 | if slot not in (HashTable.EMPTY, HashTable.UNUSED): 146 | yield slot.key 147 | 148 | 149 | def test_hash_table(): 150 | h = HashTable() 151 | h.add('a', 0) 152 | h.add('b', 1) 153 | h.add('c', 2) 154 | assert len(h) == 3 155 | assert h.get('a') == 0 156 | assert h.get('b') == 1 157 | assert h.get('hehe') is None 158 | 159 | h.remove('a') 160 | assert h.get('a') is None 161 | assert sorted(list(h)) == ['b', 'c'] 162 | 163 | n = 50 164 | for i in range(n): 165 | h.add(i, i) 166 | 167 | for i in range(n): 168 | assert h.get(i) == i 169 | 170 | 171 | if __name__ == '__main__': 172 | print( 173 | 'beg', 174 | test_hash_table(), 175 | 'end', 176 | ) 177 | -------------------------------------------------------------------------------- /docs/07_哈希表/insert_hash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PegasusWang/python_data_structures_and_algorithms/3469a79c34b6c08ae52797c3974b49fbfa8cca51/docs/07_哈希表/insert_hash.png -------------------------------------------------------------------------------- /docs/07_哈希表/insert_hash_chaining.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PegasusWang/python_data_structures_and_algorithms/3469a79c34b6c08ae52797c3974b49fbfa8cca51/docs/07_哈希表/insert_hash_chaining.png -------------------------------------------------------------------------------- /docs/07_哈希表/quadratic_hash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PegasusWang/python_data_structures_and_algorithms/3469a79c34b6c08ae52797c3974b49fbfa8cca51/docs/07_哈希表/quadratic_hash.png -------------------------------------------------------------------------------- /docs/07_哈希表/quadratic_result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PegasusWang/python_data_structures_and_algorithms/3469a79c34b6c08ae52797c3974b49fbfa8cca51/docs/07_哈希表/quadratic_result.png -------------------------------------------------------------------------------- /docs/08_字典/dict.md: -------------------------------------------------------------------------------- 1 | # 字典 dict 2 | 3 | 上一章我们介绍了哈希表,其实 python 内置的 dict 就是用哈希表实现的,所以这一章实现 dict 就非常简单了。 4 | 当然 cpython 使用的是 c 语言实现的,远比我们写的复杂得多 (cpython/Objects/dictobject.c)。 5 | 上一章我们用 python 自己写的一个 Array 来代表定长数组,然后用它实现的 HashTable,它支持三个最基本的方法 6 | 7 | - add(key ,value): 有 key 则更新,否则插入 8 | - get(key, default=None): 或者 key 的值,不存在返回默认值 None 9 | - remove(key): 删除一个 key,这里其实不是真删除,而是标记为 Empty 10 | 11 | 字典最常使用的场景就是 k,v 存储,经常用作缓存,它的 key 值是唯一的。 12 | 内置库 collections.OrderedDict 还保持了 key 的添加顺序,其实用我们之前实现的链表也能自己实现一个 OrderedDict。 13 | 14 | # 实现 dict ADT 15 | 16 | 其实上边 HashTable 实现的三个基本方法就是我们使用字典最常用的三个基本方法, 这里我们继承一下这个类, 17 | 然后实现更多 dict 支持的方法,items(), keys(), values()。不过需要注意的是,在 python2 和 python3 里这些方法 18 | 的返回是不同的,python3 里一大改进就是不再返回浪费内存的 列表,而是返回迭代器,你要获得列表必须用 list() 转换成列表。 这里我们实现 python3 的方式返回迭代器。 19 | 20 | 21 | ```py 22 | class DictADT(HashTable): 23 | pass 24 | ``` 25 | 26 | 视频里我们将演示如何实现这些方法,并且写单测验证正确性。 27 | 28 | # Hashable 29 | 作为 dict 的 key 必须是可哈希的,也就是说不能是 list 等可变对象。不信你在 ipython 里运行如下代码: 30 | 31 | ```py 32 | d = dict() 33 | d[[1]] = 1 34 | # TypeError: unhashable type: 'list' 35 | ``` 36 | 37 | 我引用 python 文档里的说法,大家可以自己理解下: 38 | 39 | ``` 40 | An object is hashable if it has a hash value which never changes during its lifetime (it needs a __hash__() method), and can be compared to other objects (it needs an __eq__() or __cmp__() method). Hashable objects which compare equal must have the same hash value. 41 | 42 | Hashability makes an object usable as a dictionary key and a set member, because these data structures use the hash value internally. 43 | 44 | All of Python’s immutable built-in objects are hashable, while no mutable containers (such as lists or dictionaries) are. Objects which are instances of user-defined classes are hashable by default; they all compare unequal (except with themselves), and their hash value is derived from their id(). 45 | ``` 46 | 47 | 48 | # 思考题: 49 | - 你能在哈希表的基础上实现 dict 的其他操作吗? 50 | - 对于 python 来说,哪些内置数据类型是可哈希的呢?list, dict, tuple, set 等类型哪些可以作为字典的 key 呢? 51 | - 你了解可变对象和不可变对象的区别吗? 52 | - 你了解 python 的 hash 函数吗?你了解 python 的`__hash__` 和 `__eq__` 魔术方法吗?它们何时被调用 53 | 54 | # 延伸阅读 55 | 阅读 python 文档关于 dict 的相关内容 56 | -------------------------------------------------------------------------------- /docs/08_字典/dict_adt.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 从数组和列表章复制的代码 4 | 5 | 6 | class Array(object): 7 | 8 | def __init__(self, size=32, init=None): 9 | self._size = size 10 | self._items = [init] * size 11 | 12 | def __getitem__(self, index): 13 | return self._items[index] 14 | 15 | def __setitem__(self, index, value): 16 | self._items[index] = value 17 | 18 | def __len__(self): 19 | return self._size 20 | 21 | def clear(self, value=None): 22 | for i in range(len(self._items)): 23 | self._items[i] = value 24 | 25 | def __iter__(self): 26 | for item in self._items: 27 | yield item 28 | 29 | 30 | class Slot(object): 31 | """定义一个 hash 表 数组的槽 32 | 注意,一个槽有三种状态,看你能否想明白。相比链接法解决冲突,二次探查法删除一个 key 的操作稍微复杂。 33 | 1.从未使用 HashMap.UNUSED。此槽没有被使用和冲突过,查找时只要找到 UNUSED 就不用再继续探查了 34 | 2.使用过但是 remove 了,此时是 HashMap.EMPTY,该探查点后边的元素扔可能是有key 35 | 3.槽正在使用 Slot 节点 36 | """ 37 | 38 | def __init__(self, key, value): 39 | self.key, self.value = key, value 40 | 41 | 42 | class HashTable(object): 43 | 44 | UNUSED = None # 没被使用过 45 | EMPTY = Slot(None, None) # 使用却被删除过 46 | 47 | def __init__(self): 48 | self._table = Array(8, init=HashTable.UNUSED) # 保持 2*i 次方 49 | self.length = 0 50 | 51 | @property 52 | def _load_factor(self): 53 | # load_factor 超过 0.8 重新分配 54 | return self.length / float(len(self._table)) 55 | 56 | def __len__(self): 57 | return self.length 58 | 59 | def _hash(self, key): 60 | return abs(hash(key)) % len(self._table) 61 | 62 | def _find_key(self, key): 63 | index = self._hash(key) 64 | _len = len(self._table) 65 | while self._table[index] is not HashTable.UNUSED: 66 | if self._table[index] is HashTable.EMPTY: 67 | index = (index*5 + 1) % _len 68 | continue 69 | elif self._table[index].key == key: 70 | return index 71 | else: 72 | index = (index*5 + 1) % _len 73 | return None 74 | 75 | def _find_slot_for_insert(self, key): 76 | index = self._hash(key) 77 | _len = len(self._table) 78 | while not self._slot_can_insert(index): 79 | index = (index*5 + 1) % _len 80 | return index 81 | 82 | def _slot_can_insert(self, index): 83 | return (self._table[index] is HashTable.EMPTY or self._table[index] is HashTable.UNUSED) 84 | 85 | def __contains__(self, key): # in operator 86 | index = self._find_key(key) 87 | return index is not None 88 | 89 | def add(self, key, value): 90 | if key in self: 91 | index = self._find_key(key) 92 | self._table[index].value = value 93 | return False 94 | else: 95 | index = self._find_slot_for_insert(key) 96 | self._table[index] = Slot(key, value) 97 | self.length += 1 98 | if self._load_factor >= 0.8: 99 | self._rehash() 100 | return True 101 | 102 | def _rehash(self): 103 | old_table = self._table 104 | newsize = len(self._table) * 2 105 | self._table = Array(newsize, HashTable.UNUSED) 106 | 107 | self.length = 0 108 | 109 | for slot in old_table: 110 | if slot is not HashTable.UNUSED and slot is not HashTable.EMPTY: 111 | index = self._find_slot_for_insert(slot.key) 112 | self._table[index] = slot 113 | self.length += 1 114 | 115 | def get(self, key, default=None): 116 | index = self._find_key(key) 117 | if index is None: 118 | return default 119 | else: 120 | return self._table[index].value 121 | 122 | def remove(self, key): 123 | index = self._find_key(key) 124 | if index is None: 125 | raise KeyError() 126 | value = self._table[index].value 127 | self.length -= 1 128 | self._table[index] = HashTable.EMPTY 129 | return value 130 | 131 | def __iter__(self): 132 | for slot in self._table: 133 | if slot not in (HashTable.EMPTY, HashTable.UNUSED): 134 | yield slot.key 135 | 136 | ######################################### 137 | # 上边是从 哈希表章 拷贝过来的代码,我们会直接继承 HashTable 实现 dict 138 | ######################################### 139 | 140 | 141 | class DictADT(HashTable): 142 | 143 | def _iter_slot(self): 144 | for slot in self._table: 145 | if slot not in (HashTable.EMPTY, HashTable.UNUSED): 146 | yield slot 147 | 148 | def __setitem__(self, key, value): 149 | self.add(key, value) 150 | 151 | def __getitem__(self, key): 152 | if key not in self: 153 | raise KeyError() 154 | else: 155 | return self.get(key) 156 | 157 | def items(self): 158 | for slot in self._iter_slot(): 159 | yield (slot.key, slot.value) 160 | 161 | def keys(self): 162 | for slot in self._iter_slot(): 163 | yield slot.key 164 | 165 | def values(self): 166 | for slot in self._iter_slot(): 167 | yield slot.value 168 | 169 | 170 | def test_dict_adt(): 171 | import random 172 | d = DictADT() 173 | 174 | d['a'] = 1 175 | assert d['a'] == 1 176 | d.remove('a') 177 | 178 | l = list(range(30)) 179 | random.shuffle(l) 180 | for i in l: 181 | d.add(i, i) 182 | 183 | for i in range(30): 184 | assert d.get(i) == i 185 | 186 | assert sorted(list(d.keys())) == sorted(l) 187 | 188 | 189 | test_dict_adt() 190 | -------------------------------------------------------------------------------- /docs/09_集合/set.md: -------------------------------------------------------------------------------- 1 | # 集合 set 2 | 3 | 集合是一种不包含重复元素的数据结构,经常用来判断是否重复这种操作,或者集合中是否存在一个元素。 4 | 这一章讲集合,实际上它的底层也是哈希表实现的,所以像实现 DictADT 一样,借助 HashTable 实现它也比较简单。 5 | 6 | 7 | # 集合操作 8 | 集合可能最常用的就是去重,判断是否存在一个元素等,但是 set 相比 dict 有更丰富的操作,主要是数学概念上的。 9 | 如果你学过《离散数学》中集合相关的概念,基本上是一致的。 python 的 set 提供了如下基本的集合操作, 10 | 假设有两个集合 A,B,有以下操作: 11 | 12 | - 交集: A & B,表示同时在 A 和 B 中的元素。 python 中重载 `__and__` 实现 13 | - 并集: A | B,表示在 A 或者 B 中的元素,两个集合相加。python 中重载 `__or__` 实现 14 | - 差集: A - B,表示在 A 中但是不在 B 中的元素。 python 中重载 `__sub__` 实现 15 | - 对称差: A ^ B,返回在 A 或 B 但是不在 A、B 中都出现的元素。其实就是 (A|B) - (A&B), python 中重载 `__xor__` 实现 16 | 17 | 这里使用的 &, |, -, ^ 在 python 内置的 set 实现中都是重载了内置的运算符。这里我们也用这种方式实现, 18 | 具体实现我会在视频里演示。python 同样实现了 intersection, union, difference, symmetric_difference 这四个方法, 19 | 和使用运算符的功能是一样的。 20 | 21 | ![](./set.png) 22 | 23 | # python frozenset 24 | 在 python 里还有一个 frozenset,看它的名字就知道这种也是集合,但是它的内容是无法变动的。一般我们使用 25 | 它的常见就是用一个可迭代对象初始化它,然后只用来判重等操作。 26 | 27 | 28 | # 实现一个 set ADT 29 | 如何实现一个集合的 ADT 呢,其实还是个哈希表,哈希表不是有 key 和 value 嘛,咱把 value 置为 1 不就行了。 30 | 31 | ```py 32 | class SetADT(HashTable): 33 | 34 | def add(self, key): 35 | # 集合其实就是一个 dict,只不过我们把它的 value 设置成 1 36 | return super(SetADT, self).add(key, True) 37 | ``` 38 | 39 | 当然其它数学上的操作就麻烦点了,不过也很容易实现。 40 | 41 | 42 | # 思考题 43 | - 集合判断一个元素是否存在的时间复杂度是多少? 44 | - 集合的元素 key 需要满足什么概念?可变对象可以吗? 45 | - 请你在 SetADT 基础上实现集合的 remove 操作和 pop 操作 46 | - 你能尝试实现对称差操作吗?这里我没有实现,留给你作为练习 47 | - 你知道如何重载 python 的内置运算符吗?这里我们实现 set 的集合操作就是用到了重载,请阅读相关 python 文档。 48 | - 当元素个数不多的时候,我们可以用 set 来判重,但是如果是大量元素会非常耗费内存。请你了解下 Bloom Filter 49 | 50 | 51 | # 延伸阅读 52 | 阅读 python 文档关于 set 的相关章节,了解 set 还有哪些操作?比如比较运算符的概念,比较两个集合意味着什么。 53 | -------------------------------------------------------------------------------- /docs/09_集合/set.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PegasusWang/python_data_structures_and_algorithms/3469a79c34b6c08ae52797c3974b49fbfa8cca51/docs/09_集合/set.png -------------------------------------------------------------------------------- /docs/09_集合/set_adt.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 从数组和列表章复制的代码 4 | 5 | 6 | class Array(object): 7 | 8 | def __init__(self, size=32, init=None): 9 | self._size = size 10 | self._items = [init] * size 11 | 12 | def __getitem__(self, index): 13 | return self._items[index] 14 | 15 | def __setitem__(self, index, value): 16 | self._items[index] = value 17 | 18 | def __len__(self): 19 | return self._size 20 | 21 | def clear(self, value=None): 22 | for i in range(len(self._items)): 23 | self._items[i] = value 24 | 25 | def __iter__(self): 26 | for item in self._items: 27 | yield item 28 | 29 | 30 | class Slot(object): 31 | """定义一个 hash 表 数组的槽 32 | 注意,一个槽有三种状态,看你能否想明白。相比链接法解决冲突,二次探查法删除一个 key 的操作稍微复杂。 33 | 1.从未使用 HashMap.UNUSED。此槽没有被使用和冲突过,查找时只要找到 UNUSED 就不用再继续探查了 34 | 2.使用过但是 remove 了,此时是 HashMap.EMPTY,该探查点后边的元素扔可能是有key 35 | 3.槽正在使用 Slot 节点 36 | """ 37 | 38 | def __init__(self, key, value): 39 | self.key, self.value = key, value 40 | 41 | 42 | class HashTable(object): 43 | 44 | UNUSED = None # 没被使用过 45 | EMPTY = Slot(None, None) # 使用却被删除过 46 | 47 | def __init__(self): 48 | self._table = Array(8, init=HashTable.UNUSED) # 保持 2*i 次方 49 | self.length = 0 50 | 51 | @property 52 | def _load_factor(self): 53 | # load_factor 超过 0.8 重新分配 54 | return self.length / float(len(self._table)) 55 | 56 | def __len__(self): 57 | return self.length 58 | 59 | def _hash(self, key): 60 | return abs(hash(key)) % len(self._table) 61 | 62 | def _find_key(self, key): 63 | index = self._hash(key) 64 | _len = len(self._table) 65 | while self._table[index] is not HashTable.UNUSED: 66 | if self._table[index] is HashTable.EMPTY: 67 | index = (index*5 + 1) % _len 68 | continue 69 | elif self._table[index].key == key: 70 | return index 71 | else: 72 | index = (index*5 + 1) % _len 73 | return None 74 | 75 | def _find_slot_for_insert(self, key): 76 | index = self._hash(key) 77 | _len = len(self._table) 78 | while not self._slot_can_insert(index): 79 | index = (index*5 + 1) % _len 80 | return index 81 | 82 | def _slot_can_insert(self, index): 83 | return (self._table[index] is HashTable.EMPTY or self._table[index] is HashTable.UNUSED) 84 | 85 | def __contains__(self, key): # in operator 86 | index = self._find_key(key) 87 | return index is not None 88 | 89 | def add(self, key, value): 90 | if key in self: 91 | index = self._find_key(key) 92 | self._table[index].value = value 93 | return False 94 | else: 95 | index = self._find_slot_for_insert(key) 96 | self._table[index] = Slot(key, value) 97 | self.length += 1 98 | if self._load_factor >= 0.8: 99 | self._rehash() 100 | return True 101 | 102 | def _rehash(self): 103 | old_table = self._table 104 | newsize = len(self._table) * 2 105 | self._table = Array(newsize, HashTable.UNUSED) 106 | 107 | self.length = 0 108 | 109 | for slot in old_table: 110 | if slot is not HashTable.UNUSED and slot is not HashTable.EMPTY: 111 | index = self._find_slot_for_insert(slot.key) 112 | self._table[index] = slot 113 | self.length += 1 114 | 115 | def get(self, key, default=None): 116 | index = self._find_key(key) 117 | if index is None: 118 | return default 119 | else: 120 | return self._table[index].value 121 | 122 | def remove(self, key): 123 | index = self._find_key(key) 124 | if index is None: 125 | raise KeyError() 126 | value = self._table[index].value 127 | self.length -= 1 128 | self._table[index] = HashTable.EMPTY 129 | return value 130 | 131 | def __iter__(self): 132 | for slot in self._table: 133 | if slot not in (HashTable.EMPTY, HashTable.UNUSED): 134 | yield slot.key 135 | 136 | ######################################### 137 | # 上边是从 哈希表章 拷贝过来的代码,我们会直接继承 HashTable 实现 集合 set 138 | ######################################### 139 | 140 | 141 | class SetADT(HashTable): 142 | 143 | def add(self, key): 144 | # 集合其实就是一个 dict,只不过我们把它的 value 设置成 1 145 | return super(SetADT, self).add(key, True) 146 | 147 | def __and__(self, other_set): 148 | """交集 A&B""" 149 | new_set = SetADT() 150 | for element_a in self: 151 | if element_a in other_set: 152 | new_set.add(element_a) 153 | return new_set 154 | 155 | def __sub__(self, other_set): 156 | """差集 A-B""" 157 | new_set = SetADT() 158 | for element_a in self: 159 | if element_a not in other_set: 160 | new_set.add(element_a) 161 | return new_set 162 | 163 | def __or__(self, other_set): 164 | """并集 A|B""" 165 | new_set = SetADT() 166 | for element_a in self: 167 | new_set.add(element_a) 168 | for element_b in other_set: 169 | new_set.add(element_b) 170 | return new_set 171 | 172 | 173 | def test_set_adt(): 174 | sa = SetADT() 175 | sa.add(1) 176 | sa.add(2) 177 | sa.add(3) 178 | assert 1 in sa # 测试 __contains__ 方法,实现了 add 和 __contains__,集合最基本的功能就实现啦 179 | 180 | sb = SetADT() 181 | sb.add(3) 182 | sb.add(4) 183 | sb.add(5) 184 | 185 | assert sorted(list(sa & sb)) == [3] 186 | assert sorted(list(sa - sb)) == [1, 2] 187 | assert sorted(list(sa | sb)) == [1, 2, 3, 4, 5] 188 | 189 | 190 | if __name__ == '__main__': 191 | test_set_adt() 192 | -------------------------------------------------------------------------------- /docs/10_递归/fact.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PegasusWang/python_data_structures_and_algorithms/3469a79c34b6c08ae52797c3974b49fbfa8cca51/docs/10_递归/fact.png -------------------------------------------------------------------------------- /docs/10_递归/hanoi.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PegasusWang/python_data_structures_and_algorithms/3469a79c34b6c08ae52797c3974b49fbfa8cca51/docs/10_递归/hanoi.gif -------------------------------------------------------------------------------- /docs/10_递归/hanoi_four_disks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PegasusWang/python_data_structures_and_algorithms/3469a79c34b6c08ae52797c3974b49fbfa8cca51/docs/10_递归/hanoi_four_disks.png -------------------------------------------------------------------------------- /docs/10_递归/hanoi_tower.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PegasusWang/python_data_structures_and_algorithms/3469a79c34b6c08ae52797c3974b49fbfa8cca51/docs/10_递归/hanoi_tower.png -------------------------------------------------------------------------------- /docs/10_递归/print_rec.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PegasusWang/python_data_structures_and_algorithms/3469a79c34b6c08ae52797c3974b49fbfa8cca51/docs/10_递归/print_rec.png -------------------------------------------------------------------------------- /docs/10_递归/recursion.md: -------------------------------------------------------------------------------- 1 | # 递归 2 | 3 | > Recursion is a process for solving problems by subdividing a larger 4 | > problem into smaller cases of the problem itself and then solving 5 | > the smaller, more trivial parts. 6 | 7 | 递归是计算机科学里出现非常多的一个概念,有时候用递归解决问题看起来非常简单优雅。 8 | 之前讲过的数据结构中我们并没有使用递归,因为递归涉及到调用栈,可能会让初学者搞晕。这一章我们开始介绍递归, 9 | 后边讲到树和一些排序算法的时候我们还会碰到它。我非常推荐你先看看《算法图解》第三章 递归, 10 | 举的例子比较浅显易懂。 11 | 12 | 13 | # 什么是递归? 14 | 递归用一种通俗的话来说就是自己调用自己,但是需要分解它的参数,让它解决一个更小一点的问题,当问题小到一定规模的时候,需要一个递归出口返回。 15 | 这里举一个和其他很多老套的教科书一样喜欢举的例子,阶乘函数,我觉得用来它演示再直观不过。它的定义是这样的: 16 | 17 | ![](./fact.png) 18 | 19 | 我们很容易根据它的定义写出这样一个递归函数,因为它本身就是递归定义的。 20 | 21 | ```py 22 | def fact(n): 23 | if n == 0: 24 | return 1 25 | else: 26 | return n * fact(n-1) 27 | ``` 28 | 看吧,几乎完全是按照定义来写的。我们来看下递归函数的几个特点: 29 | 30 | - 递归必须包含一个基本的出口(base case),否则就会无限递归,最终导致栈溢出。比如这里就是 n == 0 返回 1 31 | - 递归必须包含一个可以分解的问题(recursive case)。 要想求得 fact(n),就需要用 n * fact(n-1) 32 | - 递归必须必须要向着递归出口靠近(toward the base case)。 这里每次递归调用都会 n-1,向着递归出口 n == 0 靠近 33 | 34 | 35 | # 调用栈 36 | 看了上一个例子你可能觉得递归好简单,先别着急,我们再举个简单的例子,上边我们并没有讲递归如何工作的。 37 | 假如让你输出从 1 到 10 这十个数字,如果你是个正常人的话,我想你的第一反应都是这么写: 38 | 39 | ```py 40 | def print_num(n): 41 | for i in range(1, n + 1): # 注意很多编程语言使用的都是 从 0 开始的左闭右开区间, python 也不例外 42 | print(i) 43 | 44 | 45 | if __name__ == '__main__': 46 | print_num(10) 47 | ``` 48 | 49 | 我们尝试写一个递归版本,不就是自己调用自己嘛: 50 | 51 | ```py 52 | def print_num_recursive(n): 53 | if n > 0: 54 | print_num_recursive(n-1) 55 | print(n) 56 | ``` 57 | 58 | 你猜下它的输出?然后我们调换下 print 顺序,你再猜下它的输出 59 | 60 | ```py 61 | def print_num_recursive_revserve(n): 62 | if n > 0: 63 | print(n) 64 | print_num_recursive_revserve(n-1) 65 | ``` 66 | 你能明白是为什么吗?我建议你运行下这几个小例子,它们很简单但是却能说明问题。 67 | 计算机内部使用调用栈来实现递归,这里的栈一方面指的是内存中的栈区,一方面栈又是之前讲到的后进先出这种数据结构。 68 | 每当进入递归函数的时候,系统都会为当前函数开辟内存保存当前变量值等信息,每个调用栈之间的数据互不影响,新调用的函数 69 | 入栈的时候会放在栈顶。视频里我们会画图来演示这个过程。 70 | 71 | 递归只用大脑不用纸笔模拟的话很容易晕,因为明明是同一个变量名字,但是在不同的调用栈里它是不同的值,所以我建议 72 | 你最好手动画画这个过程。 73 | 74 | ![](./print_rec.png) 75 | 76 | # 用栈模拟递归 77 | 刚才说到了调用栈,我们就用栈来模拟一把。之前栈这一章我们讲了如何自己实现栈,不过这里为了不拷贝太多代码,我们直接用 collections.deque 就可以 78 | 快速实现一个简单的栈。 79 | 80 | ```py 81 | from collections import deque 82 | 83 | 84 | class Stack(object): 85 | def __init__(self): 86 | self._deque = deque() 87 | 88 | def push(self, value): 89 | return self._deque.append(value) 90 | 91 | def pop(self): 92 | return self._deque.pop() 93 | 94 | def is_empty(self): 95 | return len(self._deque) == 0 96 | 97 | 98 | def print_num_use_stack(n): 99 | s = Stack() 100 | while n > 0: # 不断将参数入栈 101 | s.push(n) 102 | n -= 1 103 | 104 | while not s.is_empty(): # 参数弹出 105 | print(s.pop()) 106 | ``` 107 | 这里结果也是输出 1 到 10,只不过我们是手动模拟了入栈和出栈的过程,帮助你理解计算机是如何实现递归的,是不是挺简单!现在你能明白为什么上边 print_num_recursive print_num_recursive_revserve 两个函数输出的区别了吗? 108 | 109 | 110 | # 尾递归 111 | 上边的代码示例(麻雀虽小五脏俱全)中实际上包含了两种形式的递归,一种是普通的递归,还有一种叫做尾递归: 112 | 113 | ```py 114 | def print_num_recursive(n): 115 | if n > 0: 116 | print_num_recursive(n-1) 117 | print(n) 118 | 119 | 120 | def print_num_recursive_revserve(n): 121 | if n > 0: 122 | print(n) 123 | print_num_recursive_revserve(n-1) # 尾递归 124 | ``` 125 | 126 | 概念上它很简单,就是递归调用放在了函数的最后。有什么用呢? 127 | 普通的递归, 每一级递归都产生了新的局部变量, 必须创建新的调用栈, 随着递归深度的增加, 创建的栈越来越多, 造成爆栈。虽然尾递归调用也会创建新的栈, 128 | 但是我们可以优化使得尾递归的每一级调用共用一个栈!, 如此便可解决爆栈和递归深度限制的问题! 129 | 不幸的是 python 默认不支持尾递归优化(见延伸阅读),不过一般尾递归我们可以用一个迭代来优化它。 130 | 131 | 132 | # 汉诺塔问题 133 | 134 | 有三根杆子A,B,C。A杆上有N个(N>1)穿孔圆盘,盘的尺寸由下到上依次变小。要求按下列规则将所有圆盘移至C杆: 135 | 但是有两个条件: 136 | 137 | - 每次只能移动一个圆盘; 138 | - 大盘不能叠在小盘上面。 139 | 140 | > 最早发明这个问题的人是法国数学家爱德华·卢卡斯。 141 | > 传说越南河内某间寺院有三根银棒,上串64个金盘。寺院里的僧侣依照一个古老的预言,以上述规则移动这些盘子;预言说当这些盘子移动完毕,世界就会灭亡。 142 | > 这个传说叫做梵天寺之塔问题(Tower of Brahma puzzle)。但不知道是卢卡斯自创的这个传说,还是他受他人启发。 143 | 144 | 145 | 146 | ![五个盘子的汉诺塔问题](./hanoi_tower.png) 147 | 148 | 理解这个问题需要我们一些思维上的转换,因为我们正常的思维可能都是从上边最小的盘子开始移动,但是这里我们从移动最底下的盘子开始思考。 149 | 假设我们已经知道了如何移动上边的四个盘子到 B(pole2),现在把最大的盘子从 A -> C 就很简单了。当把最大的盘子移动到 150 | C 之后,只需要把 B 上的 4 个盘子从 B -> C 就行。(这里的 pole1, 2, 3 分别就是 A, B, C 杆) 151 | 152 | ![](./hanoi_four_disks.png) 153 | 154 | 问题是仍要想办法如何移动上边的 4 个盘子,我们可以同样的方式来移动上边的 4 个盘子,这就是一种递归的解法。 155 | 给定 n 个盘子和三个杆分别是 源杆(Source), 目标杆(Destination),和中介杆(Intermediate),我们可以定义如下递归操作: 156 | 157 | - 把上边的 n-1 个盘子从 S 移动到 I,借助 D 杆 158 | - 把最底下的盘子从 S 移动到 D 159 | - 把 n-1 个盘子从 I 移动到 D,借助 S 160 | 161 | 我们把它转换成代码: 162 | 163 | ```py 164 | def hanoi_move(n, source, dest, intermediate): 165 | if n >= 1: # 递归出口,只剩一个盘子 166 | hanoi_move(n-1, source, intermediate, dest) 167 | print("Move %s -> %s" % (source, dest)) 168 | hanoi_move(n-1, intermediate, dest, source) 169 | hanoi_move(3, 'A', 'C', 'B') 170 | 171 | # 输出,建议你手动模拟下。三个盘子 A(Source), B(intermediate), C(Destination) 172 | """ 173 | Move A -> C 174 | Move A -> B 175 | Move C -> B 176 | Move A -> C 177 | Move B -> A 178 | Move B -> C 179 | Move A -> C 180 | """ 181 | ``` 182 | 183 |
184 | ![三个盘子的汉诺塔解法](./hanoi.gif) 185 |
186 | 187 | 是不是很神奇,但是老实说这个过程仅凭大脑空想是比较难以想象出来的。人的大脑『栈』深度很有限,因为你甚至都没法同时记住超过 8 个以上的 188 | 无意义数字,所以用大脑模拟不如用纸笔来模拟下。(不排除有些聪明的同学能迅速在脑瓜里完成这个过程) 189 | 190 | # 延伸阅读 191 | 递归是个非常重要的概念,我们后边的数据结构和算法中还会多次碰到它,我建议你多阅读一些资料加深理解: 192 | 193 | - 《算法图解》第三章 递归 194 | - 《Data Structures and Algorithms in Python》 第 10 章 Recursion 195 | - [《Python开启尾递归优化!》](https://segmentfault.com/a/1190000007641519) 196 | - [尾调用优化](http://www.ruanyifeng.com/blog/2015/04/tail-call.html) 197 | - [汉诺塔](https://zh.wikipedia.org/wiki/%E6%B1%89%E8%AF%BA%E5%A1%94) 198 | 199 | # 思考题 200 | - 你能举出其他一些使用到递归的例子吗? 201 | - 实现一个 flatten 函数,把嵌套的列表扁平化,你需要用递归函数来实现。比如 [[1,2], [1,2,3] -> [1,2,1,2,3] 202 | - 使用递归和循环各有什么优缺点,你能想到吗?怎么把一个尾递归用迭代替换? 203 | - 递归有时候虽然很优雅直观,但是时间复杂度却不理想,比如斐波那契数列,它的表达式是 F(n) = F(n-1) + F(n-2),你能计算它的时间复杂度吗?请你画个树来表示它的计算过程,为什么这个时间复杂度很不理想?我们怎样去优化它。 204 | - python 内置的 dict 只能用 dict['key'] 的形式访问比较麻烦,我们想用 dict.key 的形式访问。tornado web 框架中提供了一个 ObjectDict,请你实现一个递归函数接收一个字典,并返回一个可以嵌套访问的 ObjectDict 205 | -------------------------------------------------------------------------------- /docs/10_递归/recursion.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | def fact(n): 5 | if n == 0: 6 | return 1 7 | else: 8 | return n * fact(n - 1) 9 | 10 | 11 | def print_num(n): 12 | for i in range(1, n + 1): # 注意很多编程语言使用的都是 从 0 开始的左闭右开区间, python 也不例外 13 | print(i) 14 | 15 | 16 | def print_num_recursive(n): 17 | if n > 0: 18 | print_num_recursive(n - 1) 19 | print(n) 20 | 21 | 22 | def print_num_recursive_revserve(n): 23 | if n > 0: 24 | print(n) 25 | print_num_recursive_revserve(n - 1) 26 | 27 | 28 | from collections import deque 29 | 30 | 31 | class Stack(object): 32 | def __init__(self): 33 | self._deque = deque() 34 | 35 | def push(self, value): 36 | return self._deque.append(value) 37 | 38 | def pop(self): 39 | return self._deque.pop() 40 | 41 | def is_empty(self): 42 | return len(self._deque) == 0 43 | 44 | 45 | def print_num_use_stack(n): 46 | s = Stack() 47 | while n > 0: # 不断将参数入栈 48 | s.push(n) 49 | n -= 1 50 | 51 | while not s.is_empty(): # 参数弹出 52 | print(s.pop()) 53 | 54 | 55 | def hanoi_move(n, source, dest, intermediate): 56 | if n >= 1: # 递归出口,只剩一个盘子 57 | hanoi_move(n - 1, source, intermediate, dest) 58 | print("Move %s -> %s" % (source, dest)) 59 | hanoi_move(n - 1, intermediate, dest, source) 60 | 61 | 62 | def flatten(rec_list): 63 | for i in rec_list: 64 | if isinstance(i, list): 65 | for i in flatten(i): 66 | yield i 67 | else: 68 | yield i 69 | 70 | 71 | def test_flatten(): 72 | assert list(flatten([[[1], 2, 3], [1, 2, 3]])) == [1, 2, 3, 1, 2, 3] 73 | -------------------------------------------------------------------------------- /docs/11_线性查找与二分查找/search.md: -------------------------------------------------------------------------------- 1 | # 查找 2 | 3 | 查找可以说是我们业务代码里用得最多的操作,比如我们经常需要在一个列表里找到我们需要的一个元素,然后返回它的位置。 4 | 其实之前我们介绍的哈希表就是非常高效率的查找数据结构,很明显地它是用空间换时间。这一节介绍两个基本的基于线性结构的查找。 5 | 6 | # 线性查找 7 | 线性查找就是从头找到尾,直到符合条件了就返回。比如在一个 list 中找到一个等于 5 的元素并返回下标: 8 | 9 | ```py 10 | number_list = [0, 1, 2, 3, 4, 5, 6, 7] 11 | 12 | 13 | def linear_search(value, iterable): 14 | for index, val in enumerate(iterable): 15 | if val == value: 16 | return index 17 | return -1 18 | 19 | 20 | assert linear_search(5, number_list) == 5 21 | 22 | ``` 23 | 是不是 so easy。当然我们需要来一点花样,比如传一个谓词进去,你要知道,在 python 里一切皆对象,所以我们可以把函数当成一个参数传给另一个函数。 24 | 25 | ```py 26 | def linear_search_v2(predicate, iterable): 27 | for index, val in enumerate(iterable): 28 | if predicate(val): 29 | return index 30 | return -1 31 | 32 | 33 | assert linear_search_v2(lambda x: x == 5, number_list) == 5 34 | ``` 35 | 36 | 效果是一样的,但是传入一个谓词函数进去更灵活一些,比如我们可以找到第一个大于或者小于 5 的,从而控制函数的行为。 37 | 还能玩出什么花样呢?前面我们刚学习了递归,能不能发挥自虐精神没事找事用递归来实现呢? 38 | 39 | ```py 40 | def linear_search_recusive(array, value): 41 | if len(array) == 0: 42 | return -1 43 | index = len(array)-1 44 | if array[index] == value: 45 | return index 46 | return linear_search_recusive(array[0:index], value) 47 | 48 | 49 | assert linear_search_recusive(number_list, 5) == 5 50 | assert linear_search_recusive(number_list, 8) == -1 51 | assert linear_search_recusive(number_list, 7) == 7 52 | assert linear_search_recusive(number_list, 0) == 0 53 | ``` 54 | 这里的 assert 我多写了几个,包括正常情况、异常情况和边界值等,因为递归比较容易出错。注意这里的两个递归出口。 55 | 当然业务代码里如果碰到这种问题我们肯定是选上边最直白的方式来实现,要不你的同事肯定想打你。 56 | 57 | # 二分查找 58 | 上一小节说的线性查找针对的是无序序列,假如一个序列已经有序了呢,我们还需要从头找到尾吗?当然不用,折半(二分)是一种经典思想。日常生活中还有哪些经典的二分思想呢? 59 | 60 | - 猜数字游戏 61 | - 一尺之棰,日取其半,万世不竭 62 | - 有些民间股神,告诉一堆人某个股票会涨,告诉另一半人会跌。后来真涨了,慢慢又告诉信了他的一半人另一个股票会涨,另一半说会跌。就这样韭菜多了总有一些人信奉他为股神。。。 63 | 64 | 其实之前写过博客[《抱歉,我是开发,你居然让我写单测[视频]》](https://zhuanlan.zhihu.com/p/35352024)讲过二分查找,当时主要是为了引入单元测试这个概念的,因为很多不正规的项目代码很糙,更别说写单测了。这里我就直接贴代码啦 65 | 66 | ```py 67 | def binary_search(sorted_array, val): 68 | if not sorted_array: 69 | return -1 70 | 71 | beg = 0 72 | end = len(sorted_array) - 1 73 | 74 | while beg <= end: 75 | mid = int((beg + end) / 2) # beg + (end-beg)/2, 为了屏蔽 python 2/3 差异我用了强转 76 | if sorted_array[mid] == val: 77 | return mid 78 | elif sorted_array[mid] > val: 79 | end = mid - 1 80 | else: 81 | beg = mid + 1 82 | return -1 83 | 84 | 85 | def test_binary_search(): 86 | a = list(range(10)) 87 | 88 | # 正常值 89 | assert binary_search(a, 1) == 1 90 | assert binary_search(a, -1) == -1 91 | 92 | # 异常值 93 | assert binary_search(None, 1) == -1 94 | 95 | # 边界值 96 | assert binary_search(a, 0) == 0 97 | ``` 98 | 99 | 100 | # 思考题 101 | - 给你个挑战,用递归来实现本章的二分查找。你要十分注意边界条件,注意用单测测试呦,在你写代码的时候,可能会碰到边界问题或者无穷递归等。 如果你想不起来,可以看看本章的代码示例 102 | - 二分查找有一个变形,比如我们想在一个有序数组中插入一个值之后,数组仍保持有序,请你找出这个位置。(bisect 模块) 103 | 104 | 105 | # 延伸阅读 106 | 这里没给链接,请善用 google 等搜索引擎和 Dash(mac) 等文档查询工具,在你学习代码的过程中你会非常频繁地使用它们。 107 | 或者如果你有时间也可以跳转到这些模块的源码,看看它们的实现方式。标准库都是些高手写的,肯定能学到一些姿势。 108 | 109 | - 阅读 python 文档关于二分的 bisect 模块。 110 | - 阅读 python 文档 itertools 相关模块和常见的几个函数 takewhile, dropwhile, from_iterable, count, tee 等用法 111 | - [每个程序员都应该会点形式化证明](https://zhuanlan.zhihu.com/p/35364999?group_id=967109293607129088) 112 | 113 | 114 | # Leetcode 115 | 116 | [找旋转过的排序数组中最小的数 find-minimum-in-rotated-sorted-array](https://leetcode.com/problems/find-minimum-in-rotated-sorted-array/description/) 117 | 118 | [已排序的数组中找到第一和最后一个元素 find-first-and-last-position-of-element-in-sorted-array/](https://leetcode.com/problems/find-first-and-last-position-of-element-in-sorted-array/submissions/) 119 | -------------------------------------------------------------------------------- /docs/11_线性查找与二分查找/search.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | number_list = [0, 1, 2, 3, 4, 5, 6, 7] 4 | 5 | 6 | def linear_search(value, iterable): 7 | for index, val in enumerate(iterable): 8 | if val == value: 9 | return index 10 | return -1 11 | 12 | 13 | assert linear_search(5, number_list) == 5 14 | 15 | 16 | def linear_search_v2(predicate, iterable): 17 | for index, val in enumerate(iterable): 18 | if predicate(val): 19 | return index 20 | return -1 21 | 22 | 23 | assert linear_search_v2(lambda x: x == 5, number_list) == 5 24 | 25 | 26 | def linear_search_recusive(array, value): 27 | if len(array) == 0: 28 | return -1 29 | index = len(array)-1 30 | if array[index] == value: 31 | return index 32 | return linear_search_recusive(array[0:index], value) 33 | 34 | 35 | assert linear_search_recusive(number_list, 5) == 5 36 | assert linear_search_recusive(number_list, 8) == -1 37 | assert linear_search_recusive(number_list, 7) == 7 38 | assert linear_search_recusive(number_list, 0) == 0 39 | 40 | 41 | def binary_search_recursive(sorted_array, beg, end, val): 42 | if beg >= end: 43 | return -1 44 | mid = int((beg + end) / 2) # beg + (end-beg)/2 45 | if sorted_array[mid] == val: 46 | return mid 47 | elif sorted_array[mid] > val: 48 | return binary_search_recursive(sorted_array, beg, mid, val) # 注意我依然假设 beg, end 区间是左闭右开的 49 | else: 50 | return binary_search_recursive(sorted_array, mid+1, end, val) 51 | 52 | 53 | def test_binary_search_recursive(): 54 | # 我们测试所有值和边界条件 55 | a = list(range(10)) 56 | for i in a: 57 | assert binary_search_recursive(a, 0, len(a), i) == i 58 | 59 | assert binary_search_recursive(a, 0, len(a), -1) == -1 60 | assert binary_search_recursive(a, 0, len(a), 10) == -1 61 | -------------------------------------------------------------------------------- /docs/12_基本排序算法/basic_sort.md: -------------------------------------------------------------------------------- 1 | # 基本排序算法 2 | 从本章开始讲常见的基于比较的排序算法,先讲三个简单的但是时间复杂度却不太理想的排序算法,包括冒泡排序、选择排序和插入排序。 3 | 4 | 5 | # 冒泡排序 6 | bubble sort 可以说是最简单的一种排序算法了,它的思想如下。对一个数组进行 n-1 轮迭代,每次比较相邻两个元素, 7 | 如果相邻的元素前者大于后者,就交换它们。因为直接在元素上操作而不是返回新的数组,所以是一个 inplace 的操作。 8 | 这里冒泡的意思其实就是每一轮冒泡一个最大的元素就会通过不断比较和交换相邻元素使它转移到最右边。 9 | 10 | 你可以想象假如有 10 个小盆友从左到右站成一排,个头不等。老师想让他们按照个头从低到高站好,于是他开始喊口号。 11 | 每喊一次,从第一个小盆友开始,相邻的小朋友如果身高不是正序就会两两调换,就这样第一轮个头最高的排到了最右边。(冒泡到最右边) 12 | 第二轮依次这么来,从第一个小朋友开始两两交换,这样次高的小盆友又排到了倒数第二个位置。依次类推。 13 | 14 | 15 | 我们在视频里手动模拟下它的过程。 16 | 17 | 18 | ```py 19 | import random 20 | 21 | 22 | def bubble_sort(seq): # O(n^2), n(n-1)/2 = 1/2(n^2 + n) 23 | n = len(seq) 24 | for i in range(n-1): 25 | print(seq) # 我打印出来让你看清楚每一轮最高、次高、次次高...的小朋友会冒泡到右边 26 | for j in range(n-1-i): # 这里之所以 n-1 还需要 减去 i 是因为每一轮冒泡最大的元素都会冒泡到最后,无需再比较 27 | if seq[j] > seq[j+1]: 28 | seq[j], seq[j+1] = seq[j+1], seq[j] 29 | print(seq) 30 | 31 | 32 | def test_bubble_sort(): 33 | seq = list(range(10)) # 注意 python3 返回迭代器,所以我都用 list 强转了,python2 range 返回的就是 list 34 | random.shuffle(seq) # shuffle inplace 操作,打乱数组 35 | bubble_sort(seq) 36 | assert seq == sorted(seq) # 注意呦,内置的 sorted 就不是 inplace 的,它返回一个新的数组,不影响传入的参数 37 | 38 | """ 我打印出来让你看到每次从最高到次高的小盆友就这么排好序了,因为是随机数,你第一个没有排序的数组应该和我的不一样 39 | [3, 4, 5, 0, 9, 1, 7, 8, 6, 2] 40 | [3, 4, 0, 5, 1, 7, 8, 6, 2, 9] 41 | [3, 0, 4, 1, 5, 7, 6, 2, 8, 9] 42 | [0, 3, 1, 4, 5, 6, 2, 7, 8, 9] 43 | [0, 1, 3, 4, 5, 2, 6, 7, 8, 9] 44 | [0, 1, 3, 4, 2, 5, 6, 7, 8, 9] 45 | [0, 1, 3, 2, 4, 5, 6, 7, 8, 9] 46 | [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] 47 | [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] 48 | [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] 49 | """ 50 | ``` 51 | 52 | 53 | 54 | # 选择排序 55 | 刚才看到冒泡是每轮迭代中,如果相邻的两个元素前者大于后者了就交换两个相邻元素(假设正序排序)。其实还有一种思路就是, 56 | 每次我们找到最小的元素插入迭代的起始位置,这样每个位置从它自己的位置开始它就是最小的了,一圈下来数组就有序了。 57 | 选择可以理解为 一个 0 到 n-1 的迭代,每次向后查找选择一个最小的元素。 58 | 59 | 同样小盆友又来啦,这次我们从第一个开始,从头到尾找一个个头最小的小盆友,然后把它和第一个小盆友交换。 60 | 然后从第二个小盆友开始采取同样的策略,这样一圈下来小盆友就有序了。 61 | 62 | ```py 63 | def select_sort(seq): 64 | n = len(seq) 65 | for i in range(n-1): 66 | min_idx = i # 我们假设当前下标的元素是最小的 67 | for j in range(i+1, n): # 从 i 的后边开始找到最小的元素,得到它的下标 68 | if seq[j] < seq[min_idx]: 69 | min_idx = j # 一个 j 循环下来之后就找到了最小的元素它的下标 70 | if min_idx != i: # swap 71 | seq[i], seq[min_idx] = seq[min_idx], seq[i] 72 | 73 | 74 | def test_select_sort(): 75 | seq = list(range(10)) 76 | random.shuffle(seq) 77 | select_sort(seq) 78 | assert seq == sorted(seq) 79 | 80 | """ 81 | [4, 7, 5, 3, 6, 0, 2, 9, 8, 1] 82 | [0, 7, 5, 3, 6, 4, 2, 9, 8, 1] 83 | [0, 1, 5, 3, 6, 4, 2, 9, 8, 7] 84 | [0, 1, 2, 3, 6, 4, 5, 9, 8, 7] 85 | [0, 1, 2, 3, 6, 4, 5, 9, 8, 7] 86 | [0, 1, 2, 3, 4, 6, 5, 9, 8, 7] 87 | [0, 1, 2, 3, 4, 5, 6, 9, 8, 7] 88 | [0, 1, 2, 3, 4, 5, 6, 9, 8, 7] 89 | [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] 90 | [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] 91 | 92 | """ 93 | ``` 94 | 95 | 96 | # 插入排序 97 | 插入排序很多教科书都是用扑克牌的例子讲的,想象你手里有一些扑克牌,它们顺序是散乱的,现在需要你把它们整理成有序的,你会怎么做呢? 98 | 首先拿最顶上的一张,然后拿第二张,第二张点数大,你就把第二张放在第一张的下边,否则放在第一张上边。 99 | 当你拿第三张的时候,你同样会找到适合它大小的位置插入进去。 100 | 101 | 换成小朋友一样,第一个小盆友只有一个人我们假设是有序的,然后第二个小盆友会跟第一个比,如果第一个高就交换位置。 102 | 接下来第三个小盆友从第二个位置开始比较,如果没第二个高就交换位置,然后没第一个高也交换位置,保持前边三个小盆友身高有序就好。 103 | 依次类推,等到最后一个小盆友也转移到合适的位置,整个队列就是有序的了。 104 | 105 | 插入排序就是这个道理, 每次挑选下一个元素插入已经排序的数组中,初始时已排序数组只有一个元素。我们就直接上代码吧。 106 | 107 | 108 | ```py 109 | def insertion_sort(seq): 110 | """ 每次挑选下一个元素插入已经排序的数组中,初始时已排序数组只有一个元素""" 111 | n = len(seq) 112 | print(seq) 113 | for i in range(1, n): 114 | value = seq[i] # 保存当前位置的值,因为转移的过程中它的位置可能被覆盖 115 | # 找到这个值的合适位置,使得前边的数组有序 [0,i] 有序 116 | pos = i 117 | while pos > 0 and value < seq[pos-1]: 118 | seq[pos] = seq[pos-1] # 如果前边的元素比它大,就让它一直前移 119 | pos -= 1 120 | seq[pos] = value # 找到了合适的位置赋值就好 121 | print(seq) 122 | 123 | 124 | """ 不断把新元素放到已经有序的数组中 125 | [1, 7, 3, 0, 9, 4, 8, 2, 6, 5] 126 | [1, 7, 3, 0, 9, 4, 8, 2, 6, 5] 127 | [1, 3, 7, 0, 9, 4, 8, 2, 6, 5] 128 | [0, 1, 3, 7, 9, 4, 8, 2, 6, 5] 129 | [0, 1, 3, 7, 9, 4, 8, 2, 6, 5] 130 | [0, 1, 3, 4, 7, 9, 8, 2, 6, 5] 131 | [0, 1, 3, 4, 7, 8, 9, 2, 6, 5] 132 | [0, 1, 2, 3, 4, 7, 8, 9, 6, 5] 133 | [0, 1, 2, 3, 4, 6, 7, 8, 9, 5] 134 | [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] 135 | """ 136 | ``` 137 | 138 | 139 | # 思考题 140 | - 本章介绍的几个排序算法平均时间复杂度是多少? 141 | - 请你补充插入排序的单元测试代码 142 | 143 | 144 | # 延伸阅读 145 | - 《Data Structures and Algorithms in Python》第5章 146 | -------------------------------------------------------------------------------- /docs/12_基本排序算法/basic_sort.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | import random 5 | 6 | 7 | def bubble_sort(seq): # O(n^2), n(n-1)/2 = 1/2(n^2 + n) 8 | n = len(seq) 9 | for i in range(n-1): 10 | print(seq) # 我打印出来让你看清楚每一轮最高、次高、次次高...的小朋友会冒泡到右边 11 | for j in range(n-1-i): # 这里之所以 n-1 还需要 减去 i 是因为每一轮冒泡最大的元素都会冒泡到最后,无需再比较 12 | if seq[j] > seq[j+1]: 13 | seq[j], seq[j+1] = seq[j+1], seq[j] 14 | print(seq) 15 | 16 | 17 | def test_bubble_sort(): 18 | seq = list(range(10)) # 注意 python3 返回迭代器,所以我都用 list 强转了,python2 range 返回的就是 list 19 | random.shuffle(seq) # shuffle inplace 操作,打乱数组 20 | sorted_seq = sorted(seq) # 注意呦,内置的 sorted 就不是 inplace 的,它返回一个新的数组,不影响传入的参数 21 | bubble_sort(seq) 22 | assert seq == sorted_seq 23 | 24 | 25 | def select_sort(seq): 26 | n = len(seq) 27 | for i in range(n-1): 28 | min_idx = i # 我们假设当前下标的元素是最小的 29 | for j in range(i+1, n): # 从 i 的后边开始找到最小的元素,得到它的下标 30 | if seq[j] < seq[min_idx]: 31 | min_idx = j # 一个 j 循环下来之后就找到了最小的元素它的下标 32 | if min_idx != i: # swap 33 | seq[i], seq[min_idx] = seq[min_idx], seq[i] 34 | 35 | 36 | def test_select_sort(): 37 | seq = list(range(10)) 38 | random.shuffle(seq) 39 | sorted_seq = sorted(seq) 40 | select_sort(seq) 41 | assert seq == sorted_seq 42 | 43 | 44 | def insertion_sort(seq): 45 | """ 每次挑选下一个元素插入已经排序的数组中,初始时已排序数组只有一个元素""" 46 | n = len(seq) 47 | print(seq) 48 | for i in range(1, n): 49 | value = seq[i] # 保存当前位置的值,因为转移的过程中它的位置可能被覆盖 50 | # 找到这个值的合适位置,使得前边的数组有序 [0,i] 有序 51 | pos = i 52 | while pos > 0 and value < seq[pos-1]: 53 | seq[pos] = seq[pos-1] # 如果前边的元素比它大,就让它一直前移 54 | pos -= 1 55 | seq[pos] = value # 找到了合适的位置赋值就好 56 | print(seq) 57 | -------------------------------------------------------------------------------- /docs/13_高级排序算法/advanced_sorting.md: -------------------------------------------------------------------------------- 1 | # 高级排序算法 2 | 3 | 本章开始讲几个高级一些的排序算法,因为涉及到分治、递归和一些高级数据结构等,所以比前一章节的基本排序要稍微难理解一些。包括: 4 | 5 | - [分治法与归并排序](./merge_sort.md) 6 | - [快速排序](./quick_sort.md) 7 | 8 | 在讲完二叉树之后,我们会看下它的应用: 9 | 10 | - 堆和堆排序 11 | -------------------------------------------------------------------------------- /docs/13_高级排序算法/merge_sort.md: -------------------------------------------------------------------------------- 1 | # 分治法 (Divide and Conquer) 2 | 3 | 很多有用的算法结构上是递归的,为了解决一个特定问题,算法一次或者多次递归调用其自身以解决若干子问题。 4 | 这些算法典型地遵循分治法的思想:将原问题分解为几个规模较小但是类似于原问题的子问题,递归求解这些子问题, 5 | 然后再合并这些问题的解来建立原问题的解。 6 | 7 | 分治法在每层递归时有三个步骤: 8 | 9 | - **分解**原问题为若干子问题,这些子问题是原问题的规模最小的实例 10 | - **解决**这些子问题,递归地求解这些子问题。当子问题的规模足够小,就可以直接求解 11 | - **合并**这些子问题的解成原问题的解 12 | 13 | 14 | # 归并排序 15 | 现在我们就来看下归并排序是是如何利用分治法解决问题的。 16 | 17 | - **分解**:将待排序的 n 个元素分成各包含 n/2 个元素的子序列 18 | - **解决**:使用归并排序递归排序两个子序列 19 | - **合并**:合并两个已经排序的子序列以产生已排序的答案 20 | 21 | 考虑我们排序这个数组:[10,23,51,18,4,31,13,5] ,我们递归地将数组进行分解 22 | 23 | ![](./merge_sort_split.png) 24 | 25 | 当数组被完全分隔成只有单个元素的数组时,我们需要把它们合并回去,每次两两合并成一个有序的序列。 26 | 27 | ![](./merge_sort_merge.png) 28 | 29 | 用递归代码来描述这个问题: 30 | 31 | ```py 32 | def merge_sort(seq): 33 | if len(seq) <= 1: # 只有一个元素是递归出口 34 | return seq 35 | else: 36 | mid = int(len(seq)/2) 37 | left_half = merge_sort(seq[:mid]) 38 | right_half = merge_sort(seq[mid:]) 39 | 40 | # 合并两个有序的数组 41 | new_seq = merge_sorted_list(left_half, right_half) 42 | return new_seq 43 | ``` 44 | 45 | 注意我们这里有一个函数没实现,就是如何合并两个有序数组 merge_sorted_list。其实你在纸上画一画, 46 | 合并两个有序数组并不难实现。 47 | 48 | ![](./merge_sorted_array.png) 49 | 50 | ![](./merge_sorted_array_2.png) 51 | 52 | 53 | ```py 54 | def merge_sorted_list(sorted_a, sorted_b): 55 | """ 合并两个有序序列,返回一个新的有序序列 56 | 57 | :param sorted_a: 58 | :param sorted_b: 59 | """ 60 | length_a, length_b = len(sorted_a), len(sorted_b) 61 | a = b = 0 62 | new_sorted_seq = list() 63 | 64 | while a < length_a and b < length_b: 65 | if sorted_a[a] < sorted_b[b]: 66 | new_sorted_seq.append(sorted_a[a]) 67 | a += 1 68 | else: 69 | new_sorted_seq.append(sorted_b[b]) 70 | b += 1 71 | 72 | # 最后别忘记把多余的都放到有序数组里 73 | if a < length_a: 74 | new_sorted_seq.extend(sorted_a[a:]) 75 | else: 76 | new_sorted_seq.extend(sorted_b[b:]) 77 | 78 | return new_sorted_seq 79 | ``` 80 | 81 | 这样就实现了归并排序,并且你会发现它返回一个新的数组而不是修改原有数组。 82 | 83 | 84 | # 时间复杂度 85 | 我们来简单看下它归并排序的时间复杂度,假设排序 n 个数字时间复杂度是 T(n),这里为了方便假设 n 是 2 的幂 86 | 87 | \begin{align} 88 | T(n)= \begin{cases} c, & \text {if $n$ = 1} \\ 2T(n/2)+cn, & \text{if $n$ > 1} \end{cases} 89 | \end{align} 90 | 91 | ![](./merge_sort_recursion_tree.png) 92 | 93 | 总的代价是 $cnlg(n)+cn$ ,忽略常数项可以认为是 O(nlg(n))。如果这个图看不懂,我们自己求解下也不难,首先我们简化一下, 94 | 把常数系数当成 1,得到以下递归式: 95 | 96 | \begin{align} 97 | T(n)= \begin{cases} 1, & \text {if $n$ = 1} \\ 2T(n/2)+n, & \text{if $n$ > 1} \end{cases} 98 | \end{align} 99 | 100 | ![](./tn.png) 101 | 102 | 103 | # 思考题 104 | - 请你完成归并排序的单元测试 105 | - 这里实现的归并排序是 inplace 的吗? 106 | - 归并排序是稳定的吗?稳定指的是排序前后相同大小的数字依然保持相对顺序。 107 | 108 | # 延伸阅读 109 | - 《算法导论》第 2 章和第 4 章,你需要了解下『主定理』,以及如何求解形如 $T(n)=aT(n/b) + f(n)$ 的递归式复杂度 110 | - 了解算法导论上递归式的三种求解方法:代入法,递归树法,主方法 111 | -------------------------------------------------------------------------------- /docs/13_高级排序算法/merge_sort.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | def merge_sort(seq): 5 | if len(seq) <= 1: # 只有一个元素是递归出口 6 | return seq 7 | else: 8 | mid = int(len(seq)/2) 9 | left_half = merge_sort(seq[:mid]) 10 | right_half = merge_sort(seq[mid:]) 11 | 12 | # 合并两个有序的数组 13 | new_seq = merge_sorted_list(left_half, right_half) 14 | return new_seq 15 | 16 | 17 | def merge_sorted_list(sorted_a, sorted_b): 18 | """ 合并两个有序序列,返回一个新的有序序列 19 | 20 | :param sorted_a: 21 | :param sorted_b: 22 | """ 23 | length_a, length_b = len(sorted_a), len(sorted_b) 24 | a = b = 0 25 | new_sorted_seq = list() 26 | 27 | while a < length_a and b < length_b: 28 | if sorted_a[a] < sorted_b[b]: 29 | new_sorted_seq.append(sorted_a[a]) 30 | a += 1 31 | else: 32 | new_sorted_seq.append(sorted_b[b]) 33 | b += 1 34 | 35 | # 如果 a或b 中还有剩余元素,需要放到最后 36 | if a < length_a: 37 | new_sorted_seq.extend(sorted_a[a:]) 38 | else: 39 | new_sorted_seq.extend(sorted_b[b:]) 40 | 41 | return new_sorted_seq 42 | 43 | 44 | def test_merge_sort(): 45 | import random 46 | seq = list(range(10)) 47 | random.shuffle(seq) 48 | assert merge_sort(seq) == sorted(seq) 49 | -------------------------------------------------------------------------------- /docs/13_高级排序算法/merge_sort_merge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PegasusWang/python_data_structures_and_algorithms/3469a79c34b6c08ae52797c3974b49fbfa8cca51/docs/13_高级排序算法/merge_sort_merge.png -------------------------------------------------------------------------------- /docs/13_高级排序算法/merge_sort_recursion_tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PegasusWang/python_data_structures_and_algorithms/3469a79c34b6c08ae52797c3974b49fbfa8cca51/docs/13_高级排序算法/merge_sort_recursion_tree.png -------------------------------------------------------------------------------- /docs/13_高级排序算法/merge_sort_split.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PegasusWang/python_data_structures_and_algorithms/3469a79c34b6c08ae52797c3974b49fbfa8cca51/docs/13_高级排序算法/merge_sort_split.png -------------------------------------------------------------------------------- /docs/13_高级排序算法/merge_sorted_array.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PegasusWang/python_data_structures_and_algorithms/3469a79c34b6c08ae52797c3974b49fbfa8cca51/docs/13_高级排序算法/merge_sorted_array.png -------------------------------------------------------------------------------- /docs/13_高级排序算法/merge_sorted_array_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PegasusWang/python_data_structures_and_algorithms/3469a79c34b6c08ae52797c3974b49fbfa8cca51/docs/13_高级排序算法/merge_sorted_array_2.png -------------------------------------------------------------------------------- /docs/13_高级排序算法/partition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PegasusWang/python_data_structures_and_algorithms/3469a79c34b6c08ae52797c3974b49fbfa8cca51/docs/13_高级排序算法/partition.png -------------------------------------------------------------------------------- /docs/13_高级排序算法/quick_sort.md: -------------------------------------------------------------------------------- 1 | # 快速排序 2 | 3 | 快速排序名字可不是盖的,很多程序语言标准库实现的内置排序都有它的身影,我们就直奔主题吧。 4 | 和归并排序一样,快排也是一种分而治之(divide and conquer)的策略。归并排序把数组递归成只有单个元素的数组,之后再不断两两 5 | 合并,最后得到一个有序数组。这里的递归基本条件就是只包含一个元素的数组,当数组只包含一个元素的时候,我们可以认为它本来就是有序的(当然空数组也不用排序)。 6 | 7 | 快排的工作过程其实比较简单,三步走: 8 | 9 | - 选择基准值 pivot 将数组分成两个子数组:小于基准值的元素和大于基准值的元素。这个过程称之为 partition 10 | 11 | - 对这两个子数组进行快速排序。 12 | 13 | - 合并结果 14 | 15 | ![](./quick_sort.png) 16 | 17 | 根据这个想法我们可以快速写出快排的代码,简直就是在翻译上边的描述: 18 | 19 | ```py 20 | def quicksort(array): 21 | size = len(array) 22 | if not array or size < 2: # NOTE: 递归出口,空数组或者只有一个元素的数组都是有序的 23 | return array 24 | pivot_idx = 0 25 | pivot = array[pivot_idx] 26 | less_part = [array[i] for i in range(size) if array[i] <= pivot and pivot_idx != i] 27 | great_part = [array[i] for i in range(size) if array[i] > pivot and pivot_idx != i] 28 | return quicksort(less_part) + [pivot] + quicksort(great_part) 29 | 30 | def test_quicksort(): 31 | import random 32 | seq = list(range(10)) 33 | random.shuffle(seq) 34 | assert quicksort(seq) == sorted(seq) 35 | ``` 36 | 是不是很简单,下次面试官让你手写快排你再写不出来就有点不太合适啦。 当然这个实现有两个不好的地方: 37 | 38 | - 第一是它需要额外的存储空间,我们想实现 inplace 原地排序。 39 | - 第二是它的 partition 操作每次都要两次遍历整个数组,我们想改善一下。 40 | 41 | 这里我们就来优化一下它,实现 inplace 排序并且改善下 partition 操作。新的代码大概如下: 42 | 43 | ```py 44 | def quicksort_inplace(array, beg, end): # 注意这里我们都用左闭右开区间,end 传入 len(array) 45 | if beg < end: # beg == end 的时候递归出口 46 | pivot = partition(array, beg, end) 47 | quicksort_inplace(array, beg, pivot) 48 | quicksort_inplace(array, pivot+1, end) 49 | ``` 50 | 51 | 能否实现只遍历一次数组就可以完成 partition 操作呢?实际上是可以的。我们设置首位俩个指针 left, right,两个指针不断向中间收拢。如果遇到 left 位置的元素大于 pivot 并且 right 指向的元素小于 pivot,我们就交换这俩元素,当 left > right 的时候退出就行了,这样实现了一次遍历就完成了 partition。如果你感觉懵逼,纸上画画就立马明白了。我们来撸代码实现: 52 | 53 | ![](./partition.png) 54 | 55 | ```py 56 | def partition(array, beg, end): 57 | pivot_index = beg 58 | pivot = array[pivot_index] 59 | left = pivot_index + 1 60 | right = end - 1 # 开区间,最后一个元素位置是 end-1 [0, end-1] or [0: end),括号表示开区间 61 | 62 | while True: 63 | # 从左边找到比 pivot 大的 64 | while left <= right and array[left] < pivot: 65 | left += 1 66 | 67 | while right >= left and array[right] >= pivot: 68 | right -= 1 69 | 70 | if left > right: 71 | break 72 | else: 73 | array[left], array[right] = array[right], array[left] 74 | 75 | array[pivot_index], array[right] = array[right], array[pivot_index] 76 | return right # 新的 pivot 位置 77 | 78 | 79 | def test_partition(): 80 | l = [4, 1, 2, 8] 81 | assert partition(l, 0, len(l)) == 2 82 | l = [1, 2, 3, 4] 83 | assert partition(l, 0, len(l)) == 0 84 | l = [4, 3, 2, 1] 85 | assert partition(l, 0, len(l)) 86 | ``` 87 | 88 | 大功告成,新的快排就实现好了。 89 | 90 | # 时间复杂度 91 | 在比较理想的情况下,比如数组每次都被 pivot 均分,我们可以得到递归式: 92 | 93 | T(n) = 2T(n/2) + n 94 | 95 | 上一节我们讲过通过递归树得到它的时间复杂度是 O(nlog(n))。即便是很坏的情况,比如 pivot 每次都把数组按照 1:9 划分,依然是 O(nlog(n)),感兴趣请阅读算法导论相关章节。 96 | 97 | ![](quicksort_worst.png) 98 | 99 | 100 | # 思考题 101 | - 请你补充 quicksort_inplace 的单元测试 102 | - 最坏的情况下快排的时间复杂度是多少?什么时候会发生这种情况? 103 | - 我们实现的快排是稳定的啵? 104 | - 选择基准值如果选不好就可能导致复杂度升高,算导中提到了一种『median of 3』策略,就是说选择 pivot 的时候 从子数组中随机选三个元素,再取它的中位数,你能实现这个想法吗?这里我们的代码很简单地取了第一个元素作为 pivot 105 | - 利用快排中的 partition 操作,我们还能实现另一个算法,nth_element,快速查找一个无序数组中的第 n 大元素,请你尝试实现它并编写单测。其实这个函数是 C++ STL 中的一个函数。 106 | - 你知道 Python 内置的 sorted 如何实现的吗?请你 Google 相关资料了解下。很多内置的排序都使用了快排的改良版。 107 | 108 | 109 | # 延伸阅读 110 | - 《算法导论》第 7 章 111 | - [《面试必备 | 排序算法的Python实现》](https://zhuanlan.zhihu.com/p/36419582) 112 | 113 | # 总结 114 | 115 | 面试经常问的就是常用排序算法的时间空间复杂,这里列一个表格方便记忆: 116 | 117 | | 排序算法 | 最差时间分析 | 平均时间复杂度 | 稳定度 | 空间复杂度 | 118 | |------------|--------------|----------------|--------|----------------| 119 | | 冒泡排序 | O(n^2) | O(n^2) | 稳定 | O(1) | 120 | | 选择排序 | O(n^2) | O(n^2) | 不稳定 | O(1) | 121 | | 插入排序 | O(n^2) | O(n^2) | 稳定 | O(1) | 122 | | 二叉树排序 | O(n^2) | O(n\*log2n) | 不一顶 | O(n) | 123 | | 快速排序 | O(n^2) | O(n\*log2n) | 不稳定 | O(log2n)\~O(n) | 124 | | 堆排序 | O(n\*log2n) | O(n\*log2n) | 不稳定 | O(1) | 125 | 126 | [数据结构与算法-排序篇-Python描述](https://blog.csdn.net/mrlevo520/article/details/77829204) 127 | 128 | # Leetcode 129 | 130 | 无序数组寻找第 k 大的数字,不止一种方法。 131 | https://leetcode.com/problems/kth-largest-element-in-an-array/description/ 132 | -------------------------------------------------------------------------------- /docs/13_高级排序算法/quick_sort.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PegasusWang/python_data_structures_and_algorithms/3469a79c34b6c08ae52797c3974b49fbfa8cca51/docs/13_高级排序算法/quick_sort.png -------------------------------------------------------------------------------- /docs/13_高级排序算法/quicksort.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | def quicksort(array): 5 | size = len(array) 6 | if not array or size < 2: # NOTE: 递归出口,空数组或者只有一个元素的数组都是有序的 7 | return array 8 | pivot_idx = 0 9 | pivot = array[pivot_idx] 10 | less_part = [array[i] for i in range(size) if array[i] <= pivot and pivot_idx != i] 11 | great_part = [array[i] for i in range(size) if array[i] > pivot and pivot_idx != i] 12 | return quicksort(less_part) + [pivot] + quicksort(great_part) 13 | 14 | 15 | def test_quicksort(): 16 | import random 17 | seq = list(range(10)) 18 | random.shuffle(seq) 19 | assert quicksort(seq) == sorted(seq) # 用内置的sorted 『对拍』 20 | 21 | 22 | def quicksort_inplace(array, beg, end): # 注意这里我们都用左闭右开区间 23 | if beg < end: # beg == end 的时候递归出口 24 | pivot = partition(array, beg, end) 25 | quicksort_inplace(array, beg, pivot) 26 | quicksort_inplace(array, pivot + 1, end) 27 | 28 | 29 | def partition(array, beg, end): 30 | """对给定数组执行 partition 操作,返回新的 pivot 位置""" 31 | pivot_index = beg 32 | pivot = array[pivot_index] 33 | left = pivot_index + 1 34 | right = end - 1 # 开区间,最后一个元素位置是 end-1 [0, end-1] or [0: end),括号表示开区间 35 | 36 | while True: 37 | # 从左边找到比 pivot 大的 38 | while left <= right and array[left] < pivot: 39 | left += 1 40 | 41 | while right >= left and array[right] >= pivot: 42 | right -= 1 43 | 44 | if left > right: 45 | break 46 | else: 47 | array[left], array[right] = array[right], array[left] 48 | 49 | array[pivot_index], array[right] = array[right], array[pivot_index] 50 | return right # 新的 pivot 位置 51 | 52 | 53 | def test_partition(): 54 | l = [4, 1, 2, 8] 55 | assert partition(l, 0, len(l)) == 2 56 | l = [1, 2, 3, 4] 57 | assert partition(l, 0, len(l)) == 0 58 | l = [4, 3, 2, 1] 59 | assert partition(l, 0, len(l)) == 3 60 | l = [1] 61 | assert partition(l, 0, len(l)) == 0 62 | l = [2,1] 63 | assert partition(l, 0, len(l)) == 1 64 | 65 | 66 | def test_quicksort_inplace(): 67 | import random 68 | seq = list(range(10)) 69 | random.shuffle(seq) 70 | sorted_seq = sorted(seq) 71 | quicksort_inplace(seq, 0, len(seq)) 72 | assert seq == sorted_seq 73 | 74 | 75 | def nth_element(array, beg, end, nth): 76 | """查找一个数组第 n 大元素""" 77 | if beg < end: 78 | pivot_idx = partition(array, beg, end) 79 | if pivot_idx == nth - 1: # 数组小标从 0 开始 80 | return array[pivot_idx] 81 | elif pivot_idx > nth - 1: 82 | return nth_element(array, beg, pivot_idx, nth) 83 | else: 84 | return nth_element(array, pivot_idx + 1, end, nth) 85 | 86 | 87 | def test_nth_element(): 88 | l1 = [3, 5, 4, 2, 1] 89 | assert nth_element(l1, 0, len(l1), 3) == 3 90 | assert nth_element(l1, 0, len(l1), 2) == 2 91 | 92 | l = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 93 | for i in l: 94 | assert nth_element(l, 0, len(l), i) == i 95 | for i in reversed(l): 96 | assert nth_element(l, 0, len(l), i) == i 97 | 98 | array = [3, 2, 1, 5, 6, 4] 99 | assert nth_element(array, 0, len(array), 2) == 2 100 | 101 | array = [2,1] 102 | assert nth_element(array, 0, len(array), 1) == 1 103 | assert nth_element(array, 0, len(array), 2) == 2 104 | 105 | array = [3,3,3,3,3,3,3,3,3] 106 | assert nth_element(array, 0, len(array), 1) == 3 107 | 108 | 109 | if __name__ == '__main__': 110 | test_nth_element() 111 | -------------------------------------------------------------------------------- /docs/13_高级排序算法/quicksort_worst.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PegasusWang/python_data_structures_and_algorithms/3469a79c34b6c08ae52797c3974b49fbfa8cca51/docs/13_高级排序算法/quicksort_worst.png -------------------------------------------------------------------------------- /docs/13_高级排序算法/test.sh: -------------------------------------------------------------------------------- 1 | 2 | 3 | #!/usr/bin/env bash 4 | 5 | # pip install when-changed, 监控文件变动并且文件修改之后自动执行 pytest 单测,方便我们边修改边跑测试 6 | when-changed -v -r -1 -s ./ "py.test -s $1" 7 | 8 | -------------------------------------------------------------------------------- /docs/13_高级排序算法/tn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PegasusWang/python_data_structures_and_algorithms/3469a79c34b6c08ae52797c3974b49fbfa8cca51/docs/13_高级排序算法/tn.png -------------------------------------------------------------------------------- /docs/14_树与二叉树/binary_tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PegasusWang/python_data_structures_and_algorithms/3469a79c34b6c08ae52797c3974b49fbfa8cca51/docs/14_树与二叉树/binary_tree.png -------------------------------------------------------------------------------- /docs/14_树与二叉树/binary_tree_level.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PegasusWang/python_data_structures_and_algorithms/3469a79c34b6c08ae52797c3974b49fbfa8cca51/docs/14_树与二叉树/binary_tree_level.png -------------------------------------------------------------------------------- /docs/14_树与二叉树/btree.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | from collections import deque 5 | 6 | 7 | class Queue(object): # 借助内置的 deque 我们可以迅速实现一个 Queue 8 | def __init__(self): 9 | self._items = deque() 10 | 11 | def append(self, value): 12 | return self._items.append(value) 13 | 14 | def pop(self): 15 | return self._items.popleft() 16 | 17 | def empty(self): 18 | return len(self._items) == 0 19 | 20 | 21 | class Stack(object): 22 | def __init__(self): 23 | self._items = deque() 24 | 25 | def push(self, value): 26 | return self._items.append(value) 27 | 28 | def pop(self): 29 | return self._items.pop() 30 | 31 | def empty(self): 32 | return len(self._items) == 0 33 | 34 | 35 | class BinTreeNode(object): 36 | def __init__(self, data, left=None, right=None): 37 | self.data, self.left, self.right = data, left, right 38 | 39 | 40 | class BinTree(object): 41 | def __init__(self, root=None): 42 | self.root = root 43 | 44 | @classmethod 45 | def build_from(cls, node_list): 46 | """build_from 47 | 48 | :param node_list: {'data': 'A', 'left': None, 'right': None, 'is_root': False} 49 | """ 50 | node_dict = {} 51 | for node_data in node_list: 52 | data = node_data['data'] 53 | node_dict[data] = BinTreeNode(data) 54 | for node_data in node_list: 55 | data = node_data['data'] 56 | node = node_dict[data] 57 | if node_data['is_root']: 58 | root = node 59 | node.left = node_dict.get(node_data['left']) 60 | node.right = node_dict.get(node_data['right']) 61 | return cls(root) 62 | 63 | def preorder_trav(self, subtree): 64 | if subtree is not None: 65 | print(subtree.data) 66 | self.preorder_trav(subtree.left) 67 | self.preorder_trav(subtree.right) 68 | 69 | def preorder_trav_use_stack(self, subtree): 70 | """递归的方式其实是计算机帮我们实现了栈结构,我们可以自己显示的用栈来实现""" 71 | s = Stack() 72 | if subtree: 73 | s.push(subtree) 74 | while not s.empty(): 75 | top_node = s.pop() 76 | print(top_node.data) # 注意这里我用了 print,你可以用 yield 产出值然后在调用的地方转成 list 77 | if top_node.right: 78 | s.push(top_node.right) 79 | if top_node.left: 80 | s.push(top_node.left) 81 | 82 | def inorder_trav(self, subtree): 83 | if subtree is not None: 84 | self.inorder_trav(subtree.left) 85 | print(subtree.data) 86 | self.inorder_trav(subtree.right) 87 | 88 | def yield_inorder(self, subtree): # for val in yield_inorder(root): print(val) 89 | if subtree: 90 | yield from self.inorder(subtree.left) 91 | yield subtree.val 92 | yield from self.inorder(subtree.right) 93 | 94 | def reverse(self, subtree): 95 | if subtree is not None: 96 | subtree.left, subtree.right = subtree.right, subtree.left 97 | self.reverse(subtree.left) 98 | self.reverse(subtree.right) 99 | 100 | def layer_trav(self, subtree): 101 | cur_nodes = [subtree] 102 | next_nodes = [] 103 | while cur_nodes or next_nodes: 104 | for node in cur_nodes: 105 | print(node.data) 106 | if node.left: 107 | next_nodes.append(node.left) 108 | if node.right: 109 | next_nodes.append(node.right) 110 | cur_nodes = next_nodes # 继续遍历下一层 111 | next_nodes = [] 112 | 113 | def layer_trav_use_queue(self, subtree): 114 | q = Queue() 115 | q.append(subtree) 116 | while not q.empty(): 117 | cur_node = q.pop() 118 | print(cur_node.data) 119 | if cur_node.left: 120 | q.append(cur_node.left) 121 | if cur_node.right: 122 | q.append(cur_node.right) 123 | 124 | 125 | node_list = [ 126 | {'data': 'A', 'left': 'B', 'right': 'C', 'is_root': True}, 127 | {'data': 'B', 'left': 'D', 'right': 'E', 'is_root': False}, 128 | {'data': 'D', 'left': None, 'right': None, 'is_root': False}, 129 | {'data': 'E', 'left': 'H', 'right': None, 'is_root': False}, 130 | {'data': 'H', 'left': None, 'right': None, 'is_root': False}, 131 | {'data': 'C', 'left': 'F', 'right': 'G', 'is_root': False}, 132 | {'data': 'F', 'left': None, 'right': None, 'is_root': False}, 133 | {'data': 'G', 'left': 'I', 'right': 'J', 'is_root': False}, 134 | {'data': 'I', 'left': None, 'right': None, 'is_root': False}, 135 | {'data': 'J', 'left': None, 'right': None, 'is_root': False}, 136 | ] 137 | 138 | 139 | btree = BinTree.build_from(node_list) 140 | print('====先序遍历=====') 141 | btree.preorder_trav(btree.root) 142 | 143 | print('====使用 stack 实现先序遍历=====') 144 | btree.preorder_trav_use_stack(btree.root) 145 | 146 | print('====层序遍历=====') 147 | btree.layer_trav(btree.root) 148 | print('====用队列层序遍历=====') 149 | btree.layer_trav_use_queue(btree.root) 150 | 151 | btree.reverse(btree.root) 152 | print('====反转之后的结果=====') 153 | btree.preorder_trav(btree.root) 154 | -------------------------------------------------------------------------------- /docs/14_树与二叉树/complete_binary_tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PegasusWang/python_data_structures_and_algorithms/3469a79c34b6c08ae52797c3974b49fbfa8cca51/docs/14_树与二叉树/complete_binary_tree.png -------------------------------------------------------------------------------- /docs/14_树与二叉树/family_tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PegasusWang/python_data_structures_and_algorithms/3469a79c34b6c08ae52797c3974b49fbfa8cca51/docs/14_树与二叉树/family_tree.png -------------------------------------------------------------------------------- /docs/14_树与二叉树/full_binary_tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PegasusWang/python_data_structures_and_algorithms/3469a79c34b6c08ae52797c3974b49fbfa8cca51/docs/14_树与二叉树/full_binary_tree.png -------------------------------------------------------------------------------- /docs/14_树与二叉树/perfect_binary_tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PegasusWang/python_data_structures_and_algorithms/3469a79c34b6c08ae52797c3974b49fbfa8cca51/docs/14_树与二叉树/perfect_binary_tree.png -------------------------------------------------------------------------------- /docs/14_树与二叉树/preorder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PegasusWang/python_data_structures_and_algorithms/3469a79c34b6c08ae52797c3974b49fbfa8cca51/docs/14_树与二叉树/preorder.png -------------------------------------------------------------------------------- /docs/14_树与二叉树/tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PegasusWang/python_data_structures_and_algorithms/3469a79c34b6c08ae52797c3974b49fbfa8cca51/docs/14_树与二叉树/tree.png -------------------------------------------------------------------------------- /docs/15_堆与堆排序/heap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PegasusWang/python_data_structures_and_algorithms/3469a79c34b6c08ae52797c3974b49fbfa8cca51/docs/15_堆与堆排序/heap.png -------------------------------------------------------------------------------- /docs/15_堆与堆排序/heap_and_heapsort.md: -------------------------------------------------------------------------------- 1 | # 堆(heap) 2 | 前面我们讲了两种使用分治和递归解决排序问题的归并排序和快速排序,中间又穿插了一把树和二叉树, 3 | 本章我们开始介绍另一种有用的数据结构堆(heap), 以及借助堆来实现的堆排序,相比前两种排序算法要稍难实现一些。 4 | 最后我们简单提一下 python 标准库内置的 heapq 模块。 5 | 6 | 7 | # 什么是堆? 8 | 堆是一种完全二叉树(请你回顾下上一章的概念),有最大堆和最小堆两种。 9 | 10 | - 最大堆: 对于每个非叶子节点 V,V 的值都比它的两个孩子大,称为 最大堆特性(heap order property) 11 | 最大堆里的根总是存储最大值,最小的值存储在叶节点。 12 | - 最小堆:和最大堆相反,每个非叶子节点 V,V 的两个孩子的值都比它大。 13 | 14 | ![](./heap.png) 15 | 16 | # 堆的操作 17 | 堆提供了很有限的几个操作: 18 | 19 | - 插入新的值。插入比较麻烦的就是需要维持堆的特性。需要 sift-up 操作,具体会在视频和代码里解释,文字描述起来比较麻烦。 20 | - 获取并移除根节点的值。每次我们都可以获取最大值或者最小值。这个时候需要把底层最右边的节点值替换到 root 节点之后 21 | 执行 sift-down 操作。 22 | 23 | ![](./siftup.png) 24 | ![](./siftdown.png) 25 | 26 | # 堆的表示 27 | 上一章我们用一个节点类和二叉树类表示树,这里其实用数组就能实现堆。 28 | 29 | ![](heap_array.png) 30 | 31 | 仔细观察下,因为完全二叉树的特性,树不会有间隙。对于数组里的一个下标 i,我们可以得到它的父亲和孩子的节点对应的下标: 32 | 33 | ``` 34 | parent = int((i-1) / 2) # 取整 35 | left = 2 * i + 1 36 | right = 2 * i + 2 37 | ``` 38 | 超出下标表示没有对应的孩子节点。 39 | 40 | # 实现一个最大堆 41 | 我们将在视频里详细描述和编写各个操作 42 | 43 | ```py 44 | class MaxHeap(object): 45 | def __init__(self, maxsize=None): 46 | self.maxsize = maxsize 47 | self._elements = Array(maxsize) 48 | self._count = 0 49 | 50 | def __len__(self): 51 | return self._count 52 | 53 | def add(self, value): 54 | if self._count >= self.maxsize: 55 | raise Exception('full') 56 | self._elements[self._count] = value 57 | self._count += 1 58 | self._siftup(self._count-1) # 维持堆的特性 59 | 60 | def _siftup(self, ndx): 61 | if ndx > 0: 62 | parent = int((ndx-1)/2) 63 | if self._elements[ndx] > self._elements[parent]: # 如果插入的值大于 parent,一直交换 64 | self._elements[ndx], self._elements[parent] = self._elements[parent], self._elements[ndx] 65 | self._siftup(parent) # 递归 66 | 67 | def extract(self): 68 | if self._count <= 0: 69 | raise Exception('empty') 70 | value = self._elements[0] # 保存 root 值 71 | self._count -= 1 72 | self._elements[0] = self._elements[self._count] # 最右下的节点放到root后siftDown 73 | self._siftdown(0) # 维持堆特性 74 | return value 75 | 76 | def _siftdown(self, ndx): 77 | left = 2 * ndx + 1 78 | right = 2 * ndx + 2 79 | # determine which node contains the larger value 80 | largest = ndx 81 | if (left < self._count and # 有左孩子 82 | self._elements[left] >= self._elements[largest] and 83 | self._elements[left] >= self._elements[right]): # 原书这个地方没写实际上找的未必是largest 84 | largest = left 85 | elif right < self._count and self._elements[right] >= self._elements[largest]: 86 | largest = right 87 | if largest != ndx: 88 | self._elements[ndx], self._elements[largest] = self._elements[largest], self._elements[ndx] 89 | self._siftdown(largest) 90 | 91 | 92 | def test_maxheap(): 93 | import random 94 | n = 5 95 | h = MaxHeap(n) 96 | for i in range(n): 97 | h.add(i) 98 | for i in reversed(range(n)): 99 | assert i == h.extract() 100 | ``` 101 | 102 | # 实现堆排序 103 | 上边我们实现了最大堆,每次我们都能 extract 一个最大的元素了,于是一个倒序排序函数就能很容易写出来了: 104 | 105 | ```py 106 | def heapsort_reverse(array): 107 | length = len(array) 108 | maxheap = MaxHeap(length) 109 | for i in array: 110 | maxheap.add(i) 111 | res = [] 112 | for i in range(length): 113 | res.append(maxheap.extract()) 114 | return res 115 | 116 | 117 | def test_heapsort_reverse(): 118 | import random 119 | l = list(range(10)) 120 | random.shuffle(l) 121 | assert heapsort_reverse(l) == sorted(l, reverse=True) 122 | ``` 123 | 124 | # Python 里的 heapq 模块 125 | python 其实自带了 heapq 模块,用来实现堆的相关操作,原理是类似的。请你阅读相关文档并使用内置的 heapq 模块完成堆排序。 126 | 一般我们刷题或者写业务代码的时候,使用这个内置的 heapq 模块就够用了,内置的实现了是最小堆。 127 | 128 | 129 | # Top K 问题 130 | 面试题中有这样一类问题,让求出大量数据中的top k 个元素,比如一亿个数字中最大的100个数字。 131 | 对于这种问题有很多种解法,比如直接排序、mapreduce、trie 树、分治法等,当然如果内存够用直接排序是最简单的。 132 | 如果内存不够用呢? 这里我们提一下使用固定大小的堆来解决这个问题的方式。 133 | 134 | 一开始的思路可能是,既然求最大的 k 个数,是不是应该维护一个包含 k 个元素的最大堆呢? 135 | 稍微尝试下你会发现走不通。我们先用数组的前面 k 个元素建立最大堆,然后对剩下的元素进行比对,但是最大堆只能每次获取堆顶 136 | 最大的一个元素,如果我们取下一个大于堆顶的值和堆顶替换,你会发现堆底部的小数一直不会被换掉。如果下一个元素小于堆顶 137 | 就替换也不对,这样可能最大的元素就被我们丢掉了。 138 | 139 | 相反我们用最小堆呢? 140 | 先迭代前 k 个元素建立一个最小堆,之后的元素如果小于堆顶最小值,跳过,否则替换堆顶元素并重新调整堆。你会发现最小堆里 141 | 慢慢就被替换成了最大的那些值,并且最后堆顶是最大的 topk 个值中的最小值。 142 | (比如1000个数找10个,最后堆里剩余的是 [990, 991, 992, 996, 994, 993, 997, 998, 999, 995],第一个 990 最小) 143 | 144 | 按照这个思路很容易写出来代码: 145 | 146 | ```py 147 | import heapq 148 | 149 | 150 | class TopK: 151 | """获取大量元素 topk 大个元素,固定内存 152 | 思路: 153 | 1. 先放入元素前 k 个建立一个最小堆 154 | 2. 迭代剩余元素: 155 | 如果当前元素小于堆顶元素,跳过该元素(肯定不是前 k 大) 156 | 否则替换堆顶元素为当前元素,并重新调整堆 157 | """ 158 | 159 | def __init__(self, iterable, k): 160 | self.minheap = [] 161 | self.capacity = k 162 | self.iterable = iterable 163 | 164 | def push(self, val): 165 | if len(self.minheap) >= self.capacity: 166 | min_val = self.minheap[0] 167 | if val < min_val: # 当然你可以直接 if val > min_val操作,这里我只是显示指出跳过这个元素 168 | pass 169 | else: 170 | heapq.heapreplace(self.minheap, val) # 返回并且pop堆顶最小值,推入新的 val 值并调整堆 171 | else: 172 | heapq.heappush(self.minheap, val) # 前面 k 个元素直接放入minheap 173 | 174 | def get_topk(self): 175 | for val in self.iterable: 176 | self.push(val) 177 | return self.minheap 178 | 179 | 180 | def test(): 181 | import random 182 | i = list(range(1000)) # 这里可以是一个可迭代元素,节省内存 183 | random.shuffle(i) 184 | _ = TopK(i, 10) 185 | print(_.get_topk()) # [990, 991, 992, 996, 994, 993, 997, 998, 999, 995] 186 | 187 | 188 | if __name__ == '__main__': 189 | test() 190 | ``` 191 | 192 | 193 | # 练习题 194 | 195 | - 这里我用最大堆实现了一个 heapsort_reverse 函数,请你实现一个正序排序的函数。似乎不止一种方式 196 | - 请你实现一个最小堆,你需要修改那些代码呢? 197 | - 我们实现的堆排序是 inplace 的吗,如果不是,你能改成 inplace 的吗? 198 | - 堆排序的时间复杂度是多少? siftup 和 siftdown 的时间复杂度是多少?(小提示:考虑树的高度,它决定了操作次数) 199 | - 请你思考 Top K 问题的时间复杂度是多少? 200 | 201 | 202 | # 延伸阅读 203 | - 《算法导论》第 6 章 Heapsort 204 | - 《Data Structures and Algorithms in Python》 13.5 节 Heapsort 205 | - 阅读 Python heapq 模块的文档 206 | 207 | # Leetcode 208 | 209 | 合并 k 个有序链表 https://leetcode.com/problems/merge-k-sorted-lists/description/ 210 | -------------------------------------------------------------------------------- /docs/15_堆与堆排序/heap_and_heapsort.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | # 第二章拷贝的 Array 代码 4 | 5 | 6 | class Array(object): 7 | 8 | def __init__(self, size=32): 9 | self._size = size 10 | self._items = [None] * size 11 | 12 | def __getitem__(self, index): 13 | return self._items[index] 14 | 15 | def __setitem__(self, index, value): 16 | self._items[index] = value 17 | 18 | def __len__(self): 19 | return self._size 20 | 21 | def clear(self, value=None): 22 | for i in range(len(self._items)): 23 | self._items[i] = value 24 | 25 | def __iter__(self): 26 | for item in self._items: 27 | yield item 28 | 29 | ##################################################### 30 | # heap 实现 31 | ##################################################### 32 | 33 | 34 | class MaxHeap(object): 35 | """ 36 | Heaps: 37 | 完全二叉树,最大堆的非叶子节点的值都比孩子大,最小堆的非叶子结点的值都比孩子小 38 | Heap包含两个属性,order property 和 shape property(a complete binary tree),在插入 39 | 一个新节点的时候,始终要保持这两个属性 40 | 插入操作:保持堆属性和完全二叉树属性, sift-up 操作维持堆属性 41 | extract操作:只获取根节点数据,并把树最底层最右节点copy到根节点后,sift-down操作维持堆属性 42 | 43 | 用数组实现heap,从根节点开始,从上往下从左到右给每个节点编号,则根据完全二叉树的 44 | 性质,给定一个节点i, 其父亲和孩子节点的编号分别是: 45 | parent = (i-1) // 2 46 | left = 2 * i + 1 47 | rgiht = 2 * i + 2 48 | 使用数组实现堆一方面效率更高,节省树节点的内存占用,一方面还可以避免复杂的指针操作,减少 49 | 调试难度。 50 | 51 | """ 52 | 53 | def __init__(self, maxsize=None): 54 | self.maxsize = maxsize 55 | self._elements = Array(maxsize) 56 | self._count = 0 57 | 58 | def __len__(self): 59 | return self._count 60 | 61 | def add(self, value): 62 | if self._count >= self.maxsize: 63 | raise Exception('full') 64 | self._elements[self._count] = value 65 | self._count += 1 66 | self._siftup(self._count-1) # 维持堆的特性 67 | 68 | def _siftup(self, ndx): 69 | if ndx > 0: 70 | parent = int((ndx-1)/2) 71 | if self._elements[ndx] > self._elements[parent]: # 如果插入的值大于 parent,一直交换 72 | self._elements[ndx], self._elements[parent] = self._elements[parent], self._elements[ndx] 73 | self._siftup(parent) # 递归 74 | 75 | def extract(self): 76 | if self._count <= 0: 77 | raise Exception('empty') 78 | value = self._elements[0] # 保存 root 值 79 | self._count -= 1 80 | self._elements[0] = self._elements[self._count] # 最右下的节点放到root后siftDown 81 | self._siftdown(0) # 维持堆特性 82 | return value 83 | 84 | def _siftdown(self, ndx): 85 | left = 2 * ndx + 1 86 | right = 2 * ndx + 2 87 | # determine which node contains the larger value 88 | largest = ndx 89 | if (left < self._count and # 有左孩子 90 | self._elements[left] >= self._elements[largest] and 91 | self._elements[left] >= self._elements[right]): # 原书这个地方没写实际上找的未必是largest 92 | largest = left 93 | elif right < self._count and self._elements[right] >= self._elements[largest]: 94 | largest = right 95 | if largest != ndx: 96 | self._elements[ndx], self._elements[largest] = self._elements[largest], self._elements[ndx] 97 | self._siftdown(largest) 98 | 99 | 100 | def test_maxheap(): 101 | import random 102 | n = 5 103 | h = MaxHeap(n) 104 | for i in range(n): 105 | h.add(i) 106 | for i in reversed(range(n)): 107 | assert i == h.extract() 108 | 109 | 110 | def heapsort_reverse(array): 111 | length = len(array) 112 | maxheap = MaxHeap(length) 113 | for i in array: 114 | maxheap.add(i) 115 | res = [] 116 | for i in range(length): 117 | res.append(maxheap.extract()) 118 | return res 119 | 120 | 121 | def test_heapsort_reverse(): 122 | import random 123 | l = list(range(10)) 124 | random.shuffle(l) 125 | assert heapsort_reverse(l) == sorted(l, reverse=True) 126 | 127 | 128 | def heapsort_use_heapq(iterable): 129 | from heapq import heappush, heappop 130 | items = [] 131 | for value in iterable: 132 | heappush(items, value) 133 | return [heappop(items) for i in range(len(items))] 134 | 135 | 136 | def test_heapsort_use_heapq(): 137 | import random 138 | l = list(range(10)) 139 | random.shuffle(l) 140 | assert heapsort_use_heapq(l) == sorted(l) 141 | -------------------------------------------------------------------------------- /docs/15_堆与堆排序/heap_array.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PegasusWang/python_data_structures_and_algorithms/3469a79c34b6c08ae52797c3974b49fbfa8cca51/docs/15_堆与堆排序/heap_array.png -------------------------------------------------------------------------------- /docs/15_堆与堆排序/lfu.py: -------------------------------------------------------------------------------- 1 | """ 2 | https://medium.com/@epicshane/a-python-implementation-of-lfu-least-frequently-used-cache-with-o-1-time-complexity-e16b34a3c49b 3 | https://leetcode.com/problems/lfu-cache/ 4 | 5 | 这里学习下 LRU(least frequently used),就是当缓存满了之后剔除一个最少使用的 key。 6 | """ 7 | from collections import defaultdict, OrderedDict 8 | 9 | 10 | class Node: 11 | __slots__ = 'key', 'val', 'cnt' 12 | 13 | def __init__(self, key, val, cnt=0): 14 | self.key, self.val, self.cnt = key, val, cnt 15 | 16 | 17 | class LFUCache: 18 | def __init__(self, capacity): 19 | self.capacity = capacity 20 | self.cache = {} # type {key: node} 21 | self.cnt2node = defaultdict(OrderedDict) 22 | self.mincnt = 0 23 | 24 | def get(self, key, default=-1): 25 | if key not in self.cache: 26 | return default 27 | 28 | node = self.cache[key] 29 | del self.cnt2node[node.cnt][key] 30 | 31 | if not self.cnt2node[node.cnt]: 32 | del self.cnt2node[node.cnt] 33 | 34 | node.cnt += 1 35 | self.cnt2node[node.cnt][key] = node 36 | 37 | if not self.cnt2node[self.mincnt]: 38 | self.mincnt += 1 39 | return node.val 40 | 41 | def put(self, key, value): 42 | if key in self.cache: 43 | self.cache[key].val = value 44 | self.get(key) 45 | return 46 | if len(self.cache) >= self.capacity: 47 | pop_key, _pop_node = self.cnt2node[self.mincnt].popitem(last=False) 48 | del self.cache[pop_key] 49 | 50 | self.cache[key] = self.cnt2node[1][key] = Node(key, value, 1) 51 | self.mincnt = 1 52 | 53 | 54 | def test(): 55 | c = LFUCache(2) 56 | c.put(1, 1) 57 | c.put(2, 2) 58 | assert c.get(1) == 1 59 | c.put(3, 3) 60 | assert c.get(2) == -1 61 | assert c.get(3) == 3 62 | c.put(4, 4) 63 | assert c.get(1) == -1 64 | assert c.get(3) == 3 65 | assert c.get(4) == 4 66 | 67 | 68 | if __name__ == '__main__': 69 | test() 70 | -------------------------------------------------------------------------------- /docs/15_堆与堆排序/min_heap.py: -------------------------------------------------------------------------------- 1 | # python3 2 | class MinHeap: 3 | def __init__(self): 4 | """ 5 | 这里提供一个最小堆实现。如果面试不让用内置的堆非让你自己实现的话,考虑用这个简版的最小堆实现。 6 | 一般只需要实现 heqppop,heappush 两个操作就可以应付面试题了 7 | parent: (i-1)//2。注意这么写 int((n-1)/2), python3 (n-1)//2当n=0结果是-1而不是0 8 | left: 2*i+1 9 | right: 2*i+2 10 | 参考: 11 | https://favtutor.com/blogs/heap-in-python 12 | https://runestone.academy/ns/books/published/pythonds/Trees/BinaryHeapImplementation.html 13 | https://www.askpython.com/python/examples/min-heap 14 | """ 15 | self.pq = [] 16 | 17 | def min_heapify(self, nums, k): 18 | """递归调用,维持最小堆特性""" 19 | l = 2*k+1 # 左节点位置 20 | r = 2*k+2 # 右节点 21 | if l < len(nums) and nums[l] < nums[k]: 22 | smallest = l 23 | else: 24 | smallest = k 25 | if r < len(nums) and nums[r] < nums[smallest]: 26 | smallest = r 27 | if smallest != k: 28 | nums[k], nums[smallest] = nums[smallest], nums[k] 29 | self.min_heapify(nums, smallest) 30 | 31 | def heappush(self, num): 32 | """列表最后就加入一个元素,之后不断循环调用维持堆特性""" 33 | self.pq.append(num) 34 | n = len(self.pq) - 1 35 | # 注意必须加上n>0。因为 python3 (n-1)//2 当n==0 的时候结果是-1而不是0! 36 | while n > 0 and self.pq[n] < self.pq[(n-1)//2]: # parent 交换 37 | self.pq[n], self.pq[(n-1)//2] = self.pq[(n-1)//2], self.pq[n] # swap 38 | n = (n-1)//2 39 | 40 | def heqppop(self): # 取 pq[0],之后和pq最后一个元素pq[-1]交换之后调用 min_heapify(0) 41 | minval = self.pq[0] 42 | last = self.pq[-1] 43 | self.pq[0] = last 44 | self.min_heapify(self.pq, 0) 45 | self.pq.pop() 46 | return minval 47 | 48 | def heapify(self, nums): 49 | n = int((len(nums)//2)-1) 50 | for k in range(n, -1, -1): 51 | self.min_heapify(nums, k) 52 | 53 | 54 | def test_MinHeqp(): 55 | import random 56 | l = list(range(1, 9)) 57 | random.shuffle(l) 58 | pq = MinHeap() 59 | for num in l: 60 | pq.heappush(num) 61 | res = [] 62 | for _ in range(len(l)): 63 | res.append(pq.heqppop()) # 利用 heqppop,heqppush 实现堆排序 64 | 65 | def issorted(l): return all(l[i] <= l[i+1] for i in range(len(l) - 1)) 66 | assert issorted(res) 67 | -------------------------------------------------------------------------------- /docs/15_堆与堆排序/siftdown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PegasusWang/python_data_structures_and_algorithms/3469a79c34b6c08ae52797c3974b49fbfa8cca51/docs/15_堆与堆排序/siftdown.png -------------------------------------------------------------------------------- /docs/15_堆与堆排序/siftup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PegasusWang/python_data_structures_and_algorithms/3469a79c34b6c08ae52797c3974b49fbfa8cca51/docs/15_堆与堆排序/siftup.png -------------------------------------------------------------------------------- /docs/15_堆与堆排序/topk.py: -------------------------------------------------------------------------------- 1 | import heapq 2 | 3 | 4 | class TopK: 5 | """获取大量元素 topk 大个元素,固定内存 6 | 思路: 7 | 1. 先放入元素前 k 个建立一个最小堆 8 | 2. 迭代剩余元素: 9 | 如果当前元素小于堆顶元素,跳过该元素(肯定不是前 k 大) 10 | 否则替换堆顶元素为当前元素,并重新调整堆 11 | """ 12 | 13 | def __init__(self, iterable, k): 14 | self.minheap = [] 15 | self.capacity = k 16 | self.iterable = iterable 17 | 18 | def push(self, val): 19 | if len(self.minheap) >= self.capacity: 20 | min_val = self.minheap[0] 21 | if val < min_val: # 当然你可以直接 if val > min_val操作,这里我只是显示指出跳过这个元素 22 | pass 23 | else: 24 | heapq.heapreplace(self.minheap, val) # 返回并且pop堆顶最小值,推入新的 val 值并调整堆 25 | else: 26 | heapq.heappush(self.minheap, val) # 前面 k 个元素直接放入minheap 27 | 28 | def get_topk(self): 29 | for val in self.iterable: 30 | self.push(val) 31 | return self.minheap 32 | 33 | 34 | def test(): 35 | import random 36 | i = list(range(1000)) # 这里可以是一个可迭代元素,节省内存 37 | random.shuffle(i) 38 | _ = TopK(i, 10) 39 | print(_.get_topk()) # [990, 991, 992, 996, 994, 993, 997, 998, 999, 995] 40 | 41 | 42 | if __name__ == '__main__': 43 | test() 44 | -------------------------------------------------------------------------------- /docs/163_course.png: -------------------------------------------------------------------------------- 1 | ../163_course.png -------------------------------------------------------------------------------- /docs/16_优先级队列/priority_queue.md: -------------------------------------------------------------------------------- 1 | # 优先级队列 2 | 你可能比较奇怪,队列不是早就讲了嘛。这里之所以放到这里讲优先级队列,是因为虽然名字有队列, 3 | 但其实是使用堆来实现的。上一章讲完了堆,这一章我们就趁热打铁来实现一个优先级队列。 4 | 5 | 6 | # 实现优先级队列 7 | 优先级队列(Priority Queue) 顾名思义,就是入队的时候可以给一个优先级,通常是个数字或者时间戳等, 8 | 当出队的时候我们希望按照给定的优先级出队,我们按照 TDD(测试驱动开发) 的方式先来写测试代码: 9 | 10 | ```py 11 | def test_priority_queue(): 12 | size = 5 13 | pq = PriorityQueue(size) 14 | pq.push(5, 'purple') # priority, value 15 | pq.push(0, 'white') 16 | pq.push(3, 'orange') 17 | pq.push(1, 'black') 18 | 19 | res = [] 20 | while not pq.is_empty(): 21 | res.append(pq.pop()) 22 | assert res == ['purple', 'orange', 'black', 'white'] 23 | ``` 24 | 25 | 上边就是期望的行为,写完测试代码后我们来编写优先级队列的代码,按照出队的时候最大优先级先出的顺序: 26 | 27 | 28 | ```py 29 | class PriorityQueue(object): 30 | def __init__(self, maxsize): 31 | self.maxsize = maxsize 32 | self._maxheap = MaxHeap(maxsize) 33 | 34 | def push(self, priority, value): 35 | # 注意这里把这个 tuple push 进去,python 比较 tuple 从第一个开始比较 36 | # 这样就很巧妙地实现了按照优先级排序 37 | entry = (priority, value) # 入队的时候会根据 priority 维持堆的特性 38 | self._maxheap.add(entry) 39 | 40 | def pop(self, with_priority=False): 41 | entry = self._maxheap.extract() 42 | if with_priority: 43 | return entry 44 | else: 45 | return entry[1] 46 | 47 | def is_empty(self): 48 | return len(self._maxheap) == 0 49 | ``` 50 | 51 | 52 | # 练习题 53 | - 请你实现按照小优先级先出队的顺序的优先级队列 54 | -------------------------------------------------------------------------------- /docs/16_优先级队列/priority_queue.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | # 第二章拷贝的 Array 代码 4 | 5 | 6 | class Array(object): 7 | 8 | def __init__(self, size=32): 9 | self._size = size 10 | self._items = [None] * size 11 | 12 | def __getitem__(self, index): 13 | return self._items[index] 14 | 15 | def __setitem__(self, index, value): 16 | self._items[index] = value 17 | 18 | def __len__(self): 19 | return self._size 20 | 21 | def clear(self, value=None): 22 | for i in range(len(self._items)): 23 | self._items[i] = value 24 | 25 | def __iter__(self): 26 | for item in self._items: 27 | yield item 28 | 29 | ##################################################### 30 | # heap 实现 31 | ##################################################### 32 | 33 | 34 | class MaxHeap(object): 35 | """ 36 | Heaps: 37 | 完全二叉树,最大堆的非叶子节点的值都比孩子大,最小堆的非叶子结点的值都比孩子小 38 | Heap包含两个属性,order property 和 shape property(a complete binary tree),在插入 39 | 一个新节点的时候,始终要保持这两个属性 40 | 插入操作:保持堆属性和完全二叉树属性, sift-up 操作维持堆属性 41 | extract操作:只获取根节点数据,并把树最底层最右节点copy到根节点后,sift-down操作维持堆属性 42 | 43 | 用数组实现heap,从根节点开始,从上往下从左到右给每个节点编号,则根据完全二叉树的 44 | 性质,给定一个节点i, 其父亲和孩子节点的编号分别是: 45 | parent = (i-1) // 2 46 | left = 2 * i + 1 47 | rgiht = 2 * i + 2 48 | 使用数组实现堆一方面效率更高,节省树节点的内存占用,一方面还可以避免复杂的指针操作,减少 49 | 调试难度。 50 | 51 | """ 52 | 53 | def __init__(self, maxsize=None): 54 | self.maxsize = maxsize 55 | self._elements = Array(maxsize) 56 | self._count = 0 57 | 58 | def __len__(self): 59 | return self._count 60 | 61 | def add(self, value): 62 | if self._count >= self.maxsize: 63 | raise Exception('full') 64 | self._elements[self._count] = value 65 | self._count += 1 66 | self._siftup(self._count-1) # 维持堆的特性 67 | 68 | def _siftup(self, ndx): 69 | if ndx > 0: 70 | parent = int((ndx-1)/2) 71 | if self._elements[ndx] > self._elements[parent]: # 如果插入的值大于 parent,一直交换 72 | self._elements[ndx], self._elements[parent] = self._elements[parent], self._elements[ndx] 73 | self._siftup(parent) # 递归 74 | 75 | def extract(self): 76 | if self._count <= 0: 77 | raise Exception('empty') 78 | value = self._elements[0] # 保存 root 值 79 | self._count -= 1 80 | self._elements[0] = self._elements[self._count] # 最右下的节点放到root后siftDown 81 | self._siftdown(0) # 维持堆特性 82 | return value 83 | 84 | def _siftdown(self, ndx): 85 | left = 2 * ndx + 1 86 | right = 2 * ndx + 2 87 | # determine which node contains the larger value 88 | largest = ndx 89 | if (left < self._count and # 有左孩子 90 | self._elements[left] >= self._elements[largest] and 91 | self._elements[left] >= self._elements[right]): # 原书这个地方没写实际上找的未必是largest 92 | largest = left 93 | elif right < self._count and self._elements[right] >= self._elements[largest]: 94 | largest = right 95 | if largest != ndx: 96 | self._elements[ndx], self._elements[largest] = self._elements[largest], self._elements[ndx] 97 | self._siftdown(largest) 98 | 99 | 100 | class PriorityQueue(object): 101 | def __init__(self, maxsize): 102 | self.maxsize = maxsize 103 | self._maxheap = MaxHeap(maxsize) 104 | 105 | def push(self, priority, value): 106 | entry = (priority, value) # 注意这里把这个 tuple push进去,python 比较 tuple 从第一个开始比较 107 | self._maxheap.add(entry) 108 | 109 | def pop(self, with_priority=False): 110 | entry = self._maxheap.extract() 111 | if with_priority: 112 | return entry 113 | else: 114 | return entry[1] 115 | 116 | def is_empty(self): 117 | return len(self._maxheap) == 0 118 | 119 | 120 | def test_priority_queue(): 121 | size = 5 122 | pq = PriorityQueue(size) 123 | pq.push(5, 'purple') 124 | pq.push(0, 'white') 125 | pq.push(3, 'orange') 126 | pq.push(1, 'black') 127 | 128 | res = [] 129 | while not pq.is_empty(): 130 | res.append(pq.pop()) 131 | assert res == ['purple', 'orange', 'black', 'white'] 132 | 133 | 134 | def test_buildin_PriorityQueue(): # python3 135 | """ 136 | 测试内置的 PriorityQueue 137 | https://pythonguides.com/priority-queue-in-python/ 138 | """ 139 | from queue import PriorityQueue 140 | q = PriorityQueue() 141 | q.put((10, 'Red balls')) 142 | q.put((8, 'Pink balls')) 143 | q.put((5, 'White balls')) 144 | q.put((4, 'Green balls')) 145 | while not q.empty(): 146 | item = q.get() 147 | print(item) 148 | 149 | 150 | def test_buildin_heapq_as_PriorityQueue(): 151 | """ 152 | 测试使用 heapq 实现优先级队列,保存一个 tuple 比较元素(tuple第一个元素是优先级) 153 | """ 154 | import heapq 155 | s_roll = [] 156 | heapq.heappush(s_roll, (4, "Tom")) 157 | heapq.heappush(s_roll, (1, "Aruhi")) 158 | heapq.heappush(s_roll, (3, "Dyson")) 159 | heapq.heappush(s_roll, (2, "Bob")) 160 | while s_roll: 161 | deque_r = heapq.heappop(s_roll) 162 | print(deque_r) 163 | 164 | 165 | # python3 没有了 __cmp__ 魔法函数 https://stackoverflow.com/questions/8276983/why-cant-i-use-the-method-cmp-in-python-3-as-for-python-2 166 | class Item: 167 | def __init__(self, key, weight): 168 | self.key, self.weight = key, weight 169 | 170 | def __lt__(self, other): # 看其来 heapq 实现只用了 小于 比较,这里定义了就可以 push 一个 item 类 171 | return self.weight < other.weight 172 | 173 | def __eq__(self, other): 174 | return self.weight == other.weight 175 | 176 | def __str__(self): 177 | return '{}:{}'.format(self.key,self.weight) 178 | 179 | 180 | def test_heap_item(): 181 | """ 182 | 测试使用 Item 类实现优先级队列,因为 heapq 内置使用的是小于运算法, 183 | 重写魔术 < 比较方法即可实现 184 | """ 185 | import heapq 186 | pq = [] 187 | heapq.heappush(pq, Item('c', 3)) 188 | heapq.heappush(pq, Item('a', 1)) 189 | heapq.heappush(pq, Item('b', 2)) 190 | while pq: 191 | print(heapq.heappop(pq)) 192 | -------------------------------------------------------------------------------- /docs/17_二叉查找树/bst.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PegasusWang/python_data_structures_and_algorithms/3469a79c34b6c08ae52797c3974b49fbfa8cca51/docs/17_二叉查找树/bst.png -------------------------------------------------------------------------------- /docs/17_二叉查找树/bst.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | class BSTNode(object): 5 | def __init__(self, key, value, left=None, right=None): 6 | self.key, self.value, self.left, self.right = key, value, left, right 7 | 8 | 9 | class BST(object): 10 | def __init__(self, root=None): 11 | self.root = root 12 | 13 | @classmethod 14 | def build_from(cls, node_list): 15 | cls.size = 0 16 | key_to_node_dict = {} 17 | for node_dict in node_list: 18 | key = node_dict['key'] 19 | key_to_node_dict[key] = BSTNode(key, value=key) # 这里值暂时用 和 key一样的 20 | 21 | for node_dict in node_list: 22 | key = node_dict['key'] 23 | node = key_to_node_dict[key] 24 | if node_dict['is_root']: 25 | root = node 26 | node.left = key_to_node_dict.get(node_dict['left']) 27 | node.right = key_to_node_dict.get(node_dict['right']) 28 | cls.size += 1 29 | return cls(root) 30 | 31 | def _bst_search(self, subtree, key): 32 | if subtree is None: # 没找到 33 | return None 34 | elif key < subtree.key: 35 | return self._bst_search(subtree.left, key) 36 | elif key > subtree.key: 37 | return self._bst_search(subtree.right, key) 38 | else: 39 | return subtree 40 | 41 | def __contains__(self, key): 42 | """实现 in 操作符""" 43 | return self._bst_search(self.root, key) is not None 44 | 45 | def get(self, key, default=None): 46 | node = self._bst_search(self.root, key) 47 | if node is None: 48 | return default 49 | else: 50 | return node.value 51 | 52 | def _bst_min_node(self, subtree): 53 | if subtree is None: 54 | return None 55 | elif subtree.left is None: # 找到左子树的头 56 | return subtree 57 | else: 58 | return self._bst_min_node(subtree.left) 59 | 60 | def bst_min(self): 61 | node = self._bst_min_node(self.root) 62 | return node.value if node else None 63 | 64 | def _bst_insert(self, subtree, key, value): 65 | """ 插入并且返回根节点 66 | 67 | :param subtree: 68 | :param key: 69 | :param value: 70 | """ 71 | if subtree is None: # 插入的节点一定是根节点,包括 root 为空的情况 72 | subtree = BSTNode(key, value) 73 | elif key < subtree.key: 74 | subtree.left = self._bst_insert(subtree.left, key, value) 75 | elif key > subtree.key: 76 | subtree.right = self._bst_insert(subtree.right, key, value) 77 | return subtree 78 | 79 | def add(self, key, value): 80 | node = self._bst_search(self.root, key) 81 | if node is not None: # 更新已经存在的 key 82 | node.value = value 83 | return False 84 | else: 85 | self.root = self._bst_insert(self.root, key, value) 86 | self.size += 1 87 | return True 88 | 89 | def _bst_remove(self, subtree, key): 90 | """删除节点并返回根节点""" 91 | if subtree is None: 92 | return None 93 | elif key < subtree.key: 94 | subtree.left = self._bst_remove(subtree.left, key) 95 | return subtree 96 | elif key > subtree.key: 97 | subtree.right = self._bst_remove(subtree.right, key) 98 | return subtree 99 | else: # 找到了需要删除的节点 100 | if subtree.left is None and subtree.right is None: # 叶节点,返回 None 把其父亲指向它的指针置为 None 101 | return None 102 | elif subtree.left is None or subtree.right is None: # 只有一个孩子 103 | if subtree.left is not None: 104 | return subtree.left # 返回它的孩子并让它的父亲指过去 105 | else: 106 | return subtree.right 107 | else: # 俩孩子,寻找后继节点替换,并删除其右子树的后继节点,同时更新其右子树 108 | successor_node = self._bst_min_node(subtree.right) 109 | subtree.key, subtree.value = successor_node.key, successor_node.value 110 | subtree.right = self._bst_remove(subtree.right, successor_node.key) 111 | return subtree 112 | 113 | def remove(self, key): 114 | assert key in self 115 | self.size -= 1 116 | return self._bst_remove(self.root, key) 117 | 118 | 119 | NODE_LIST = [ 120 | {'key': 60, 'left': 12, 'right': 90, 'is_root': True}, 121 | {'key': 12, 'left': 4, 'right': 41, 'is_root': False}, 122 | {'key': 4, 'left': 1, 'right': None, 'is_root': False}, 123 | {'key': 1, 'left': None, 'right': None, 'is_root': False}, 124 | {'key': 41, 'left': 29, 'right': None, 'is_root': False}, 125 | {'key': 29, 'left': 23, 'right': 37, 'is_root': False}, 126 | {'key': 23, 'left': None, 'right': None, 'is_root': False}, 127 | {'key': 37, 'left': None, 'right': None, 'is_root': False}, 128 | {'key': 90, 'left': 71, 'right': 100, 'is_root': False}, 129 | {'key': 71, 'left': None, 'right': 84, 'is_root': False}, 130 | {'key': 100, 'left': None, 'right': None, 'is_root': False}, 131 | {'key': 84, 'left': None, 'right': None, 'is_root': False}, 132 | ] 133 | 134 | 135 | def test_bst_tree(): 136 | bst = BST.build_from(NODE_LIST) 137 | for node_dict in NODE_LIST: 138 | key = node_dict['key'] 139 | assert bst.get(key) == key 140 | assert bst.size == len(NODE_LIST) 141 | assert bst.get(-1) is None # 单例的 None 我们用 is 来比较 142 | 143 | assert bst.bst_min() == 1 144 | 145 | bst.add(0, 0) 146 | assert bst.bst_min() == 0 147 | 148 | bst.remove(12) 149 | assert bst.get(12) is None 150 | 151 | bst.remove(1) 152 | assert bst.get(1) is None 153 | 154 | bst.remove(29) 155 | assert bst.get(29) is None 156 | -------------------------------------------------------------------------------- /docs/17_二叉查找树/bst_insert.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PegasusWang/python_data_structures_and_algorithms/3469a79c34b6c08ae52797c3974b49fbfa8cca51/docs/17_二叉查找树/bst_insert.png -------------------------------------------------------------------------------- /docs/17_二叉查找树/bst_remove_leaf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PegasusWang/python_data_structures_and_algorithms/3469a79c34b6c08ae52797c3974b49fbfa8cca51/docs/17_二叉查找树/bst_remove_leaf.png -------------------------------------------------------------------------------- /docs/17_二叉查找树/bst_remove_node_with_one_child.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PegasusWang/python_data_structures_and_algorithms/3469a79c34b6c08ae52797c3974b49fbfa8cca51/docs/17_二叉查找树/bst_remove_node_with_one_child.png -------------------------------------------------------------------------------- /docs/17_二叉查找树/bst_search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PegasusWang/python_data_structures_and_algorithms/3469a79c34b6c08ae52797c3974b49fbfa8cca51/docs/17_二叉查找树/bst_search.png -------------------------------------------------------------------------------- /docs/17_二叉查找树/bst_worstcase.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PegasusWang/python_data_structures_and_algorithms/3469a79c34b6c08ae52797c3974b49fbfa8cca51/docs/17_二叉查找树/bst_worstcase.png -------------------------------------------------------------------------------- /docs/17_二叉查找树/find_successor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PegasusWang/python_data_structures_and_algorithms/3469a79c34b6c08ae52797c3974b49fbfa8cca51/docs/17_二叉查找树/find_successor.png -------------------------------------------------------------------------------- /docs/17_二叉查找树/predecessor_successor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PegasusWang/python_data_structures_and_algorithms/3469a79c34b6c08ae52797c3974b49fbfa8cca51/docs/17_二叉查找树/predecessor_successor.png -------------------------------------------------------------------------------- /docs/17_二叉查找树/remove_interior_replace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PegasusWang/python_data_structures_and_algorithms/3469a79c34b6c08ae52797c3974b49fbfa8cca51/docs/17_二叉查找树/remove_interior_replace.png -------------------------------------------------------------------------------- /docs/18_图与图的遍历/bfs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PegasusWang/python_data_structures_and_algorithms/3469a79c34b6c08ae52797c3974b49fbfa8cca51/docs/18_图与图的遍历/bfs.png -------------------------------------------------------------------------------- /docs/18_图与图的遍历/graph.md: -------------------------------------------------------------------------------- 1 | # 图 2 | 之前们讲过很多数据结构了,包括线性结构、链式结构、树结构等,这些结构基本就能应付我们的业务开发了。 3 | 这一章来看下图结构,图的使用也比较广泛,比如人物关系、路径选择等等,相比前面的一些数据结构和算法要相对复杂一些。 4 | 不过也不用担心,除非是特定的后端业务,一般图结构的使用比较少。这一章我们简单地介绍下图结构,以及图的搜索算法。 5 | 6 | # 什么是图? 7 | 我们先来考虑日常生活中的一个问题,我们在出行的时候一般会考虑使用地图软件搜下从一个地点到另外一个地点的路线。 8 | 这里把地点抽象成一个圈,路径抽象成线,于是乎就有了下面的图,其实还是非常好理解的。 9 | 10 | ![](./graph_road.png) 11 | 12 | 简单地说就是有节点(node)和边(edge)组成的一种数据结构,相邻的节点称之为邻居。 注意图分为有向图和无向图, 13 | 比如有些路是单行道,有些是双行道,有向图我们用箭头指向,无向图就是一条直线连接。 14 | 15 | # 图的表示 16 | 那我们怎么把一个图抽象成代码来表示呢?因为最终我们还是需要代码来实现的。通常有两种表示方法,邻接表法和邻接矩阵表示。 17 | 18 | ![](./graph_rep.png) 19 | 20 | - 邻接表法:对于每个图中的点,将它的邻居放到一个链表里 21 | - 邻接矩阵:对于 n 个点,构造一个 n * n 的矩阵,如果有从点 i 到点 j 的边,就将矩阵的位置 matrix[i][j] 置为 1. 22 | 23 | 不过我们可以看到,用矩阵存储图是非常耗费空间的,大部分情况下矩阵是稀疏的,所以我们后边选择使用邻接表。 24 | 25 | # 图的遍历 26 | 遍历图最常用的有两种方式,就是你常听到的 BFS 和 DFS. 27 | 28 | - BFS: Breadth First Search,广度优先搜索 29 | - DFS: Depdth First Search,深度优先搜索 30 | 31 | ### BFS 32 | BFS 类似于树的层序遍历,从第一个节点开始,先访问离 A 最近的点,接着访问次近的点。我们先来构造一个图: 33 | 34 | ```py 35 | graph = { 36 | 'A': ['B', 'F'], 37 | 'B': ['C', 'I', 'G'], 38 | 'C': ['B', 'I', 'D'], 39 | 'D': ['C', 'I', 'G', 'H', 'E'], 40 | 'E': ['D', 'H', 'F'], 41 | 'F': ['A', 'G', 'E'], 42 | 'G': ['B', 'F', 'H', 'D'], 43 | 'H': ['G', 'D', 'E'], 44 | 'I': ['B', 'C', 'D'], 45 | } 46 | ``` 47 | 如何『由近及远』地访问节点呢?我们先访问起点 A 的邻居,然后邻居访问完再访问邻居的邻居不就行了? 48 | 就是这个思想,不过我们需要一个队列辅助,队列之前说过是一种先进先出结构,我们只需要把起点的邻居先入队, 49 | 当邻居访问完了再去访问邻居的邻居就可以了,对于已经访问过的节点,我们用一个 set 记录它就好了。代码如下: 50 | 51 | ```py 52 | # -*- coding: utf-8 -*- 53 | 54 | from collections import deque 55 | 56 | 57 | GRAPH = { 58 | 'A': ['B', 'F'], 59 | 'B': ['C', 'I', 'G'], 60 | 'C': ['B', 'I', 'D'], 61 | 'D': ['C', 'I', 'G', 'H', 'E'], 62 | 'E': ['D', 'H', 'F'], 63 | 'F': ['A', 'G', 'E'], 64 | 'G': ['B', 'F', 'H', 'D'], 65 | 'H': ['G', 'D', 'E'], 66 | 'I': ['B', 'C', 'D'], 67 | } 68 | 69 | 70 | class Queue(object): 71 | def __init__(self): 72 | self._deque = deque() 73 | 74 | def push(self, value): 75 | return self._deque.append(value) 76 | 77 | def pop(self): 78 | return self._deque.popleft() 79 | 80 | def __len__(self): 81 | return len(self._deque) 82 | 83 | 84 | def bfs(graph, start): 85 | search_queue = Queue() 86 | search_queue.push(start) 87 | searched = set() 88 | while search_queue: # 队列不为空就继续 89 | cur_node = search_queue.pop() 90 | if cur_node not in searched: 91 | yield cur_node 92 | searched.add(cur_node) 93 | for node in graph[cur_node]: 94 | search_queue.push(node) 95 | 96 | print('bfs:') 97 | bfs(GRAPH, 'A') 98 | """ 99 | bfs: 100 | A 101 | B 102 | F 103 | C 104 | I 105 | G 106 | E 107 | D 108 | H 109 | """ 110 | ``` 111 | 112 | ![](./bfs.png) 113 | 114 | ### DFS 115 | 深度优先搜索(DFS)是每遇到一个节点,如果没有被访问过,就直接去访问它的邻居节点,不断加深。代码其实很简单: 116 | 117 | ``` 118 | DFS_SEARCHED = set() 119 | 120 | 121 | def dfs(graph, start): 122 | if start not in DFS_SEARCHED: 123 | print(start) 124 | DFS_SEARCHED.add(start) 125 | for node in graph[start]: 126 | if node not in DFS_SEARCHED: 127 | dfs(graph, node) 128 | 129 | 130 | print('dfs:') 131 | dfs(GRAPH, 'A') # A B C I D G F E H 132 | 133 | ``` 134 | 135 | 136 | # 思考题 137 | - DFS 中我们使用到了递归,请你用栈来改写这个函数?(代码里有答案,我建议你先尝试自己实现) 138 | 139 | # 延伸阅读 140 | 图的算法还有很多,这里就不一一列举了,感兴趣的读者可以继续阅读一下材料。 141 | 142 | - [数据结构之图](https://www.zybuluo.com/guoxs/note/249812) 143 | - 《算法图解》第六章 144 | -------------------------------------------------------------------------------- /docs/18_图与图的遍历/graph.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from collections import deque 4 | 5 | 6 | GRAPH = { 7 | 'A': ['B', 'F'], 8 | 'B': ['C', 'I', 'G'], 9 | 'C': ['B', 'I', 'D'], 10 | 'D': ['C', 'I', 'G', 'H', 'E'], 11 | 'E': ['D', 'H', 'F'], 12 | 'F': ['A', 'G', 'E'], 13 | 'G': ['B', 'F', 'H', 'D'], 14 | 'H': ['G', 'D', 'E'], 15 | 'I': ['B', 'C', 'D'], 16 | } 17 | 18 | 19 | class Queue(object): 20 | def __init__(self): 21 | self._deque = deque() 22 | 23 | def push(self, value): 24 | return self._deque.append(value) 25 | 26 | def pop(self): 27 | return self._deque.popleft() 28 | 29 | def __len__(self): 30 | return len(self._deque) 31 | 32 | 33 | def bfs(graph, start): 34 | search_queue = Queue() 35 | search_queue.push(start) 36 | searched = set() 37 | while search_queue: # 队列不为空就继续 38 | cur_node = search_queue.pop() 39 | if cur_node not in searched: 40 | print(cur_node) 41 | searched.add(cur_node) 42 | for node in graph[cur_node]: 43 | search_queue.push(node) 44 | 45 | 46 | print('bfs:') 47 | bfs(GRAPH, 'A') 48 | 49 | 50 | DFS_SEARCHED = set() 51 | 52 | 53 | def dfs(graph, start): 54 | if start not in DFS_SEARCHED: 55 | print(start) 56 | DFS_SEARCHED.add(start) 57 | for node in graph[start]: 58 | if node not in DFS_SEARCHED: 59 | dfs(graph, node) 60 | 61 | 62 | print('dfs:') 63 | dfs(GRAPH, 'A') 64 | 65 | 66 | class Stack(object): 67 | def __init__(self): 68 | self._deque = deque() 69 | 70 | def push(self, value): 71 | return self._deque.append(value) 72 | 73 | def pop(self): 74 | return self._deque.pop() 75 | 76 | def __len__(self): 77 | return len(self._deque) 78 | 79 | 80 | def dfs_use_stack(graph, start): 81 | stack = Stack() 82 | stack.push(start) 83 | searched = set() 84 | while stack: 85 | cur_node = stack.pop() 86 | if cur_node not in searched: 87 | print(cur_node) 88 | searched.add(cur_node) 89 | # 请读者思考这里我为啥加了 reversed,其实不加上是可以的,你看下代码输出 90 | for node in reversed(graph[cur_node]): 91 | stack.push(node) 92 | 93 | 94 | print('dfs_use_stack:') 95 | dfs_use_stack(GRAPH, 'A') 96 | -------------------------------------------------------------------------------- /docs/18_图与图的遍历/graph_rep.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PegasusWang/python_data_structures_and_algorithms/3469a79c34b6c08ae52797c3974b49fbfa8cca51/docs/18_图与图的遍历/graph_rep.png -------------------------------------------------------------------------------- /docs/18_图与图的遍历/graph_road.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PegasusWang/python_data_structures_and_algorithms/3469a79c34b6c08ae52797c3974b49fbfa8cca51/docs/18_图与图的遍历/graph_road.png -------------------------------------------------------------------------------- /docs/20_面试指南/interview.md: -------------------------------------------------------------------------------- 1 | # 注意事项 2 | 3 | - 电子简历尽量用 pdf 格式,方便跨平台打开。doc 等格式在不同的电脑上打开会有排版问题,很多后端技术面试官可能使用的是 mac 或者 linux。 4 | - 提前复习回顾重点知识,防止卡在基础上。比如 mac 下著名的 brew 工具作者面试 google 就因为没写出来反转二叉树被拒,后来去了苹果😂.(这就只能看人品和运气和眼缘了,如果没见到二面面试官或者 hr,大概率是挂了)。(树、链表、哈希表、二分、快排、TCP/UDP、HTTP、数据库ACID、索引优化等常考点)。 5 | - 白板编程,练习在纸上手写代码。虽然很多求职者都很抵触手写代码,但是白板编程确实是一种比较好的区分方式。你的思考过程、编码习惯、编码规范等都能看出来。 6 | - 如果被问到工程里不会使用但是比较刁钻的算法题,建议你和面试官沟通的时候问问这个算法或者题目在开发中有哪些实际使用场景,看看对方怎么说😎。少数公司喜欢考一些算法竞赛题,这种对于没有ACM,信息学竞赛背景的开发者来说比较吃力。大部分业务开发岗位应该只会考察基础算法题 7 | - 面试的时候准备充分,简历要与招聘方需求对等,甚至可以针对不同公司准备不同的简历内容。笔者每次面试都会带上白纸、笔、简历、电脑等,即使面试没过,至少也让面试官感觉我是有诚意的,给对方留下好印象。 8 | - 加分项:github、个人技术博客、开源项目、技术论坛帐号等,让面试官有更多渠道了解你,有时候仅仅根据几十分钟的面试来评判面试者是有失偏颇的。(比如面试者临场发挥不好;面试官个人偏好;会的都不问,问的都不会等) 9 | 10 | 11 | # 白板编程 12 | 其实我个人是反对出纯算法题目的,尤其是有些比较刁钻的直接出算法竞赛题,这对与很多做工程的同学来说是比较吃亏的。没事的时候可以去 LeetCode 之类的网站刷刷基础题。 13 | 一般来说 web 业务开发者掌握常见的编程语言内置算法和数据结构就够用了。 14 | 15 | - 练习手写常见的算法,比如快排,二分,归并等,记住常见排序算法时间复杂度 16 | - 逻辑正确是前提 17 | - 有图示描述思路最好,如果时间紧代码没写出来,可以直接描述自己的思路。 18 | - 字不要写太大,尽量工整。每行代码之间留有一定的空隙,方便你修改(甚至笔者之前会带上铅笔和橡皮手写代码) 19 | - 如果实在写不出来可以和面试官交流,很多时候如果给不出最优方案尽量想一个次优方案,别上来就说不会 20 | - 想不起来的函数名写伪代码,一般面试官不会强制说让你记住每个 api 的名字 21 | - 如果有多余的时间(一般不会有)注意一些边界条件,防御性编程、代码风格、单元测试等东西,想好一些异常情况(空值、边界值等)的测试用例 22 | 23 | # 手写代码注意事项 24 | 25 | 这里我就直接引用《剑指offer》里内容,大家写代码的时候可以多加注意,对于应对算法面试,如果准备时间比较多,推荐看下这本书,并且刷一下 26 | leetcode 上的基础题目练练手感。 27 | 28 | - 规范性:书写清晰、布局清晰、命令合理 29 | - 完整性:完成基本功能,考虑边界条件,做好错误处理 30 | - 鲁棒性:防御式编程,处理无效输入 31 | 32 | 33 | # 结语 34 | 这套教程列举的算法很有限,包括图算法、贪心,动态规划,分布式,机器学习算法等很多没有涉及到,因为它们确实需要读者更深入的理论基础,而且这套教程的目的也不是针对算法竞赛。 35 | 不过了解了本教程涉及到的大部分算法是可以应付绝大多数的业务开发的。如果读者对算法有兴趣,本教程引用的几本参考书都可以去深入学习。希望本教程能对你学习算法、养成良好的思维方式和编码习惯等有所帮助。 36 | 37 | # 延伸阅读 38 | 目前市面上有一些专门针对算法面试的书供大家参考,如果你正在准备算法面试,我强烈建议你看看下面的参考资料学习解题技巧: 39 | 40 | - [那些年,我们一起跪过的算法题[视频]](https://zhuanlan.zhihu.com/p/35175401) 41 | - [《程序员面试金典(第5版)》](https://book.douban.com/subject/25753386/) 42 | - [《剑指Offer》](https://book.douban.com/subject/25910559/) 43 | - [python leetCode](https://github.com/HuberTRoy/leetCodek) 44 | 45 | # 刷题网站 46 | leetcode 和牛客网是国内常用的两个刷题网站,笔者建议刷一下高频的 200 道题左右,基本可以应付大部分公司的算法面试了。 47 | 48 | - https://leetcode-cn.com/ 49 | - https://www.nowcoder.com/exam/oj 50 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Python 数据结构与算法视频教程 2 | theme: readthedocs 3 | extra_javascript: 4 | - https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.0/MathJax.js?config=TeX-AMS-MML_HTMLorMML 5 | 6 | markdown_extensions: 7 | - mdx_math: 8 | enable_dollar_delimiter: True #for use of inline $..$ 9 | 10 | pages: 11 | - 课程简介: 'index.md' 12 | - 00_课程简介之笨方法学算法: '00_课程简介之笨方法学算法/why_and_how_to_learn.md' 13 | - 01_抽象数据类型和面向对象编程: '01_抽象数据类型和面向对象编程/ADT_OOP.md' 14 | - 02_数组和列表: '02_数组和列表/array_and_list.md' 15 | - 03_链表: '03_链表/linked_list.md' 16 | - 04_队列: '04_队列/queue.md' 17 | - 05_栈: '05_栈/stack.md' 18 | - 06_算法分析: '06_算法分析/big_o.md' 19 | - 07_哈希表: '07_哈希表/hashtable.md' 20 | - 08_字典: '08_字典/dict.md' 21 | - 09_集合: '09_集合/set.md' 22 | - 10_递归: '10_递归/recursion.md' 23 | - 11_线性查找与二分查找: '11_线性查找与二分查找/search.md' 24 | - 12_基本排序算法: '12_基本排序算法/basic_sort.md' 25 | - 13_高级排序算法: 26 | - 高级排序算法: '13_高级排序算法/advanced_sorting.md' 27 | - 分治法与归并排序: '13_高级排序算法/merge_sort.md' 28 | - 快速排序: '13_高级排序算法/quick_sort.md' 29 | - 14_树与二叉树: '14_树与二叉树/tree.md' 30 | - 15_堆和堆排序: '15_堆与堆排序/heap_and_heapsort.md' 31 | - 16_优先级队列: '16_优先级队列/priority_queue.md' 32 | - 17_二叉查找树: '17_二叉查找树/binary_search_tree.md' 33 | - 18_图与图的遍历: '18_图与图的遍历/graph.md' 34 | - 19_python内置常用算法和数据结构: '19_python内置常用算法和数据结构/builtins.md' 35 | - 20_面试指南: '20_面试指南/interview.md' 36 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | python-markdown-math 2 | mkdocs 3 | -------------------------------------------------------------------------------- /剑指offer/03_FindInPartiallySortedMatrix(二维数组中的查找).py: -------------------------------------------------------------------------------- 1 | """ 2 | 剑指offer 第三题。从左到右升序从上到下升序数组,判断是否能找到一个值 3 | 思路: 4 | 从右上角开始找,大于 target 排除当前列。小于 target 排除当前行 5 | """ 6 | 7 | 8 | class Solution: 9 | def solve(self, matrix, target): 10 | if not matrix or not matrix[0]: 11 | return False 12 | leny = len(matrix) 13 | 14 | x = 0 15 | y = leny - 1 16 | 17 | while x >= 0 and y >= 0: 18 | if matrix[x][y] == target: 19 | return True 20 | elif matrix[x][y] > target: 21 | y -= 1 22 | else: 23 | x += 1 24 | return False 25 | 26 | 27 | def test(): 28 | s = Solution() 29 | matrix = [ 30 | [1, 2, 8, 9], 31 | [2, 4, 9, 12], 32 | [4, 7, 10, 13], 33 | [6, 8, 11, 15], 34 | ] 35 | assert s.solve(matrix, 0) is False 36 | assert s.solve(matrix, 1) is True 37 | assert s.solve(matrix, 7) is True 38 | assert s.solve(matrix, 5) is False 39 | 40 | # empty 41 | matrix = [ 42 | [], 43 | ] 44 | assert s.solve(matrix, 0) is False 45 | 46 | 47 | if __name__ == '__main__': 48 | test() 49 | -------------------------------------------------------------------------------- /剑指offer/04_ReplaceBlank(替换空格).py: -------------------------------------------------------------------------------- 1 | """ 2 | 替换字符串中的空格 3 | 题目:请实现一个函数,把字符串中的每个空格替换成"%20"。例如输入“We are happy.”,则输出“We%20are%20happy.” 4 | """ 5 | 6 | 7 | class Solution: 8 | def solve(self, string): 9 | """因为 python string 不可变对象,和其他的语言用字符串数组能直接修改有点区别""" 10 | res = [] 11 | for char in string: 12 | if char == ' ': 13 | res.append('%20') 14 | else: 15 | res.append(char) 16 | return ''.join(res) 17 | 18 | def solve2(self, string): 19 | """ 20 | 思路: 21 | 先遍历一次计算替换后的总长度 22 | 从后往前替换,防止从前往后需要 23 | """ 24 | pass 25 | 26 | 27 | def test(): 28 | s = Solution() 29 | ss = 'We are happy.' 30 | assert s.solve(ss) == 'We%20are%20happy.' 31 | 32 | assert s.solve('') == '' 33 | 34 | 35 | if __name__ == '__main__': 36 | test() 37 | -------------------------------------------------------------------------------- /剑指offer/05_PrintListInReversedOrder(从尾到头打印链表).py: -------------------------------------------------------------------------------- 1 | """ 2 | 面试题5:从尾到头打印链表 3 | 题目:输入一个链表的头结点,从尾到头反过来打印出每个结点的值。链表结点定义如下: 4 | """ 5 | from collections import deque 6 | 7 | 8 | class Stack: 9 | def __init__(self): 10 | self.items = deque() 11 | 12 | def push(self, val): 13 | return self.items.append(val) 14 | 15 | def pop(self): 16 | return self.items.pop() 17 | 18 | def empty(self): 19 | return len(self.items) == 0 20 | 21 | 22 | class Node: 23 | def __init__(self, val, next=None): 24 | self.val, self.next = val, next 25 | 26 | 27 | class Solution: 28 | def solve(self, headnode): 29 | """ 30 | 思路:用一个栈保存所有节点,之后一个一个 pop 31 | """ 32 | val_s = Stack() 33 | cur_node = headnode 34 | while cur_node: 35 | val_s.push(cur_node.val) 36 | cur_node = cur_node.next 37 | while not val_s.empty(): 38 | print(val_s.pop()) 39 | 40 | def solve2(self, headnode): 41 | """ 42 | 能用栈就可以使用递归。这一点需要能联想到 43 | """ 44 | curnode = headnode 45 | if curnode: 46 | self.solve2(curnode.next) 47 | print(curnode.val) # 注意 print 放到 递归之后才是倒序 48 | 49 | 50 | def test(): 51 | s = Solution() 52 | linklist = Node(0, Node(1)) 53 | s.solve2(linklist) 54 | 55 | # linklist = Node(0) 56 | # s.solve2(linklist) 57 | 58 | if __name__ == '__main__': 59 | test() 60 | -------------------------------------------------------------------------------- /剑指offer/06_ConstructBinaryTree(重建二叉树).py: -------------------------------------------------------------------------------- 1 | """ 2 | 面试题6:重建二叉树 3 | 题目:输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。 4 | 例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建出图2.6所示的二叉树并输出它的头结点。二叉树结点的定义如下: 5 | """ 6 | 7 | 8 | class Node: 9 | def __init__(self, val, left=None, right=None): 10 | self.val, self.left, self.right = val, left, right 11 | 12 | 13 | class Solution: 14 | def __init__(self): 15 | self.pres = [] 16 | self.inorders = [] 17 | 18 | def solve(self, prevals, invals): 19 | """ 20 | 思路:先序找到根,然后可以找到中序遍历根的位置确定左子树和右子树,递归处理 21 | """ 22 | if not prevals or not invals: 23 | return None 24 | root_val = prevals[0] 25 | root = Node(root_val) 26 | inorder_root_idx = invals.index(root_val) 27 | left_length = inorder_root_idx 28 | right_length = len(invals) - inorder_root_idx - 1 29 | 30 | if left_length: 31 | root.left = self.solve(prevals[1:1 + left_length], invals[:inorder_root_idx]) 32 | 33 | if right_length: 34 | root.right = self.solve(prevals[left_length + 1:], invals[inorder_root_idx + 1:]) 35 | return root 36 | 37 | def inorder(self, subtree): 38 | if subtree: 39 | self.inorder(subtree.left) 40 | self.inorders.append(subtree.val) 41 | self.inorder(subtree.right) 42 | 43 | def preorder(self, subtree): 44 | if subtree: 45 | self.pres.append(subtree.val) 46 | self.preorder(subtree.left) 47 | self.preorder(subtree.right) 48 | 49 | 50 | def test(): 51 | s = Solution() 52 | prevals = [1, 2, 4, 7, 3, 5, 6, 8] 53 | invals = [4, 7, 2, 1, 5, 3, 8, 6] 54 | root = s.solve(prevals, invals) 55 | s.inorder(root) 56 | assert s.inorders == invals 57 | 58 | s.preorder(root) 59 | assert s.pres == prevals 60 | 61 | 62 | if __name__ == '__main__': 63 | test() 64 | -------------------------------------------------------------------------------- /剑指offer/07_QueueWithTwoStacks(用两个栈实现队列).py: -------------------------------------------------------------------------------- 1 | """ 2 | 面试题7:用两个栈实现队列 3 | 题目:用两个栈实现一个队列。队列的声明如下,请实现它的两个函数appendTail和deleteHead,分别完成在队列尾部插入结点和在队列头部删除结点的功能。 4 | """ 5 | 6 | from collections import deque 7 | 8 | 9 | class Stack: 10 | def __init__(self): 11 | self.items = deque() 12 | 13 | def push(self, val): 14 | return self.items.append(val) 15 | 16 | def pop(self): 17 | return self.items.pop() 18 | 19 | def empty(self): 20 | return len(self.items) == 0 21 | 22 | def __len__(self): 23 | return len(self.items) 24 | 25 | 26 | class Queue: 27 | def __init__(self): 28 | self.s1 = Stack() 29 | self.s2 = Stack() 30 | 31 | def append(self, val): 32 | self.s1.push(val) 33 | 34 | def pop(self): 35 | if len(self.s2): 36 | return self.s2.pop() 37 | while len(self.s1): 38 | val = self.s1.pop() 39 | self.s2.push(val) 40 | return self.s2.pop() 41 | 42 | 43 | def test(): 44 | q = Queue() 45 | q.append(1) 46 | q.append(2) 47 | q.append(3) 48 | assert q.pop() == 1 49 | q.append(4) 50 | assert q.pop() == 2 51 | assert q.pop() == 3 52 | assert q.pop() == 4 53 | 54 | 55 | if __name__ == '__main__': 56 | test() 57 | -------------------------------------------------------------------------------- /剑指offer/08_MinNumberInRotatedArray(旋转数组最小数字).py: -------------------------------------------------------------------------------- 1 | """ 2 | 面试题8:旋转数组的最小数字 3 | 题目:把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个递增排序的数组的一个旋转,输出旋转数组的最小元素。 4 | 例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1。 5 | """ 6 | 7 | 8 | class Solution: 9 | def findMin(self, array): 10 | """ 11 | 思路:二分 12 | 关键点:旋转数组的第一个数字是前半部分最小的,也是后半部分最大的 13 | """ 14 | if len(array) == 1: 15 | return array[0] 16 | first = array[0] 17 | size = len(array) 18 | beg = 1 19 | end = size 20 | 21 | while beg < end: 22 | mid = (beg + end) // 2 23 | if array[mid] > first: 24 | beg = mid + 1 25 | else: 26 | end = mid 27 | if beg == size: 28 | return first 29 | else: 30 | return array[beg] 31 | 32 | 33 | def test(): 34 | s = Solution() 35 | assert s.findMin([0]) == 0 36 | assert s.findMin([1, 2]) == 1 # 注意这个特殊case 37 | assert s.findMin([3, 4, 5, 1, 2]) == 1 38 | 39 | 40 | if __name__ == '__main__': 41 | test() 42 | -------------------------------------------------------------------------------- /剑指offer/10_NumberOf1InBinary(二进制中1的个数).py: -------------------------------------------------------------------------------- 1 | """ 2 | 面试题10:二进制中1的个数 3 | 题目:请实现一个函数,输入一个整数,输出该数二进制表示中1的个数。例如把9表示成二进制是1001,有2位是1。因此如果输入9,该函数输出2。"" 4 | """ 5 | 6 | 7 | class Solution: 8 | def solve_wrong(self, num): 9 | """ 10 | 不断右移,每次和1做与运算,结果为1就加1. 11 | NOTE:但是输入负数会死循环。 12 | """ 13 | cnt = 0 14 | while num: 15 | if num & 1: 16 | cnt += 1 17 | num >>= 1 18 | return cnt 19 | 20 | def solve(self, num): 21 | """ 22 | 不太能想出来:把一个数字不断和它的 num-1 与运算,能做几次就有几个 1 23 | 用一条语句判断一个整数是不是2的整数次方。一个整数如果是2的整数次方,那么它的二进制表示中有且只有一位是1, 24 | 而其他所有位都是0。根据前面的分析,把这个整数减去1之后再和它自己做与运算,这个整数中唯一的1就会变成0。 25 | """ 26 | cnt = 0 27 | while num: 28 | num = (num - 1) & num 29 | cnt += 1 30 | return cnt 31 | 32 | def solve_py(self, num): 33 | """python有一种比较 tricky 的方式来做 34 | """ 35 | s = format(num, 'b') 36 | return s.counts('1') 37 | 38 | 39 | def test(): 40 | s = Solution() 41 | assert s.solve(9) == 2 42 | assert s.solve(1) == 1 43 | assert s.solve(8) == 1 44 | assert s.solve(0) == 0 45 | 46 | 47 | if __name__ == '__main__': 48 | test() 49 | -------------------------------------------------------------------------------- /剑指offer/12_Print1ToMaxOfNDigits(打印1到最大的n位数).py: -------------------------------------------------------------------------------- 1 | """ 2 | 面试题12:打印1到最大的n位数 3 | 题目:输入数字n,按顺序打印出从1最大的n位十进制数。比如输入3,则打印出1、2、3一直到最大的3位数即999。 4 | 5 | 思路:不能上来直接模拟。时间复杂度太大。 6 | 因为 python 其实支持大数字运算,不需要像其他语言一样使用数组模拟大数。 7 | """ 8 | 9 | 10 | class Solution: 11 | def solve(self, n): 12 | """递归的思路输出所有排列""" 13 | nums = [0] * n 14 | self._solve(nums, 0, n) 15 | 16 | def _solve(self, nums, beg, end): 17 | if beg == end: 18 | print(nums) 19 | return 20 | for i in range(10): 21 | nums[beg] = i 22 | self._solve(nums, beg + 1, end) 23 | 24 | 25 | def test(): 26 | s = Solution() 27 | s.solve(9) 28 | 29 | 30 | if __name__ == '__main__': 31 | test() 32 | -------------------------------------------------------------------------------- /剑指offer/13_DeleteNodeInList(O1时间删除链表节点).py: -------------------------------------------------------------------------------- 1 | """ 2 | 面试题13:在O(1)时间删除链表结点 3 | 题目:给定单向链表的头指针和一个结点指针,定义一个函数在O(1)时间删除该结点。链表结点与函数的定义如下: 4 | 5 | https://leetcode.com/problems/delete-node-in-a-linked-list/ 6 | """ 7 | 8 | 9 | class Node: 10 | def __init__(self, val, next=None): 11 | self.val, self.next = val, next 12 | 13 | 14 | class Solution: 15 | def solve(self, headnode, target_node): 16 | """ 17 | 传统方法是从头遍历到要删除的节点,然后让前一个节点指向下一个节点。 18 | 思路:把下一个节点节点复制到当前节点就好了。但是要注意只有一个节点的情况 19 | - 链表只有一个节点 20 | - 链表有多个节点并且不是尾节点 21 | - 链表有多个节点并且是尾节点(此时不存在下一个节点了,需要从头遍历) 22 | """ 23 | if not headnode or not target_node: 24 | return 25 | 26 | if target_node.next: 27 | next_node = target_node.next 28 | target_node.next = next_node.next 29 | target_node.val = next_node.val 30 | del next_node 31 | 32 | elif target_node == headnode: 33 | headnode.next = None 34 | del target_node 35 | 36 | else: # O(n) 删除 37 | cur_node = headnode 38 | while cur_node: 39 | if cur_node.next == target_node: 40 | cur_node.next = target_node.next 41 | del target_node 42 | break 43 | cur_node = cur_node.next 44 | -------------------------------------------------------------------------------- /剑指offer/14_ReorderArray(调整奇偶顺序).py: -------------------------------------------------------------------------------- 1 | """ 2 | 面试题14:调整数组顺序使奇数位于偶数前面 3 | 题目:输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有奇数位于数组的前半部分,所有偶数位于数组的后半部分 4 | 5 | https://www.lintcode.com/problem/partition-array-by-odd-and-even/description 6 | """ 7 | 8 | 9 | class Solution: 10 | """ 11 | @param: nums: an array of integers 12 | @return: nothing 13 | """ 14 | 15 | def partitionArray(self, nums): 16 | """ 17 | 和快排的 partition 比较类似,只不过一个是根据数据的大小,一个是根据是否是 奇数和偶数 18 | """ 19 | beg, end = 0, len(nums)-1 20 | while True: 21 | while beg < end and nums[beg] % 2 == 1: 22 | beg += 1 23 | while beg < end and nums[end] % 2 == 0: 24 | end -= 1 25 | if beg >= end: 26 | break 27 | else: 28 | nums[beg], nums[end] = nums[end], nums[beg] 29 | 30 | 31 | def test(): 32 | s = Solution() 33 | ll = [1, 2, 3, 4] 34 | s.partitionArray(ll) 35 | assert ll == [1, 3, 2, 4] 36 | 37 | ll = [] 38 | s.partitionArray(ll) 39 | assert ll == [] 40 | 41 | 42 | if __name__ == '__main__': 43 | test() 44 | -------------------------------------------------------------------------------- /剑指offer/15_KthNodeFromEnd(链表倒数第k个节点).py: -------------------------------------------------------------------------------- 1 | """ 2 | 面试题15:链表中倒数第k个结点 3 | 题目:输入一个链表,输出该链表中倒数第k个结点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾结点是倒数第1个结点。 4 | 例如一个链表有6个结点,从头结点开始它们的值依次是1、2、3、4、5、6。这个链表的倒数第3个结点是值为4的结点。 5 | 6 | https://leetcode.com/problems/remove-nth-node-from-end-of-list/ 7 | """ 8 | 9 | # Definition for singly-linked list. 10 | 11 | 12 | class Node: 13 | def __init__(self, x=None, next=None): 14 | self.val = x 15 | self.next = next 16 | 17 | def __str__(self): 18 | return '{}'.format(self.val) 19 | __repr__ = __str__ 20 | 21 | 22 | class Solution: 23 | def removeNthFromEnd(self, head, n): 24 | """ 25 | 思路:这一题的约束是只需要遍历一次。可以两个指针,相差 k 步 26 | 27 | 28 | :type head: ListNode 29 | :type n: int 30 | :rtype: ListNode 31 | """ 32 | # 做麻烦了,具体看 leetcode 这一题题解。加上root指向head 更容易做 33 | curnode = head 34 | prenode = behind_node = head 35 | for i in range(n-1): 36 | curnode = curnode.next 37 | while curnode.next: 38 | curnode = curnode.next 39 | prenode = behind_node 40 | behind_node = behind_node.next 41 | if prenode == behind_node == head: 42 | newhead = head.next 43 | del head 44 | return newhead 45 | elif prenode != behind_node: 46 | prenode.next = behind_node.next 47 | del behind_node 48 | return head 49 | 50 | def to_list(self, head): 51 | res = [] 52 | cur_node = head 53 | while cur_node: 54 | res.append(cur_node.val) 55 | cur_node = cur_node.next 56 | return res 57 | 58 | 59 | def test(): 60 | s = Solution() 61 | linklist = Node(1) 62 | head = s.removeNthFromEnd(linklist, 1) 63 | assert s.to_list(head) == [] 64 | 65 | linklist = Node(1, Node(2)) 66 | head = s.removeNthFromEnd(linklist, 1) 67 | assert s.to_list(head) == [1] 68 | 69 | linklist = Node(1, Node(2)) 70 | head = s.removeNthFromEnd(linklist, 2) 71 | assert s.to_list(head) == [2] 72 | 73 | linklist = Node(1, Node(2, Node(3, Node(4, Node(5))))) 74 | head = s.removeNthFromEnd(linklist, 2) 75 | assert s.to_list(head) == [1, 2, 3, 5] 76 | 77 | linklist = Node(1, Node(2, Node(3, Node(4, Node(5))))) 78 | head = s.removeNthFromEnd(linklist, 1) 79 | assert s.to_list(head) == [1, 2, 3, 4] 80 | 81 | linklist = Node(1, Node(2, Node(3, Node(4, Node(5))))) 82 | head = s.removeNthFromEnd(linklist, 5) 83 | assert s.to_list(head) == [2, 3, 4, 5] 84 | 85 | 86 | if __name__ == '__main__': 87 | test() 88 | -------------------------------------------------------------------------------- /剑指offer/16_ReverseList(翻转链表).py: -------------------------------------------------------------------------------- 1 | """ 2 | 定义一个函数,输入一个链表的头结点,反转该链表并输出反转后链表的头结点。链表结点定义如下: 3 | leetcode: 4 | https://leetcode.com/problems/reverse-linked-list/ 5 | """ 6 | 7 | # Definition for singly-linked list. 8 | 9 | 10 | class Node: 11 | def __init__(self, x, next=None): 12 | self.val = x 13 | self.next = next 14 | 15 | 16 | class Solution: 17 | 18 | def reverseList(self, head): 19 | """ 20 | :type head: ListNode 21 | :rtype: ListNode 22 | """ 23 | if not head or not head.next: 24 | return head 25 | newhead = self.reverseList(head.next) 26 | nextnode = head.next # head -> nextnode 27 | nextnode.next = head # head <- nextnode 28 | head.next = None # None -< head <= nextnode 29 | return newhead 30 | 31 | def reverseList_iter(self, head): 32 | """ 33 | :type head: ListNode 34 | :rtype: ListNode 35 | """ 36 | prenode = None 37 | curnode = head 38 | while curnode: 39 | nextnode = curnode.next 40 | curnode.next = prenode 41 | prenode = curnode 42 | curnode = nextnode 43 | return prenode # 注意返回的是 prenode 44 | 45 | def to_list(self, head): 46 | res = [] 47 | curnode = head 48 | while curnode: 49 | res.append(curnode.val) 50 | curnode = curnode.next 51 | return res 52 | 53 | 54 | def test(): 55 | s = Solution() 56 | ll = Node(1, Node(2, Node(3, Node(4)))) 57 | head = s.reverseList(ll) 58 | assert s.to_list(head) == [4, 3, 2, 1] 59 | 60 | ll = Node(1) 61 | head = s.reverseList(ll) 62 | assert s.to_list(head) == [1] 63 | 64 | 65 | def test_rec(): 66 | s = Solution() 67 | ll = Node(1, Node(2, Node(3, Node(4)))) 68 | head = s.reverseList(ll) 69 | assert s.to_list(head) == [4, 3, 2, 1] 70 | 71 | 72 | if __name__ == '__main__': 73 | test() 74 | -------------------------------------------------------------------------------- /剑指offer/17_MergeSortedLists(合并两个有序链表).py: -------------------------------------------------------------------------------- 1 | """ 2 | 面试题17:合并两个排序的链表 3 | 题目:输入两个递增排序的链表,合并这两个链表并使新链表中的结点仍然是按照递增排序的 4 | 5 | https://leetcode.com/problems/merge-two-sorted-lists/ 6 | 7 | Merge two sorted linked lists and return it as a new list. The new list should be made by splicing together the nodes of the first two lists. 8 | 9 | Example: 10 | 11 | Input: 1->2->4, 1->3->4 12 | Output: 1->1->2->3->4->4 13 | """ 14 | 15 | 16 | # Definition for singly-linked list. 17 | class ListNode: 18 | def __init__(self, x, next=None): 19 | self.val = x 20 | self.next = next 21 | 22 | def __str__(self): 23 | return '{}'.format(self.val) 24 | __repr__ = __str__ 25 | 26 | 27 | Node = ListNode 28 | 29 | 30 | class Solution: 31 | def mergeTwoLists(self, l1, l2): 32 | """ 33 | :type l1: ListNode 34 | :type l2: ListNode 35 | :rtype: ListNode 36 | """ 37 | if not l1: 38 | return l2 39 | if not l2: 40 | return l1 41 | 42 | root = cur = ListNode(None) 43 | while l1 and l2: 44 | if l1.val < l2.val: 45 | node = ListNode(l1.val) 46 | l1 = l1.next 47 | else: 48 | node = ListNode(l2.val) 49 | l2 = l2.next 50 | cur.next = node 51 | cur = node 52 | cur.next = l1 or l2 # 链接剩余元素 53 | return root.next 54 | 55 | 56 | class Solution2: 57 | 58 | def mergeTwoLists(self, l1, l2): 59 | """ 60 | 思路:使用递归简化问题,一开始想用循环来写的,比较麻烦 61 | 62 | :type l1: ListNode 63 | :type l2: ListNode 64 | :rtype: ListNode 65 | """ 66 | if not l1: 67 | return l2 68 | if not l2: 69 | return l1 70 | node1, node2 = l1, l2 71 | head = None 72 | if node1.val < node2.val: 73 | head = node1 74 | head.next = self.mergeTwoLists(node1.next, node2) 75 | else: 76 | head = node2 77 | head.next = self.mergeTwoLists(node1, node2.next) 78 | return head 79 | 80 | def to_list(self, head): 81 | res = [] 82 | curnode = head 83 | while curnode: 84 | res.append(curnode.val) 85 | curnode = curnode.next 86 | return res 87 | 88 | 89 | def test(): 90 | ll1 = Node(1, Node(2, Node(4))) 91 | ll2 = Node(1, Node(3, Node(4))) 92 | s = Solution() 93 | head = s.mergeTwoLists(ll1, ll2) 94 | assert s.to_list(head) == [1, 1, 2, 3, 4, 4] 95 | 96 | 97 | if __name__ == '__main__': 98 | test() 99 | -------------------------------------------------------------------------------- /剑指offer/18_SubstructureInTree(树的子结构).py: -------------------------------------------------------------------------------- 1 | """ 2 | 面试题18:树的子结构 3 | 题目:输入两棵二叉树A和B,判断B是不是A的子结构。 4 | 5 | https://leetcode.com/problems/subtree-of-another-tree/ 这一题和 leetcode 还不太一样。剑指offer 上要求是子树, 6 | leetcode 这一题要求是严格的子树 7 | """ 8 | 9 | 10 | class Node: 11 | def __init__(self, val, left=None, right=None): 12 | self.val, self.left, self.right = val, left, right 13 | 14 | 15 | class Solution: 16 | def isSubtree(self, s, t): 17 | """ TODO 思路 18 | :type s: TreeNode 19 | :type t: TreeNode 20 | :rtype: bool 21 | """ 22 | res = False 23 | if s and t: 24 | if s.val == t.val: 25 | res = self.is_sametree(s, t) 26 | if not res: 27 | res = self.isSubtree(s.left, t) 28 | if not res: 29 | res = self.isSubtree(s.right, t) 30 | return res 31 | 32 | def is_sametree(self, t1, t2): 33 | """递归判断两个树是否相同""" 34 | if t1 is None and t2 is None: 35 | return True 36 | if t1 is None or t2 is None: 37 | return False 38 | if t1.val != t2.val: 39 | return False 40 | return self.is_sametree(t1.left, t2.left) and self.is_sametree(t1.right, t2.right) 41 | 42 | 43 | def test(): 44 | s = Node(3, Node(4, Node(1), Node(2)), Node(5)) 45 | t = Node(4, Node(1), Node(2)) 46 | so = Solution() 47 | assert so.isSubtree(s, t) is True 48 | 49 | s = Node(3, Node(4, Node(1), Node(2, Node(0))), Node(5)) 50 | t = Node(4, Node(1), Node(2)) 51 | so = Solution() 52 | assert so.isSubtree(s, t) is False 53 | 54 | 55 | test() 56 | -------------------------------------------------------------------------------- /剑指offer/19_MirrorOfBinaryTree(二叉树镜像).py: -------------------------------------------------------------------------------- 1 | """ 2 | 请完成一个函数,输入一个二叉树,该函数输出它的镜像。 3 | https://leetcode.com/problems/invert-binary-tree/ 4 | """ 5 | 6 | # Definition for a binary tree node. 7 | # class TreeNode: 8 | # def __init__(self, x): 9 | # self.val = x 10 | # self.left = None 11 | # self.right = None 12 | 13 | 14 | class _Solution: 15 | def invertTree(self, root): 16 | """ 17 | :type root: TreeNode 18 | :rtype: TreeNode 19 | """ 20 | if root: 21 | root.left, root.right = root.right, root.left 22 | self.invertTree(root.left) 23 | self.invertTree(root.right) 24 | return root 25 | 26 | 27 | # Definition for a binary tree node. 28 | class Node: 29 | def __init__(self, x, left=None, right=None): 30 | self.val = x 31 | self.left = left 32 | self.right = right 33 | 34 | def __str__(self): 35 | return '{}'.format(self.val) 36 | 37 | __repr__ = __str__ 38 | 39 | 40 | from collections import deque 41 | 42 | 43 | class Stack: 44 | def __init__(self): 45 | self.items = deque() 46 | 47 | def push(self, val): 48 | return self.items.append(val) 49 | 50 | def pop(self): 51 | return self.items.pop() 52 | 53 | def empty(self): 54 | return len(self.items) == 0 55 | 56 | 57 | class Solution: # https://leetcode.com/problems/symmetric-tree/ 58 | 59 | def isSymmetric(self, root): 60 | """ use stack """ 61 | if not root: 62 | return True 63 | s = Stack() 64 | s.push((root.left, root.right)) # push a tuple 65 | while not s.empty(): 66 | top_vals = s.pop() 67 | left_node, right_node = top_vals[0], top_vals[1] 68 | if left_node and right_node: 69 | if left_node.val == right_node.val: 70 | s.push((left_node.left, right_node.right)) 71 | s.push((left_node.right, right_node.left)) 72 | else: 73 | return False 74 | else: 75 | if left_node != right_node: 76 | return False 77 | return True 78 | 79 | def isSymmetric_recursive(self, root): 80 | """ 判断是否是镜像,使用递归的方式 81 | :type root: TreeNode 82 | :rtype: bool 83 | """ 84 | def _check(left, right): 85 | if left and right: 86 | if left.val == right.val: 87 | flag1 = _check(left.left, right.right) 88 | flag2 = _check(left.right, right.left) 89 | return flag1 and flag2 90 | else: 91 | return False 92 | else: 93 | return left == right # 这种情况下 left 和 right 要么一个为 None,或者都是 None 94 | 95 | if root: 96 | return _check(root.left, root.right) 97 | return True 98 | 99 | def isSymmetric_layer(self, root): 100 | """ 判断是否是镜像,使用层序遍历 101 | :type root: TreeNode 102 | :rtype: bool 103 | """ 104 | if not root: 105 | return True 106 | curnodes = [root] 107 | next_nodes = [] 108 | while curnodes or next_nodes: 109 | lefts = [] 110 | rights = [] 111 | for node in curnodes: 112 | lefts.append(node.left.val if node.left else None) # NOTE: append val not node 113 | rights.append(node.right.val if node.right else None) 114 | if node.left: 115 | next_nodes.append(node.left) 116 | if node.right: 117 | next_nodes.append(node.right) 118 | if lefts != rights[::-1]: 119 | return False 120 | 121 | curnodes = next_nodes 122 | next_nodes = [] 123 | return True 124 | 125 | 126 | def test(): 127 | t = Node(1, Node(2, Node(3), Node(4)), Node(2, Node(4), Node(3))) 128 | s = Solution() 129 | assert s.isSymmetric(t) is True 130 | 131 | 132 | test() 133 | -------------------------------------------------------------------------------- /剑指offer/20_PrintMatrix(螺旋矩阵).py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | https://leetcode.com/problems/spiral-matrix/ 5 | 6 | 输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字。 7 | 8 | Given a matrix of m x n elements (m rows, n columns), return all elements of the matrix in spiral order. 9 | 10 | Example 1: 11 | 12 | Input: 13 | [ 14 | [ 1, 2, 3 ], 15 | [ 4, 5, 6 ], 16 | [ 7, 8, 9 ] 17 | ] 18 | Output: [1,2,3,6,9,8,7,4,5] 19 | Example 2: 20 | 21 | Input: 22 | [ 23 | [1, 2, 3, 4], 24 | [5, 6, 7, 8], 25 | [9,10,11,12] 26 | ] 27 | Output: [1,2,3,4,8,12,11,10,9,5,6,7] 28 | 29 | 30 | # https://leetcode.com/problems/spiral-matrix/discuss/20571/1-liner-in-Python-%2B-Ruby 31 | 看到了这个天才的解法,只需要一行 32 | 33 | spiral_order([[1, 2, 3], 34 | [4, 5, 6], 35 | [7, 8, 9]]) 36 | 37 | = [1, 2, 3] + spiral_order([[6, 9], 38 | [5, 8], 39 | [4, 7]]) 40 | 41 | = [1, 2, 3] + [6, 9] + spiral_order([[8, 7], 42 | [5, 4]]) 43 | 44 | = [1, 2, 3] + [6, 9] + [8, 7] + spiral_order([[4], 45 | [5]]) 46 | 47 | = [1, 2, 3] + [6, 9] + [8, 7] + [4] + spiral_order([[5]]) 48 | 49 | = [1, 2, 3] + [6, 9] + [8, 7] + [4] + [5] + spiral_order([]) 50 | 51 | = [1, 2, 3] + [6, 9] + [8, 7] + [4] + [5] + [] 52 | 53 | = [1, 2, 3, 6, 9, 8, 7, 4, 5] 54 | """ 55 | 56 | 57 | class Solution: 58 | def spiralOrder(self, matrix): 59 | """ 60 | :type matrix: List[List[int]] 61 | :rtype: List[int] 62 | """ 63 | if not matrix: # 注意递归出口,凡是涉及递归的都要注意 64 | return [] 65 | first_row = matrix.pop(0) # 弹出第一行元素 66 | rotate_matrix = list(zip(*matrix))[::-1] 67 | return list(first_row) + self.spiralOrder(rotate_matrix) # 注意 list(first_row)强转 68 | 69 | 70 | def test(): 71 | matrix = [ 72 | [1, 2, 3], 73 | [4, 5, 6], 74 | [7, 8, 9] 75 | ] 76 | s = Solution() 77 | assert s.spiralOrder(matrix) == [1, 2, 3, 6, 9, 8, 7, 4, 5] 78 | 79 | 80 | if __name__ == '__main__': 81 | test() 82 | -------------------------------------------------------------------------------- /剑指offer/21_MinInStack(包含min 函数的栈).py: -------------------------------------------------------------------------------- 1 | """ 2 | 面试题21:包含min函数的栈 3 | 题目:定义栈的数据结构,请在该类型中实现一个能够得到栈的最小元素的min函数。在该栈中,调用min、push及pop的时间复杂度都是O(1)。 4 | 5 | https://leetcode.com/problems/min-stack/ 6 | 7 | Design a stack that supports push, pop, top, and retrieving the minimum element in constant time. 8 | 9 | push(x) -- Push element x onto stack. 10 | pop() -- Removes the element on top of the stack. 11 | top() -- Get the top element. 12 | getMin() -- Retrieve the minimum element in the stack. 13 | 14 | Example: 15 | MinStack minStack = new MinStack(); 16 | minStack.push(-2); 17 | minStack.push(0); 18 | minStack.push(-3); 19 | minStack.getMin(); --> Returns -3. 20 | minStack.pop(); 21 | minStack.top(); --> Returns 0. 22 | minStack.getMin(); --> Returns -2. 23 | """ 24 | 25 | from collections import deque 26 | 27 | 28 | class Stack: 29 | def __init__(self): 30 | self.items = deque() 31 | 32 | def push(self, val): 33 | return self.items.append(val) 34 | 35 | def pop(self): 36 | return self.items.pop() 37 | 38 | def empty(self): 39 | return len(self.items) == 0 40 | 41 | def top(self): 42 | return self.items[-1] 43 | 44 | 45 | class MinStack: 46 | 47 | def __init__(self): 48 | """ 49 | initialize your data structure here. 50 | """ 51 | self.s = Stack() 52 | self.mins = Stack() 53 | 54 | def push(self, x): 55 | """ 56 | :type x: int 57 | :rtype: void 58 | """ 59 | self.s.push(x) 60 | if self.mins.empty(): 61 | self.mins.push(x) 62 | else: 63 | min_val = self.mins.top() 64 | if x < min_val: 65 | self.mins.push(x) 66 | else: 67 | self.mins.push(min_val) 68 | 69 | def pop(self): 70 | """ 71 | :rtype: void 72 | """ 73 | self.mins.pop() 74 | return self.s.pop() 75 | 76 | def top(self): 77 | """ 78 | :rtype: int 79 | """ 80 | return self.s.top() 81 | 82 | def getMin(self): 83 | """Retrieve the minimum element in the stack. 84 | :rtype: int 85 | """ 86 | return self.mins.top() 87 | 88 | 89 | def test(): 90 | minStack = MinStack() 91 | minStack.push(-2) 92 | minStack.push(0) 93 | minStack.push(-3) 94 | assert minStack.getMin() == -3 # --> Returns -3. 95 | minStack.pop() 96 | assert minStack.top() == 0 # --> Returns 0. 97 | assert minStack.getMin() == -2 # --> Returns -2. 98 | 99 | 100 | if __name__ == '__main__': 101 | test() 102 | -------------------------------------------------------------------------------- /剑指offer/22_StackPushPopOrder(栈的压入弹出序列).py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | 面试题22:栈的压入、弹出序列 5 | 题目:输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。 6 | 假设压入栈的所有数字均不相等。 7 | 例如序列1、2、3、4、5是某栈的压栈序列,序列4、5、3、2、1是该压栈序列对应的一个弹出序列, 8 | 但4、3、5、1、2就不可能是该压栈序列的弹出序列。 9 | 10 | 比如: 11 | push 1 12 | push 2 13 | push 3 14 | push 4 15 | pop 4 # 4 16 | push 5 17 | pop 5 # 4,5 18 | pop 3 # 4,5,3 19 | pop 2 # 4,5,3,2 20 | pop 1 # 4,5,3,2,1 21 | """ 22 | 23 | 24 | from collections import deque 25 | 26 | 27 | class Stack: 28 | def __init__(self): 29 | self.items = deque() 30 | 31 | def push(self, val): 32 | return self.items.append(val) 33 | 34 | def pop(self): 35 | return self.items.pop() 36 | 37 | def empty(self): 38 | return len(self.items) == 0 39 | 40 | def top(self): 41 | return self.items[-1] 42 | 43 | def __str__(self): 44 | return str(self.items) 45 | 46 | __repr__ = __str__ 47 | 48 | 49 | class Solution: 50 | def solve(self, nums1, nums2): 51 | """ 52 | 思路:借助一个辅助stack 53 | 如果下一个弹出数字刚好是栈顶数字,直接弹出。 54 | 如果下一个弹出数字不在栈顶,把压栈序列中还没有入栈的数字push 进辅助栈, 55 | 直到把下一个需要弹出的数字压入栈顶为止。 56 | 如果所有数字压入栈了仍然没找到下一个弹出的数字,说明不可能是一个弹出序列。 57 | """ 58 | idx1, idx2 = 0, 0 59 | s = Stack() 60 | 61 | while True: 62 | if s.empty() or s.top() != nums2[idx2]: 63 | s.push(nums1[idx1]) 64 | idx1 += 1 65 | if s.top() == nums2[idx2]: 66 | s.pop() 67 | idx2 += 1 68 | print(s, idx1, idx2) 69 | if idx1 == len(nums1) and s.empty(): 70 | return True 71 | # 注意最后一个判断条件,这个时候如果已经push 完了,结果栈顶还是找不到 nums2中元素,返回False 72 | if idx1 == len(nums1) and not s.empty() and s.top() != nums2[idx2]: 73 | return False 74 | 75 | 76 | def test(): 77 | s = Solution() 78 | assert s.solve([1, 2, 3, 4, 5], [4, 5, 3, 2, 1]) is True 79 | assert s.solve([1, 2, 3, 4, 5], [4, 3, 5, 1, 2]) is False 80 | assert s.solve([1], [1]) is True 81 | assert s.solve([1], [2]) is False 82 | 83 | 84 | if __name__ == '__main__': 85 | test() 86 | -------------------------------------------------------------------------------- /剑指offer/23_BfsTree(层序从上往下打印二叉树).py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 层序遍历二叉树 4 | """ 5 | 6 | from collections import deque 7 | 8 | 9 | class Queue: 10 | def __init__(self): 11 | self.items = deque() 12 | 13 | def append(self, val): 14 | return self.items.append(val) 15 | 16 | def pop(self): 17 | return self.items.popleft() 18 | 19 | def empty(self): 20 | return len(self.items) == 0 21 | 22 | 23 | class Node: 24 | def __init__(self, val, left=None, right=None): 25 | self.val, self.left, self.right = val, left, right 26 | 27 | 28 | class Solution: 29 | def solve(self, root): 30 | if not root: 31 | return [] 32 | curnodes = [root] 33 | nextnodes = [] 34 | res = [root.val] # 别忘记第一个元素的值放进去 35 | while curnodes: 36 | for node in curnodes: 37 | if node.left: 38 | nextnodes.append(node.left) 39 | if node.right: 40 | nextnodes.append(node.right) 41 | curnodes = nextnodes 42 | res.extend([node.val for node in nextnodes]) 43 | nextnodes = [] # 更新nextnodes 44 | return res 45 | 46 | def solve_use_queue(self, root): 47 | """use queue bfs""" 48 | if not root: 49 | return [] 50 | q = Queue() 51 | res = [root.val] 52 | q.append(root) 53 | while not q.empty(): 54 | curnode = q.pop() 55 | if curnode.left: 56 | q.append(curnode.left) 57 | res.append(curnode.left.val) 58 | if curnode.right: 59 | q.append(curnode.right) 60 | res.append(curnode.right.val) 61 | return res 62 | 63 | 64 | def test(): 65 | tree = Node(1, Node(2), Node(3)) 66 | s = Solution() 67 | assert s.solve(tree) == [1, 2, 3] 68 | assert s.solve_use_queue(tree) == [1, 2, 3] 69 | 70 | 71 | if __name__ == '__main__': 72 | test() 73 | -------------------------------------------------------------------------------- /剑指offer/24_SquenceOfBST(二叉搜索树的后序遍历序列).py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 面试题24:二叉搜索树的后序遍历序列 4 | 题目:输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历的结果。如果是则返回true,否则返回false。假设输入的数组的任意两个数字都互不相同。 5 | 例如输入数组{5,7,6,9,11,10,8},则返回true,因为这个整数序列是图4.6二叉搜索树的后序遍历结果。如果输入的数组是{7,4,6,5},由于没有哪棵二叉搜索树的后序遍历的结果是这个序列,因此返回false。 6 | 7 | 类似练习:https://leetcode.com/problems/validate-binary-search-tree/ 8 | """ 9 | 10 | 11 | class Solution: 12 | def solve(self, nums): 13 | """考点:二叉搜索树,性质:左子树都小于根节点值,右子树都大于根节点值 14 | 15 | 思路:递归求解 16 | - 注意递归出口 17 | - 找到左右子树 18 | - 判断是否右子树值都是大于根节点的,如果不是,直接 return False 19 | - 递归判断左子树和右子树 20 | """ 21 | if len(nums) == 1: # 递归出口,只有一个元素怎么遍历都是自己 22 | return True 23 | 24 | root_val = nums[-1] # 后根序,根节点的值 25 | left_end = 0 26 | while nums[left_end] < root_val: 27 | left_end += 1 28 | left_part = nums[0:left_end] 29 | right_part = nums[left_end: -1] 30 | 31 | for val in right_part: 32 | if val < root_val: # 右子树必须都要大于根节点的值 33 | return False 34 | 35 | return self.solve(left_part) and self.solve(right_part) 36 | 37 | 38 | def test(): 39 | s = Solution() 40 | nums = [5, 7, 6, 9, 11, 10, 8] 41 | assert s.solve(nums) is True 42 | 43 | nums = [5] 44 | assert s.solve(nums) is True 45 | 46 | nums = [7, 4, 6, 5] 47 | assert s.solve(nums) is False 48 | 49 | 50 | if __name__ == '__main__': 51 | test() 52 | -------------------------------------------------------------------------------- /剑指offer/25_PathInTree(二叉树和为某一个值).py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 面试题25:二叉树中和为某一值的路径 4 | 题目:输入一棵二叉树和一个整数,打印出二叉树中结点值的和为输入整数的所有路径。 5 | 从树的根结点开始往下一直到叶结点所经过的结点形成一条路径。 6 | 7 | https://leetcode.com/problems/path-sum/ 8 | """ 9 | 10 | 11 | # Definition for a binary tree node. 12 | class Node: 13 | def __init__(self, x, left=None, right=None): 14 | self.val = x 15 | self.left = left 16 | self.right = right 17 | 18 | 19 | from collections import deque 20 | 21 | 22 | class Stack: 23 | def __init__(self): 24 | self.items = deque() 25 | 26 | def push(self, val): 27 | return self.items.append(val) 28 | 29 | def pop(self): 30 | return self.items.pop() 31 | 32 | def empty(self): 33 | return len(self.items) == 0 34 | 35 | 36 | class Solution: 37 | """ 38 | https://leetcode.com/problems/path-sum/ 39 | """ 40 | 41 | def hasPathSum(self, root, sum): 42 | if not root: 43 | return False 44 | s = Stack() 45 | s.push((root, root.val)) 46 | while not s.empty(): 47 | topnode, val = s.pop() 48 | if topnode.left is None and topnode.right is None: 49 | if val == sum: 50 | return True 51 | if topnode.left: 52 | s.push((topnode.left, val+topnode.left.val)) 53 | if topnode.right: 54 | s.push((topnode.right, val+topnode.right.val)) 55 | return False 56 | 57 | def _hasPathSum(self, root, sum, cursum=0): 58 | """思路:递归判断,如果到了叶节点(left=right=None)当前 cursum 等于sum,返回 True 59 | :type root: TreeNode 60 | :type sum: int 61 | :rtype: bool 62 | """ 63 | if not root: 64 | return False 65 | cursum += root.val 66 | if root.left is None and root.right is None: 67 | if cursum == sum: 68 | return True 69 | else: 70 | cursum -= root.val 71 | return self._hasPathSum(root.left, sum, cursum) or self._hasPathSum(root.right, sum, cursum) 72 | 73 | 74 | def test(): 75 | tree = Node(1, Node(2), Node(3)) 76 | s = Solution() 77 | # assert s.hasPathSum(tree, 4) 78 | 79 | N = Node 80 | tree = N(5, N(4, N(11, N(7), N(2))), N(8, N(13), N(4, None, N(1)))) 81 | assert s.hasPathSum(tree, 22) 82 | 83 | 84 | test() 85 | -------------------------------------------------------------------------------- /剑指offer/27_ConvertBinarySearchTree(二叉搜索树转成双向链表).py: -------------------------------------------------------------------------------- 1 | """ 2 | 面试题27:二叉搜索树与双向链表 3 | 题目:输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的结点,只能调整树中结点指针的指向。 4 | 比如输入图4.12中左边的二叉搜索树,则输出转换之后的排序双向链表。 5 | 6 | https://www.lintcode.com/problem/convert-binary-search-tree-to-doubly-linked-list/description 7 | 8 | Description 9 | Convert a binary search tree to doubly linked list with in-order traversal. 10 | 11 | Have you met this question in a real interview? 12 | Example 13 | Given a binary search tree: 14 | 15 | 4 16 | / \ 17 | 2 5 18 | / \ 19 | 1 3 20 | return 1<->2<->3<->4<->5 21 | 22 | """ 23 | 24 | 25 | # Definition of Doubly-ListNode 26 | class DoublyListNode(object): 27 | def __init__(self, val, next=None): 28 | self.val = val 29 | self.next = self.prev = None # nextDefinition of TreeNode: 30 | 31 | 32 | class TreeNode: 33 | def __init__(self, val): 34 | self.val = val 35 | self.left, self.right = None, None 36 | 37 | 38 | class Solution: 39 | """ 用了一种有点 tricky 的方式,中序遍历输出所有节点以后拼成链表 40 | @param root: The root of tree 41 | @return: the head of doubly list node 42 | """ 43 | 44 | def bstToDoublyList(self, root): 45 | if not root: 46 | return None 47 | vals = [] 48 | self.inorder(root, vals) 49 | prevnode = head = DoublyListNode(vals[0]) 50 | for idx in range(1, len(vals)): 51 | node = DoublyListNode(vals[idx]) 52 | node.prev = prevnode 53 | prevnode.next = node 54 | prevnode = node 55 | return head 56 | 57 | def inorder(self, subtree, vals): 58 | if subtree: 59 | self.inorder(subtree.left, vals) 60 | vals.append(subtree.val) 61 | self.inorder(subtree.right, vals) 62 | 63 | 64 | def test(): 65 | pass 66 | -------------------------------------------------------------------------------- /剑指offer/28_StringPermutation(字符串全排列).py: -------------------------------------------------------------------------------- 1 | """ 2 | 面试题28:字符串的排列 3 | 题目:输入一个字符串,打印出该字符串中字符的所有排列。 4 | 例如输入字符串abc,则打印出由字符a、b、c所能排列出来的所有字符串abc、acb、bac、bca、cab和cba。 5 | 6 | https://leetcode.com/problems/permutations/ 7 | 8 | Given a collection of distinct integers, return all possible permutations. 9 | 10 | Example: 11 | 12 | Input: [1,2,3] 13 | Output: 14 | [ 15 | [1,2,3], 16 | [1,3,2], 17 | [2,1,3], 18 | [2,3,1], 19 | [3,1,2], 20 | [3,2,1] 21 | ] 22 | 23 | 24 | https://leetcode.com/problems/permutations-ii/ 25 | 26 | Given a collection of numbers that might contain duplicates, return all possible unique permutations. 27 | 28 | Example: 29 | 30 | Input: [1,1,2] 31 | Output: 32 | [ 33 | [1,1,2], 34 | [1,2,1], 35 | [2,1,1] 36 | ] 37 | """ 38 | 39 | 40 | class Solution1: 41 | def permute(self, nums): 42 | """ 43 | 思路:使用递归。 44 | 可以把问题拆解为更小的问题,分成两步走:(固定第一个字符并求解其后边数组的全排列) 45 | 1. 首先求解可能出现在第一个位置的所有字符(即把第一个字符和后边的所有字符交换) 46 | 2. 求该字符后的所有字符的全排列 47 | 48 | :type nums: List[int] 49 | :rtype: List[List[int]] 50 | """ 51 | def _per(nums, beg, end, res): 52 | print(nums) 53 | if beg == end - 1: 54 | res.append(nums[:]) 55 | else: 56 | for i in range(beg, end): 57 | nums[i], nums[beg] = nums[beg], nums[i] 58 | _per(nums, beg + 1, end, res) 59 | nums[i], nums[beg] = nums[beg], nums[i] 60 | res = [] 61 | _per(nums, 0, len(nums), res) 62 | return res 63 | 64 | 65 | class Solution2: 66 | def permuteUnique(self, nums): 67 | """ 68 | :type nums: List[int] 69 | :rtype: List[List[int]] 70 | """ 71 | if not nums: 72 | return [] 73 | 74 | nums.sort() 75 | 76 | def _per(nums, beg, end, res): 77 | if beg == end - 1: 78 | res.append(nums[:]) # appen copy 79 | 80 | for i in range(beg, end): 81 | if nums[i] not in nums[beg:i]: 82 | nums[i], nums[beg] = nums[beg], nums[i] 83 | _per(nums, beg + 1, end, res) 84 | nums[i], nums[beg] = nums[beg], nums[i] 85 | 86 | res = [] 87 | _per(nums, 0, len(nums), res) 88 | return res 89 | 90 | 91 | def test_per1(): 92 | s = Solution1() 93 | nums = [1, 2, 3] 94 | for i in s.permute(nums): 95 | print(i, '=') 96 | 97 | 98 | def test_per2(): 99 | s = Solution2() 100 | nums = [1, 1, 2] 101 | for i in s.permuteUnique(nums): 102 | print(i, '=') 103 | print('\n') 104 | nums = [1, 2, 3] 105 | for i in s.permuteUnique(nums): 106 | print(i, '=') 107 | 108 | 109 | def test(): 110 | # test_per1() 111 | test_per2() 112 | 113 | 114 | def perms(nums, beg, end): 115 | if beg == end - 1: 116 | print(nums) 117 | for i in range(beg, end): 118 | nums[i], nums[beg] = nums[beg], nums[i] 119 | perms(nums, beg + 1, end) 120 | nums[i], nums[beg] = nums[beg], nums[i] 121 | 122 | 123 | if __name__ == '__main__': 124 | nums = [1, 2, 3] 125 | perms(nums, 0, len(nums)) 126 | -------------------------------------------------------------------------------- /剑指offer/29_MoreThanHalfNumber(数组中出现次数超过一半的数字).py: -------------------------------------------------------------------------------- 1 | """ 2 | 面试题29:数组中出现次数超过一半的数字 3 | 题目:数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。例如输入一个长度为9的数组{1,2,3,2,2,2,5,4,2} 。 4 | 由于数字2在数组中出现了5次,超过数组长度的一半,因此输出2。 5 | 6 | https://leetcode.com/problems/majority-element/ 7 | 8 | Given an array of size n, find the majority element. The majority element is the element that appears more than ⌊ n/2 ⌋ times. 9 | 10 | You may assume that the array is non-empty and the majority element always exist in the array. 11 | 12 | Example 1: 13 | 14 | Input: [3,2,3] 15 | Output: 3 16 | Example 2: 17 | 18 | Input: [2,2,1,1,1,2,2] 19 | Output: 2 20 | 21 | 22 | 23 | 思路: 24 | 1. nlogn 直接排序输出中位数 25 | 2. 使用快排的 partion 求中位数 26 | 3. 遍历数组, 27 | 因此我们可以考虑在遍历数组的时候保存两个值:一个是数组中的一个数字,一个是次数。当我们遍历到下一个数字的时候, 28 | 如果下一个数字和我们之前保存的数字相同,则次数加1;如果下一个数字和我们之前保存的数字不同,则次数减1。如果次数为零, 29 | 我们需要保存下 一个数字,并把次数设为1。 30 | 由于我们要找的数字出现的次数比其他所有数字出现的次数之和还要多,那么要找的数字肯定是最后一次把次数设为1时对应的数字。 31 | """ 32 | 33 | 34 | class Solution3: 35 | def majorityElement(self, nums): 36 | """ 37 | :type nums: List[int] 38 | :rtype: int 39 | """ 40 | mid = len(nums)//2 41 | return sorted(nums)[mid] 42 | 43 | 44 | def partition(array, beg, end): 45 | """对给定数组执行 partition 操作,返回新的 pivot 位置""" 46 | pivot_index = beg 47 | pivot = array[pivot_index] 48 | left = pivot_index + 1 49 | right = end - 1 # 开区间,最后一个元素位置是 end-1 [0, end-1] or [0: end),括号表示开区间 50 | 51 | while True: 52 | # 从左边找到比 pivot 大的 53 | while left <= right and array[left] < pivot: 54 | left += 1 55 | 56 | while right >= left and array[right] >= pivot: 57 | right -= 1 58 | 59 | if left > right: 60 | break 61 | else: 62 | array[left], array[right] = array[right], array[left] 63 | left += 1 64 | right -= 1 65 | 66 | array[pivot_index], array[right] = array[right], array[pivot_index] 67 | return right # 新的 pivot 位置 68 | 69 | 70 | def nth_element(array, beg, end, nth): 71 | """查找一个数组第 n 大元素""" 72 | if beg < end: 73 | pivot_idx = partition(array, beg, end) 74 | if pivot_idx == nth - 1: # 数组小标从 0 开始 75 | return array[pivot_idx] 76 | elif pivot_idx > nth - 1: 77 | return nth_element(array, beg, pivot_idx, nth) 78 | else: 79 | return nth_element(array, pivot_idx + 1, end, nth) 80 | 81 | 82 | class Solution2: 83 | def majorityElement(self, nums): 84 | """ 85 | :type nums: List[int] 86 | :rtype: int 87 | """ 88 | mid = len(nums)//2+1 89 | return nth_element(nums, 0, len(nums), mid) # 自己写的 partition 超时了 90 | 91 | 92 | class Solution: 93 | def majorityElement(self, nums): 94 | """有一个次数过半,说明他出现的次数比其他所有次数和还多。 95 | 思路: 96 | 设置两个变量一个记录次数times,一个记录当前数字cur。 97 | 如果下一个数字等于当前数字,times+1,否则times-1。 98 | 如果times为0,把times重新置位1,然后赋值为当前 cur。最后返回cur 就是需要找的值 99 | :type nums: List[int] 100 | :rtype: int 101 | """ 102 | cur = nums[0] 103 | times = 1 104 | for i in range(1, len(nums)): 105 | if nums[i] == cur: 106 | times += 1 107 | else: 108 | times -= 1 109 | if times == 0: 110 | times = 1 111 | cur = nums[i] 112 | return cur 113 | 114 | 115 | def test(): 116 | s = Solution() 117 | assert s.majorityElement([3, 2, 3]) == 3 118 | assert s.majorityElement([2, 2, 1, 1, 1, 2, 2]) == 2 119 | 120 | 121 | test() 122 | -------------------------------------------------------------------------------- /剑指offer/30_KLeastNumbers(最小的 k 个数).py: -------------------------------------------------------------------------------- 1 | """ 2 | 面试题30:最小的k个数 3 | 题目:输入n个整数,找出其中最小的k个数。例如输入4、5、1、6、2、7、3、8这8个数字,则最小的4个数字是1、2、3、4。 4 | 这道题最简单的思路莫过于把输入的n个整数排序,排序之后位于最前面的k个数就是最小的k个数。这种思路的时间复杂度是O(nlogn),面试官会提示我们还有更快的算法。 5 | 6 | 7 | 思路1:O(n) 8 | 如果能修改数组,我们可以用 parittion 函数求前 k 个(前k 个最小的,不一定是排序的) 9 | 10 | 思路2:nlogk 11 | 适合大数据处理,用一个最大堆 12 | """ 13 | 14 | import heapq 15 | 16 | 17 | class MaxHeap: 18 | """ 19 | https://stackoverflow.com/questions/2501457/what-do-i-use-for-a-max-heap-implementation-in-python 20 | py 的 heapq 模块提供了方便的最小堆,但是最大堆需要我们自己实现。 21 | 有两种方式实现: 22 | 1. 对放入的数字取反。比如 10 放入 -10 ,然后取出来的时候再取反。个人喜欢这种方式 23 | 2. 新建一个对象重写 __lt__ 方法。这种方式也可以,但是重写魔术方法修改了语义不太好 24 | 25 | import heapq 26 | 27 | class MaxHeapObj(object): 28 | def __init__(self,val): self.val = val 29 | def __lt__(self,other): return self.val > other.val 30 | def __eq__(self,other): return self.val == other.val 31 | def __str__(self): return str(self.val) 32 | """ 33 | 34 | def __init__(self, capacity): 35 | self.capacity = capacity 36 | self.minheap = [] 37 | 38 | def push(self, val): 39 | heapq.heappush(self.minheap, -val) # 取反后的数字 40 | 41 | def pop(self): 42 | val = heapq.heappop(self.minheap) 43 | return -val 44 | 45 | def max(self): 46 | return -self.minheap[0] 47 | 48 | def __iter__(self): 49 | for val in self.minheap: 50 | yield -val 51 | 52 | 53 | def test_maxheap(): 54 | mh = MaxHeap(3) 55 | mh.push(2) 56 | mh.push(1) 57 | mh.push(3) 58 | assert mh.max() == 3 59 | assert mh.pop() == 3 60 | assert mh.pop() == 2 61 | assert mh.pop() == 1 62 | 63 | 64 | class Solution: 65 | 66 | def min_k(self, nums, k): 67 | """最小的 k 个数字 68 | 分析时间复杂度 69 | """ 70 | maxheap = MaxHeap(k) 71 | for idx, val in enumerate(nums): 72 | if idx < k: 73 | maxheap.push(val) 74 | else: 75 | maxval = maxheap.max() 76 | if val < maxval: 77 | maxheap.pop() 78 | maxheap.push(val) 79 | return [i for i in maxheap] 80 | 81 | 82 | def test(): 83 | s = Solution() 84 | nums = [4, 5, 1, 6, 2, 7, 3, 8] 85 | res = s.min_k(nums, 4) 86 | assert sorted(res) == [1, 2, 3, 4] 87 | -------------------------------------------------------------------------------- /剑指offer/31_GreatestSumOfSubarrays(连续子数组最大和).py: -------------------------------------------------------------------------------- 1 | """ 2 | 面试题31:连续子数组的最大和 3 | 题目:输入一个整型数组,数组里有正数也有负数。数组中一个或连续的多个整数组成一个子数组。 4 | 求所有子数组的和的最大值。 要求时间复杂度为O(n)。 5 | 6 | Given an integer array nums, find the contiguous subarray (containing at least one number) which has the largest sum and return its sum. 7 | 8 | Example: 9 | 10 | Input: [-2,1,-3,4,-1,2,1,-5,4], 11 | Output: 6 12 | Explanation: [4,-1,2,1] has the largest sum = 6. 13 | Follow up: 14 | 15 | If you have figured out the O(n) solution, try coding another solution using the divide and conquer approach, which is more subtle. 16 | 17 | https://leetcode.com/problems/maximum-subarray/ 18 | """ 19 | 20 | 21 | class Solution: 22 | def maxSubArray(self, nums): 23 | """ 动态规划问题。写出来状态转移方程。 24 | dp[i] = max(dp[i-1]+nums[i], dp[i]) 25 | :type nums: List[int] 26 | :rtype: int 27 | """ 28 | if not nums: 29 | return 0 30 | size = len(nums) 31 | max_list = [0] * size 32 | for i in range(size): 33 | if i == 0: 34 | max_list[i] = nums[i] 35 | else: 36 | max_list[i] = max(nums[i] + max_list[i-1], nums[i]) 37 | return max(max_list) 38 | 39 | 40 | def test(): 41 | s = Solution() 42 | nums = [-2, 1, -3, 4, -1, 2, 1, -5, 4] 43 | assert s.maxSubArray(nums) == 6 44 | 45 | 46 | test() 47 | -------------------------------------------------------------------------------- /剑指offer/32_NumberOf1(从1到n整数中1出现的次数).py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 面试题32:从1到n整数中1出现的次数 4 | 题目:输入一个整数n,求从1到n这n个整数的十进制表示中1出现的次数。 5 | 例如输入12,从1到12这些整数中包含1 的数字有1,10,11和12,1一共出现了5次。 6 | 7 | 思路:比较容易想出来的一种方法是从1到 n 统计每个数字包含的1。时间复杂度n*logn 8 | """ 9 | 10 | 11 | def numberof1(n): 12 | strn = str(n) 13 | length = len(strn) 14 | first = int(strn[0]) 15 | if len(strn) == 1 and first == 0: 16 | return 0 17 | if len(strn) == 1 and first > 0: 18 | return 1 19 | # 假设 strn = "21345" 20 | # num_first_digit是数字 10000-19999 第一个位中的数目 21 | num_first_digit = 0 22 | if first > 1: # 第一位不是1 23 | num_first_digit = power_base10(length - 1) 24 | elif first == 1: # 如果第一位置是1,是后边数字+1, 12345有 2346个 25 | num_first_digit = int(strn[1:]) + 1 26 | 27 | # num_other_digits 是 1346-21345 除了第一位之外的数位中的数目 28 | num_other_digits = first * (length - 1) * power_base10(length - 2) 29 | 30 | # num_recursive 是 1-1345中的数目 31 | num_recursive = numberof1(strn[1:]) 32 | return num_first_digit + num_other_digits + num_recursive 33 | 34 | 35 | def power_base10(n): 36 | res = 1 37 | i = 0 38 | while i < n: 39 | res *= 10 40 | i += 1 41 | return res 42 | 43 | 44 | def test(): 45 | assert numberof1(10) == 1 46 | print(numberof1(21345)) 47 | 48 | 49 | test() 50 | -------------------------------------------------------------------------------- /剑指offer/33_SortArrayForMinNumber(把数组排成最小的数).py: -------------------------------------------------------------------------------- 1 | """ 2 | 面试题33:把数组排成最小的数 3 | 题目:输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。 4 | 例如输入数组{3,32,321},则打印出这3个数字能排成的最小数字321323。 5 | 6 | 类似的 leetcode 题目如下,不过是排成的最大的数字: 7 | 8 | https://leetcode.com/problems/largest-number/ 9 | 10 | Given a list of non negative integers, arrange them such that they form the largest number. 11 | 12 | Example 1: 13 | 14 | Input: [10,2] 15 | Output: "210" 16 | Example 2: 17 | 18 | Input: [3,30,34,5,9] 19 | Output: "9534330" 20 | Note: The result may be very large, so you need to return a string instead of an integer. 21 | """ 22 | 23 | from functools import cmp_to_key 24 | 25 | 26 | class Solution: 27 | def largestNumber(self, nums): 28 | """思路:直接定义比较规则就好""" 29 | def str_cmp(a, b): 30 | return int(a+b) - int(b+a) 31 | strs = [str(i) for i in nums] 32 | strs.sort(key=cmp_to_key(str_cmp), reverse=True) 33 | return str(int(''.join(strs))) 34 | 35 | def largestNumber_2(self, nums): 36 | """ 思路:一开始的解法,比较 ugly,把所有的数字扩充成一样长度,之后再比较 37 | :type nums: List[int] 38 | :rtype: str 39 | """ 40 | if len(nums) == 1: 41 | return str(nums[0]) 42 | num_strs = [str(i) for i in nums] 43 | maxlen = len(max(num_strs, key=lambda _s: len(_s))) 44 | pairs = [] 45 | for num_str in num_strs: 46 | if len(num_str) < maxlen: 47 | difflen = maxlen - len(num_str) 48 | max_char = max(num_str[0], num_str[-1]) 49 | append_num_str = num_str + ''.join([max_char] * difflen) 50 | pairs.append((append_num_str, num_str)) 51 | else: 52 | pairs.append((num_str, num_str)) 53 | res = [] 54 | for pair in sorted(pairs, reverse=True): 55 | res.append(pair[1]) 56 | idx, size = 0, len(res) 57 | while res[idx] == '0' and idx != size-1: 58 | idx += 1 59 | return ''.join(res[idx:]) 60 | 61 | 62 | def test(): 63 | s = Solution() 64 | assert s.largestNumber([824, 938, 1399, 5607, 6973, 5703, 9609, 4398, 8247]) == '9609938824824769735703560743981399' 65 | assert s.largestNumber([824, 8247]) == '8248247' 66 | assert s.largestNumber([0, 0]) == '0' 67 | assert s.largestNumber([1, 1, 1]) == '111' 68 | assert s.largestNumber([3, 30, 34, 5, 9]) == "9534330" 69 | assert s.largestNumber([10, 2]) == '210' 70 | assert s.largestNumber([0, 2]) == '20' 71 | assert s.largestNumber([1]) == '1' 72 | assert s.largestNumber([0]) == '0' 73 | 74 | 75 | test() 76 | -------------------------------------------------------------------------------- /剑指offer/34_UglyNumber(丑数).py: -------------------------------------------------------------------------------- 1 | """ 2 | 面试题34:丑数 3 | 题目:我们把只包含因子2、3和5的数称作丑数(Ugly Number)。求按从小到大的顺序的第1500个丑数。例如6、8都是丑数,但14不是, 4 | 因为它包含因子7。习惯上我们把1当做第一个丑数。 5 | 6 | 7 | https://leetcode.com/problems/ugly-number/ 8 | Write a program to check whether a given number is an ugly number. 9 | 10 | Ugly numbers are positive numbers whose prime factors only include 2, 3, 5. 11 | 12 | Example 1: 13 | 14 | Input: 6 15 | Output: true 16 | Explanation: 6 = 2 × 3 17 | Example 2: 18 | 19 | Input: 8 20 | Output: true 21 | Explanation: 8 = 2 × 2 × 2 22 | Example 3: 23 | 24 | Input: 14 25 | Output: false 26 | Explanation: 14 is not ugly since it includes another prime factor 7. 27 | Note: 28 | 29 | 1 is typically treated as an ugly number. 30 | Input is within the 32-bit signed integer range: [−231, 231 − 1]. 31 | 32 | 33 | 34 | https://leetcode.com/problems/ugly-number-ii/ 35 | 36 | Write a program to find the n-th ugly number. 37 | 38 | Ugly numbers are positive numbers whose prime factors only include 2, 3, 5. 39 | 40 | Example: 41 | 42 | Input: n = 10 43 | Output: 12 44 | Explanation: 1, 2, 3, 4, 5, 6, 8, 9, 10, 12 is the sequence of the first 10 ugly numbers. 45 | Note: 46 | 47 | 1 is typically treated as an ugly number. 48 | n does not exceed 1690. 49 | """ 50 | 51 | 52 | class Solution1: 53 | def isUgly(self, num): 54 | """ 55 | :type num: int 56 | :rtype: bool 57 | """ 58 | if num == 0: 59 | return False 60 | for p in (2, 3, 5): 61 | while num % p == 0: 62 | num /= p 63 | return num == 1 64 | 65 | 66 | class Solution: 67 | def nthUglyNumber(self, n): 68 | """ 69 | 思路1:自增数字并且判断是否是丑数,直到找到第 n 个。效率低 70 | 71 | 思路2:创建一个数组保存排好序的丑数。 72 | 根据其定义,丑数应该是另一个丑数乘以2,3,5的结果(1除外)。 73 | 可以创建一个数组,里边的数字是*排好序*的丑数,每一个丑数都是前边的丑数乘以2,3或5得到的。 74 | 关键在于如何保证数组里的丑数是排好序的。 75 | 76 | :type n: int 77 | :rtype: int 78 | """ 79 | idx2, idx3, idx5 = 0, 0, 0 80 | uglys = [1] 81 | beg = 1 82 | while beg < n: 83 | u2, u3, u5 = uglys[idx2] * 2, uglys[idx3]*3, uglys[idx5]*5 84 | minu = min(u2, u3, u5) 85 | uglys.append(minu) 86 | if u2 == minu: 87 | idx2 += 1 88 | if u3 == minu: 89 | idx3 += 1 90 | if u5 == minu: 91 | idx5 += 1 92 | beg += 1 93 | return uglys[-1] 94 | 95 | 96 | def test_isugly(): 97 | s = Solution1() 98 | assert s.isUgly(6) 99 | assert s.isUgly(8) 100 | assert s.isUgly(14) is False 101 | assert s.isUgly(0) is False 102 | assert s.isUgly(1) 103 | 104 | 105 | def test_nth_ugly(): 106 | s = Solution() 107 | assert s.nthUglyNumber(11) == 15 108 | assert s.nthUglyNumber(1) == 1 109 | assert s.nthUglyNumber(10) == 12 110 | 111 | 112 | if __name__ == '__main__': 113 | test_nth_ugly() 114 | -------------------------------------------------------------------------------- /剑指offer/35_FirstNotRepeatingChar(第一个只出现一次的字符).py: -------------------------------------------------------------------------------- 1 | """ 2 | 面试题35:第一个只出现一次的字符 3 | 题目:在字符串中找出第一个只出现一次的字符。如输入"abaccdeff",则输出'b'。 4 | 5 | 6 | https://leetcode.com/problems/first-unique-character-in-a-string/ 7 | 8 | Given a string, find the first non-repeating character in it and return it's index. If it doesn't exist, return -1. 9 | 10 | Examples: 11 | 12 | s = "leetcode" 13 | return 0. 14 | 15 | s = "loveleetcode", 16 | return 2. 17 | Note: You may assume the string contain only lowercase letters. 18 | """ 19 | 20 | 21 | class Solution: 22 | def firstUniqChar(self, s): 23 | """ 24 | 思路:哈希表。 25 | 对于范围大小确定的有限集合,字母可以看成是数字0-25。可以直接用数组来保存也可以 26 | 27 | 变形题目: 28 | 1. 定义一个函数,输入俩字符串,从第一个字符串中删除第二个字符串中出现过的所有字符 29 | 2. 写一个函数删除字符串中所有重复出现的字符。 30 | 3. 判断是否是变位词。两个单词字母和每个字母出现次数均相同 31 | :type s: str 32 | :rtype: int 33 | """ 34 | chars = [0] * 26 35 | for char in s: 36 | chars[ord(char) - ord('a')] += 1 37 | for idx, char in enumerate(s): 38 | _idx = ord(char) - ord('a') 39 | if chars[_idx] == 1: 40 | return idx 41 | return -1 42 | 43 | 44 | def test(): 45 | s = Solution() 46 | assert s.firstUniqChar('ll') == -1 47 | assert s.firstUniqChar('l') == 0 48 | assert s.firstUniqChar('leetcode') == 0 49 | assert s.firstUniqChar('loveleetcode') == 2 50 | 51 | 52 | test() 53 | -------------------------------------------------------------------------------- /剑指offer/36_InversePairs(数组中的逆序对).py: -------------------------------------------------------------------------------- 1 | """ 2 | 面试题36:数组中的逆序对 3 | 题目:在数组中的两个数字如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。 4 | 输入一个数组,求出这个数组中的逆序对的总数。 5 | “例如在数组{7,5,6,4}中,一共存在5个逆序对,分别是(7,6)、(7,5)、(7,4)、(6,4)和(5,4)。” 6 | 7 | https://www.lintcode.com/problem/reverse-pairs/ 8 | 9 | 思路1:扫描整个数组,寻找每个数字后边小于它的。O(n^2) 10 | 思路2:类似归并排序的思路。 11 | """ 12 | 13 | 14 | class Solution: 15 | """ 16 | @param A: an array 17 | @return: total of reverse pairs 18 | """ 19 | 20 | def reversePairs(self, A): 21 | """做归并排序的同时计数 22 | """ 23 | count = 0 24 | 25 | def merge(A, beg, end): 26 | nonlocal count 27 | if beg == end - 1: 28 | return [A[beg]] 29 | mid = (beg + end) // 2 30 | left = merge(A, beg, mid) 31 | right = merge(A, mid, end) 32 | copy = [None] * (len(left) + len(right)) 33 | i = len(left) - 1 34 | j = len(right) - 1 35 | k = len(left) + len(right) - 1 36 | while i >= 0 and j >= 0: 37 | if left[i] > right[j]: 38 | count += j + 1 39 | copy[k] = left[i] 40 | i -= 1 41 | k -= 1 42 | else: 43 | copy[k] = right[j] 44 | k -= 1 45 | j -= 1 46 | if i >= 0: 47 | copy[0:i + 1] = left[0:i + 1] 48 | if j >= 0: 49 | copy[0:j + 1] = right[0:j + 1] 50 | return copy 51 | 52 | res = merge(A, 0, len(A)) 53 | print(res, '|||||') 54 | return count 55 | 56 | 57 | def test(): 58 | s = Solution() 59 | assert s.reversePairs([2]) == 0 60 | assert s.reversePairs([2, 4, 1, 3, 5]) == 3 61 | assert s.reversePairs([2, 1]) == 1 62 | assert s.reversePairs([2]) == 0 63 | 64 | 65 | test() 66 | -------------------------------------------------------------------------------- /剑指offer/37_FirstCommonNodesInLists(两个链表的第一个公共结点).py: -------------------------------------------------------------------------------- 1 | """ 2 | 面试题37:两个链表的第一个公共结点 3 | 题目:输入两个链表,找出它们的第一个公共结点。链表结点定义如下: 4 | 5 | 6 | https://leetcode.com/problems/intersection-of-two-linked-lists/ 7 | 8 | 思路: 9 | 两个链表连接以后,之后的节点都是一样的了。 10 | 11 | 1. 使用两个栈push 所有节点,然后比较栈顶元素,如果一样就 都 pop继续比较。如果栈顶不一样,结果就是上一次 pop 的值。 12 | 13 | 2. 先分别遍历两个链表,找到各自长度,然后让一个链表先走 diff(len1-len2)步骤,之后一起往前走,找到的第一个就是。 14 | 15 | """ 16 | 17 | # Definition for singly-linked list. 18 | 19 | 20 | class Node(object): 21 | def __init__(self, x, next=None): 22 | self.val = x 23 | self.next = next 24 | 25 | 26 | class _Solution(object): 27 | def getIntersectionNode(self, headA, headB): 28 | """ 29 | :type head1, head1: ListNode 30 | :rtype: ListNode 31 | """ 32 | if headA is None or headB is None or (headA is None and headB is None): 33 | return None 34 | len1 = 0 35 | cura = headA 36 | while cura: 37 | len1 += 1 38 | cura = cura.next 39 | 40 | len2 = 0 41 | curb = headB 42 | while curb: 43 | len2 += 1 44 | curb = curb.next 45 | 46 | difflen = abs(len1 - len2) 47 | if len1 > len2: 48 | for i in range(difflen): 49 | headA = headA.next 50 | else: 51 | for i in range(difflen): 52 | headB = headB.next 53 | 54 | while headA and headB: 55 | if headA == headB: # headA.val == headB.val and headA.next == headB.next 56 | return headA 57 | headA = headA.next 58 | headB = headB.next 59 | 60 | return None 61 | 62 | 63 | class Solution(object): 64 | def getIntersectionNode(self, headA, headB): 65 | """ 66 | :type head1, head1: ListNode 67 | :rtype: ListNode 68 | """ 69 | if headA is None or headB is None: 70 | return None 71 | 72 | len1 = 0 73 | cura = headA 74 | while cura: 75 | len1 += 1 76 | cura = cura.next 77 | 78 | len2 = 0 79 | curb = headB 80 | while curb: 81 | len2 += 1 82 | curb = curb.next 83 | 84 | difflen = abs(len1 - len2) 85 | if len1 > len2: 86 | for i in range(difflen): 87 | headA = headA.next 88 | else: 89 | for i in range(difflen): 90 | headB = headB.next 91 | 92 | while headA and headB: 93 | if headA == headB: # headA.val == headB.val and headA.next == headB.next 94 | return headA 95 | headA = headA.next 96 | headB = headB.next 97 | 98 | return None 99 | -------------------------------------------------------------------------------- /剑指offer/38_NumberOfK(数字在排序数组中出现的次数).py: -------------------------------------------------------------------------------- 1 | """ 2 | 面试题38:数字在排序数组中出现的次数 3 | 题目:统计一个数字在排序数组中出现的次数。例如输入排序数组{1,2,3,3,3,3,4,5}和数字3,由于3在这个数组中出现了4次,因此输出4 4 | 5 | 6 | https://leetcode.com/problems/find-first-and-last-position-of-element-in-sorted-array/ 7 | 8 | Given an array of integers nums sorted in ascending order, find the starting and ending position of a given target value. 9 | 10 | Your algorithm's runtime complexity must be in the order of O(log n). 11 | 12 | If the target is not found in the array, return [-1, -1]. 13 | 14 | Example 1: 15 | 16 | Input: nums = [5,7,7,8,8,10], target = 8 17 | Output: [3,4] 18 | Example 2: 19 | 20 | Input: nums = [5,7,7,8,8,10], target = 6 21 | Output: [-1,-1] 22 | """ 23 | 24 | 25 | def find_left(array, beg, end, target): 26 | while beg < end: 27 | mid = (beg + end) // 2 28 | if target > array[mid]: 29 | beg = mid + 1 30 | else: 31 | end = mid 32 | if beg != len(array) and array[beg] == target: 33 | return beg 34 | return -1 35 | 36 | 37 | def find_right(nums, beg, end, target): 38 | while beg < end: 39 | mid = (beg + end) >> 1 40 | if target >= nums[mid]: # 条件是>=,找到之后beg会在 target 后一个位置 41 | beg = mid + 1 42 | else: 43 | end = mid 44 | before = beg - 1 45 | if before != len(nums) and nums[before] == target: 46 | return before 47 | return -1 48 | 49 | 50 | class Solution(object): 51 | def searchRange(self, nums, target): 52 | """ 53 | :type nums: List[int] 54 | :type target: int 55 | :rtype: List[int] 56 | """ 57 | if not nums: 58 | return [-1, -1] 59 | l = find_left(nums, 0, len(nums), target) 60 | r = find_right(nums, 0, len(nums), target) 61 | return [l, r] 62 | 63 | 64 | def test_left(): 65 | nums = [5, 7, 7, 8, 8, 10] 66 | assert find_left(nums, 0, len(nums), 5) == 0 67 | assert find_left(nums, 0, len(nums), 10) == 5 68 | assert find_left(nums, 0, len(nums), 8) == 3 69 | assert find_left(nums, 0, len(nums), 9) == -1 70 | assert find_left(nums, 0, len(nums), 4) == -1 71 | 72 | 73 | def test_right(): 74 | nums = [5, 7, 7, 8, 8, 10] 75 | assert find_right(nums, 0, len(nums), 5) == 0 76 | assert find_right(nums, 0, len(nums), 10) == 5 77 | assert find_right(nums, 0, len(nums), 11) == -1 78 | assert find_right(nums, 0, len(nums), 8) == 4 79 | 80 | 81 | def test(): 82 | s = Solution() 83 | assert s.searchRange([5, 7, 7, 8, 8, 10], 8) == [3, 4] 84 | assert s.searchRange([5, 7, 7, 8, 8, 10], 6) == [-1, -1] 85 | 86 | 87 | test_left() 88 | test_right() 89 | -------------------------------------------------------------------------------- /剑指offer/39_1_TreeDepth(二叉树深度).py: -------------------------------------------------------------------------------- 1 | """ 2 | 面试题39:二叉树的深度 3 | 题目一:输入一棵二叉树的根结点,求该树的深度。从根结点到叶结点依次经过的 4 | 结点(含根、叶结点)形成树的一条路径,最长路径的长度为树的深度。 5 | 6 | 7 | 8 | https://leetcode.com/problems/maximum-depth-of-binary-tree/ 9 | 10 | 11 | 12 | Given a binary tree, determine if it is height-balanced. 13 | 14 | For this problem, a height-balanced binary tree is defined as: 15 | 16 | a binary tree in which the depth of the two subtrees of every node never differ by more than 1. 17 | 18 | Example 1: 19 | 20 | Given the following tree [3,9,20,null,null,15,7]: 21 | 22 | 3 23 | / \ 24 | 9 20 25 | / \ 26 | 15 7 27 | Return true. 28 | 29 | Example 2: 30 | 31 | Given the following tree [1,2,2,3,3,null,null,4,4]: 32 | 33 | 1 34 | / \ 35 | 2 2 36 | / \ 37 | 3 3 38 | / \ 39 | 4 4 40 | Return false. 41 | """ 42 | 43 | 44 | # Definition for a binary tree node. 45 | # class TreeNode: 46 | # def __init__(self, x): 47 | # self.val = x 48 | # self.left = None 49 | # self.right = None 50 | 51 | class Solution1: 52 | def maxDepth(self, root): 53 | """ 54 | :type root: TreeNode 55 | :rtype: int 56 | """ 57 | if not root: 58 | return 0 59 | left = self.maxDepth(root.left) 60 | right = self.maxDepth(root.right) 61 | if left > right: 62 | return left + 1 63 | else: 64 | return right + 1 65 | 66 | 67 | # Definition for a binary tree node. 68 | # class TreeNode: 69 | # def __init__(self, x): 70 | # self.val = x 71 | # self.left = None 72 | # self.right = None 73 | 74 | def maxDepth(root): 75 | """ 76 | :type root: TreeNode 77 | :rtype: int 78 | """ 79 | if not root: 80 | return 0 81 | left = maxDepth(root.left) 82 | right = maxDepth(root.right) 83 | if left > right: 84 | return left + 1 85 | else: 86 | return right + 1 87 | 88 | 89 | class Solution: 90 | def isBalanced(self, root): 91 | """ 92 | :type root: TreeNode 93 | :rtype: bool 94 | """ 95 | if not root: 96 | return True 97 | leftd = maxDepth(root.left) 98 | rightd = maxDepth(root.right) 99 | if abs(leftd-rightd) > 1: 100 | return False 101 | return self.isBalanced(root.left) and self.isBalanced(root.right) 102 | -------------------------------------------------------------------------------- /剑指offer/40_NumbersAppearOnce(数组中只出现一次的数字).py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | 题目:一个整型数组里除了两个数字之外,其他的数字都出现了两次。请写程序找出这两个只出现一次的数字。要求时间复杂度是O(n), 4 | 空间复杂度是O(1)。 5 | 6 | 我们还是从头到尾依次异或数组中的每一个数字,那么最终得到的结果就是两个只出现一次的数字的异或结果。因为其他数字都出现了两次, 7 | 在异或中全部抵消了。由于这两个数字肯定不一样,那么异或的结果肯定不为0,也就是说在这个结果数字的二进制表示中至少就有一位为1。 8 | 我们在结果数字中找到第一个为1的位的位置,记为第n位。现在我们以第n位是不是1为标准把原数组中的数字分成两个子数组 9 | ,第一个子数组中每个数字的第n位都是1,而第二个子数组中每个数字的第n位都是0。由于我们分组的标准是数字中的某一位是1还是0, 10 | 那么出现了两次的数字肯定被分配到同一个子数组。因为两个相同的数字的任意一位都是相同的,我们不可能把两个相同的数字分配到两个子数组中去, 11 | 于是我们已经把原数组分成了两个子数组,每个子数组都包含一个只出现一次的数字,而其他数字都出现了两次。 12 | 我们已经知道如何在数组中找出唯一一个只出现一次数字,因此到此为止所有的问题都已经解决了。 13 | 14 | 15 | 16 | https://leetcode.com/problems/single-number-iii/ 17 | 18 | Given an array of numbers nums, in which exactly two elements appear only once and all the other elements appear exactly twice. Find the two elements that appear only once. 19 | 20 | Example: 21 | 22 | Input: [1,2,1,3,2,5] 23 | Output: [3,5] 24 | Note: 25 | 26 | The order of the result is not important. So in the above example, [5, 3] is also correct. 27 | Your algorithm should run in linear runtime complexity. Could you implement it using only constant space complexity? 28 | """ 29 | 30 | 31 | class Solution1: 32 | def singleNumber(self, nums): 33 | """ 34 | 类似题:求只出现一次的数字,其他都出现两次。 35 | :type nums: List[int] 36 | :rtype: int 37 | """ 38 | first = 0 39 | for num in nums: 40 | first ^= num 41 | return first 42 | 43 | 44 | class Solution: 45 | def singleNumber(self, nums): 46 | """ 47 | :type nums: List[int] 48 | :rtype: List[int] 49 | """ 50 | def get_single_num(nums): 51 | first = 0 52 | for num in nums: 53 | first ^= num 54 | return first 55 | 56 | single = get_single_num(nums) 57 | print(single) 58 | mask = 1 59 | while single & mask == 0: 60 | mask = mask << 1 61 | 62 | print(mask, '||||||||||||||||||') 63 | left = [i for i in nums if i & mask] 64 | right = [i for i in nums if not(i & mask)] 65 | return [get_single_num(left), get_single_num(right)] 66 | 67 | 68 | class Solution(object): 69 | def singleNumber(self, nums): 70 | """ 71 | 思路:异或。 72 | 73 | 136 题做过只出现一次的一个数字,本题有两个只出现一次的数字。 74 | 核心在于把数组分成两个。怎么分组呢? 75 | 76 | 假设只出现一次的数字式 x1,x2,所有元素异或结果是 x。一定有 x=x1^x2。 77 | x不能是0,否则x1==x2,就不是只出现一次的数字了。 78 | *可以用位运算 x&-x 取出x 二进制位最低位的 1,设其实是第 L 位置。* 79 | 可以据此分类数组,一组是二进制第 L位置为 0 的数字,一组L 位置为 1的数字。 80 | x1,x2会分别出现在两个组中。这样第一组全部异或得到x1, 第二组全部异或得到 x2 81 | 82 | :type nums: List[int] 83 | :rtype: List[int] 84 | """ 85 | x = 0 86 | for num in nums: 87 | x ^= num 88 | 89 | low1pos = x & -x # 这里也可以用移位运算,x&-x 不太好想,类似上边那个解法 90 | x1, x2 = 0, 0 91 | 92 | for num in nums: 93 | if num & low1pos: 94 | x1 ^= num 95 | else: 96 | x2 ^= num 97 | 98 | return x1, x2 99 | 100 | 101 | def test(): 102 | s = Solution() 103 | assert s.singleNumber([1, 2, 1, 3, 2, 5]) == [3, 5] 104 | 105 | 106 | test() 107 | -------------------------------------------------------------------------------- /剑指offer/41_1_TwoNumbersWithSum(和为s的两个数字VS和为s的连续正数序列).py: -------------------------------------------------------------------------------- 1 | """ 2 | 2-sum问题 3 | 4 | 面试题41:和为s的两个数字VS和为s的连续正数序列 5 | 题目一:输入一个递增排序的数组和一个数字s,在数组中查找两个数,使得它们的和正好是s。如果有多对数字的和等于s, 6 | 输出任意一对即可。 7 | """ 8 | 9 | 10 | class Solution: 11 | def twoSum(self, nums, target): 12 | """ 注意这题目 letcode 不是有序的,剑指offer 上的是有序的。一种方式是用 hash 来做 13 | :type nums: List[int] 14 | :type target: int 15 | :rtype: List[int] 16 | """ 17 | num_idx_map = {} 18 | for idx, num in enumerate(nums): 19 | diff = target - num 20 | if diff in num_idx_map: 21 | return [num_idx_map[diff], idx] 22 | else: 23 | num_idx_map[num] = idx 24 | 25 | def twoSum1(self, nums, target): 26 | """ 注意这题目 letcode 不是有序的,剑指offer 上的是有序的。可以先排序来做。之后首位指针向中间归并 27 | :type nums: List[int] 28 | :type target: int 29 | :rtype: List[int] 30 | """ 31 | pairs = [(num, i) for i, num in enumerate(nums)] 32 | nums = sorted(pairs) 33 | print(nums) 34 | beg, end = 0, len(nums) - 1 35 | while beg < end: 36 | sum2 = nums[beg][0] + nums[end][0] 37 | if sum2 == target: 38 | break 39 | elif sum2 > target: 40 | end -= 1 41 | else: 42 | beg += 1 43 | return [nums[beg][1], nums[end][1]] 44 | 45 | 46 | def test(): 47 | s = Solution() 48 | assert s.twoSum([3, 2, 4], 6) == [1, 2] 49 | assert s.twoSum([2, 7], 9) == [0, 1] 50 | assert s.twoSum([2, 7, 11, 15], 9) == [0, 1] 51 | assert s.twoSum([1, 2, 4, 7, 11, 15], 15) == [2, 4] 52 | -------------------------------------------------------------------------------- /剑指offer/README.md: -------------------------------------------------------------------------------- 1 | # 《剑指offer》题解 2 | 3 | 《剑指offer》是一本经典的面试算法书籍,包含了大量常见笔试和面试题,笔者这里编写和练习了大部分题目的题解提供大家参考。 4 | 如果可以搜到对应的 leetcode 题目,笔者会在代码注释里添加链接,方便读者直接找到对应的题目进行练习。 5 | 6 | 你也可以根据关键词搜索到 《剑指offer》上对应的 leetcode 题目,尝试自己思考并且提交一下,看看能否跑通所有测试用例。 7 | 8 | # 勘误 9 | 10 | 如果您发现缺失的题目或者忘记附上 leetcode 链接,可以直接提交 MR 笔者合并进去。如果有错误也欢迎批评指正,及时修复。 11 | 12 | # leetcode 13 | 14 | 目前 leetcode 中文官方网站已经得到授权,不用辛苦找题目,可以直接在这个题库链接找到大部分题目练习: 15 | 16 | https://leetcode-cn.com/problem-list/xb9nqhhg/ 17 | --------------------------------------------------------------------------------