什么是数据结构?什么是算法?python
数据结构和算法是相辅相成的,数据结构是为了算法服务的,算法要做用在特定的数据结构之上。所以,咱们没法孤立数据结构来说算法,也没法孤立算法来说数据结构。算法
复杂度分析数据库
经常使用的数据结构和算法编程
事半功倍的学习技巧数组
为何须要复杂度分析?缓存
经过实际的代码运行来统计运行效率的方法叫作是过后统计法,这种方法存在以下以下问题:安全
因此,咱们须要一个不用具体的测试数据来测试,能够粗略地估计算法的执行效率的方法,这就是 时间、空间复杂度分析方法。bash
公式:T(n) = O(f(n))数据结构
这种复杂度表示方法只是表示一种变化趋势,当 n 很大时,公式中的低阶、常量、系数三部分并不左右增加趋势,因此能够忽略。多线程
示例代码 01
int cal(int n){ int sum = 0 int i = 1; for(;i<=n;i++){ sum = sum + i; } }
假设每行代码执行的时间都同样,为 unit_time,那么上述代码总的执行时间为:(2n+2)*unit_time,大 O 表示法为:T(n) = O(2n+2),当 n 很大时,可记为 T(n) = O(n)
示例代码 02
int cal(int n){ int sum = 0; int i = 1; int j = 1; for(;i<=n;++i){ j = 1; for(;<=n;++j){ sum = sum + i*j } } }
假设每行代码执行的时间都同样,为 unit_time,那么上述代码总的执行时间为:(2n2+2n+3)*unit_time, 大 O 表示法为:T(n) = O(2n2+2n+3), 当 n 很大时,可记为 T(n) = O(n2)
渐进时间复杂度
对于上述罗列的复杂度量级,能够粗略地分为两类:多项式量级和非多项式量级。其中,非多项式量级只有两个:O(2n) 和 O(n!)。当数据规模 n 愈来愈大时,非多项式量级算法的执行时间会急剧增长,求解问题的执行时间会无线增加。苏欧阳,非多项式时间复杂度的算法实际上是效率很是低的算法。
渐进空间复杂度
表示算法的存储空间与数据规模之间的增加关系,常见的空间复杂度以下:
是一种线性表数据结构,用一组连续的内存空间来存储一组具备相同类型的数据。
使用建议:
为何在大多数的编程语言中,数组要从 0 开发编号,而不是 1 ?
从数组存储的内存模型上来看,下标 最确切的定义应该是 偏移(offset),这样就能确保正确计算出每次随机访问的元素对于的内存地址,这样就好理解了。
是一种线性数据结构,用一组非连续的内存空间来存储一组具备相同类型的数据。
数组 VS 链表 时间复杂度比较:
数组 | 链表 | |
---|---|---|
插入、删除 | O(n) | O(1) |
随机访问 | O(1) | O(n) |
常见的链表类型:
缓存策略常有以下三种方式:
如何基于链表实现 LRU 缓存淘汰算法?
思路:维护一个有序单链表,越靠近链表尾部的结点是越早以前访问,当有一个新的数据被访问时,从链表头开始顺序遍历单链表。
若是此数据没有在缓存链表中,又能够分为两种状况:
时间复杂度为:O(n)
5 种常见的链表操做
当某个数据集合只涉及在一端插入和删除数据,而且知足后进先出、先进后出的特性,咱们就应该首选 栈 这种数据结构
无论是顺序栈仍是链式栈,入栈、出栈只涉及栈顶个别数据的操做,全部时间复杂度都是 O(1)。栈是一种操做受限的数据结构,只支持入栈和出栈操做。后进先出是它最大的特色。栈既能够经过数组实现,也能够经过链表实现。
内存中的堆栈和数据结构中的堆栈不是一个概念,内存中的堆栈是真实存在的物理区,数据结构中的堆栈是抽象出来的数据存储结构:
内存空间在逻辑上分为三部分:
先进者先出
无论是顺序队列仍是链式队列,主要的两个操做是入队和出队,最大特色是先进先出。
几种高级的队列结构:
## 10 递归
递归须要知足的三个条件:
如何编写递归代码?
缺点:
常见排序算法:
排序算法 | 时间复杂度 | 是否基于比较 |
---|---|---|
冒泡、插入、选择 | O(n2) | 是 |
快排、归并 | O(nlogn) | 是 |
桶、计数、基数 | O(n) | 否 |
如何分析一个 “排序算法”?
冒泡排序只会操做相邻的两个数据。每次冒泡操做都会对相邻的两个元素进行比较,看是否知足大小关系要求。若是不知足就让它俩互换。一次冒泡会让至少一 个元素移动到它应该在的位置,重复n次,就完成了n个数据的排序工做。
示例代码:
class Solution(): def bubbleSort(self, lis: list, n: int): if n <= 1: return for i in range(len(lis)): flag = False for j in range(len(lis)-i-1): if lis[j] > lis[j+1]: lis[j], lis[j+1] = lis[j+1], lis[j] flag = True if not flag: break arr = [4, 5, 6, 3, 2, 1] print(arr) Solution().bubbleSort(arr, len(arr)) print(arr)
插入算法的核心思想是取未排序区间中的元素,在已排序区间中找到合适的插入位置将其插入,并保证已排序区间数据一直有序。重复这个过程,直到未排序区间中元素为空,算法结束。
示例代码:
class Solution(): def insertionSort(self, lis: list, n: int): if n <= 1: return for i in range(1, len(lis)): val = lis[i] j = i-1 while j >= 0: if lis[j] > val: lis[j+1] = lis[j] j -= 1 lis[j+1] = val attr = [4, 5, 6, 3, 2, 1] print(attr) Solution().insertionSort(attr, len(attr)) print(attr)
选择排序算法的实现思路有点相似插入排序,也分已排序区间和未排序区间。可是选择排序每次会从未排序区间中找到最小的元素,将其放到已排序区间的末 尾。
示例代码:
class Solution(): def selectSort(self, lis: list, n: int): if n <= 1: return for i in range(0, len(lis) - 1): index = i for j in range(i+1, len(lis)): if lis[index] > lis[j]: index = j lis[i], lis[index] = lis[index], lis[i] attr = [4, 5, 6, 3, 2, 1] print(attr) Solution().selectSort(attr, len(attr)) print(attr)
是否原地排序 | 是否稳定 | 最好 | 最坏 | 平均 | |
---|---|---|---|---|---|
冒泡 | 是 | 是 | O(n) | O(n2) | O(n2) |
插入 | 是 | 是 | O(n) | O(n2) | O(n2) |
选择 | 是 | 否 | O(n2) | O(n2) | O(n2) |
核心思想:利用分而治之的思想,递归解决问题。若是要排序一个数组,咱们先把数组从中间分红先后两部分,而后对先后两部分分别排序,再将排好序的两部分合并在一 起,这样整个数组就都有序了。
示例代码:
class Solution(): def mergeSort(self, arr): print("Splitting ", arr) if len(arr) > 1: mid = len(arr)//2 lefthalf = arr[:mid] righthalf = arr[mid:] self.mergeSort(lefthalf) self.mergeSort(righthalf) i = 0 j = 0 k = 0 while i < len(lefthalf) and j < len(righthalf): if lefthalf[i] < righthalf[j]: arr[k] = lefthalf[i] i = i+1 else: arr[k] = righthalf[j] j = j+1 k = k+1 while i < len(lefthalf): arr[k] = lefthalf[i] i = i+1 k = k+1 while j < len(righthalf): arr[k] = righthalf[j] j = j+1 k = k+1 print("Merging ", arr) arr = [4, 5, 6, 3, 2, 1] print(arr) Solution().mergeSort(arr) print(arr)
性能分析:
快排核心思想就是分治和分区。若是要排序数组中下标从p到r之间的一组数据,咱们选择p到r之间的任意一个数据做为pivot(分区点)。 咱们遍历p到r之间的数据,将小于pivot的放到左边,将大于pivot的放到右边,将pivot放到中间。通过这一步骤以后,数组p到r之间的数据就被分红了三个部分,前 面p到q-1之间都是小于pivot的,中间是pivot,后面的q+1到r之间是大于pivot的。
示例代码:
class Solution(): def quickSort(self, arr: list): self.quickHelper(arr, 0, len(arr)-1) def quickHelper(self, arr: list, first: int, last: int): if first < last: splitpoint = self.partition(arr, first, last) self.quickHelper(arr, first, splitpoint-1) self.quickHelper(arr, splitpoint+1, last) def partition(self, arr: list, first: int, last: int): pivot = arr[first] left = first + 1 right = last done = False while not done: while left <= right and arr[left] <= pivot: left = left + 1 while arr[right] >= pivot and right >= left: right = right - 1 if right < left: done = True else: temp = arr[left] arr[left] = arr[right] arr[right] = temp temp = arr[first] arr[first] = arr[right] arr[right] = temp return right arr = [4, 5, 6, 3, 2, 1] print(arr) Solution().quickSort(arr) print(arr)
性能分析:
可是,公式成立的前提是每次分区操做,咱们选择的pivot都很合适,正好能将大区间对等地一分为二。但实际上这种状况是很难实现的
核心思想是将要排序的数据分到几个有序的桶里,每一个桶里的数据再单独进行排序。桶内排完序之 后,再把每一个桶里的数据按照顺序依次取出,组成的序列就是有序的了。
桶排序比较适合用在外部排序中。所谓的外部排序就是数据存储在外部磁盘中,数据量比较大,内存有限,没法将数据所有加载到内存中。
计数排序实际上是桶排序的一种特殊状况。当要排序的n个数据,所处的范围并不大的时候,好比最大值是k,咱们就能够把数据划分红k个桶。每一个桶 内的数据值都是相同的,省掉了桶内排序的时间。
示例代码:
class Solution: def countingSort(self, arr: list, n: int): if n <= 1: return mv = arr[0] for v in arr: if mv < v: mv = v c = [0 for x in range(mv+1)] for i in range(n): c[arr[i]] += 1 for i in range(1, mv+1): c[i] = c[i-1] + c[i] r = [0 for x in range(n)] i = n-1 while i >= 0: index = c[arr[i]] - 1 r[index] = arr[i] c[arr[i]] -= 1 i -= 1 for i in range(n): arr[i] = r[i] arr = [4, 5, 6, 3, 2, 1] print(arr) Solution().countingSort(arr, len(arr)) print(arr)
计数排序只能用在数据范围不大的场景中,若是数据范围 k 比要排序的数据 n 大不少,就不适合用计数排序了。并且,计数排序只能给非负整数排序,若是要排序的数据是其余类型的,要将其在不改变相对大小的状况下,转化为非负整数。
基数排序对要排序的数据是有要求的,须要能够分割出独立的“位”来比较,并且位之间有递进的关系,若是a数据的高位比b数据大,那剩下的低 位就不用比较了。除此以外,每一位的数据范围不能太大,要能够用线性排序算法来排序,不然,基数排序的时间复杂度就没法作到O(n)了。
时间复杂度 | 是否稳定排序 | 是否原地排序 | |
---|---|---|---|
冒泡排序 | O(n2) | 是 | 是 |
插入排序 | O(n2) | 是 | 是 |
选择排序 | O(n2) | 否 | 是 |
快速排序 | O(nlog2) | 否 | 是 |
归并排序 | O(nlog2) | 是 | 否 |
计数排序 | O(n+k) k是数据范围 | 是 | 否 |
桶排序 | O(n) | 是 | 否 |
基数排序 | O(dn) d 是维度 | 是 | 否 |
如何优化快速排序?
二分查找(Binary Search)算法,也叫折半查找算法。时间复杂度为 O(longn)
示例代码:
class Solution: def bsearch(self, arr: list, n: int, val: int): return self.bsearchInternally(arr, 0, n-1, val) def bsearchInternally(self, arr: list, low: int, high: int, val: int): if low > high: return -1 mid = low + ((high-low) >> 1) if arr[mid] == val: return mid elif arr[mid] < val: return self.bsearchInternally(arr, mid+1, high, val) else: return self.bsearchInternally(arr, low, mid-1, val) arr = [1, 2, 3, 4, 2, 2, 3, 5] v = Solution().bsearch(arr, len(arr), 4) print(v)
class Solution: def bsearch(self, arr: list, n: int, val: int): low = 0 high = n - 1 while low <= high: mid = (low+high) // 2 if arr[mid] == val: return mid elif arr[mid] < val: low = mid + 1 else: high = mid - 1 return -1 arr = [1, 2, 3, 4, 2, 2, 3, 5] v = Solution().bsearch(arr, len(arr), 4) print(v)
应用场景的局限性:
二分查找的变形问题:
示例代码:
class Solution: def bsearch(self, arr: list, n: int, val: int): low = 0 high = n-1 while low <= high: mid = low + ((high-low) >> 1) if arr[mid] > val: high = mid - 1 elif arr[mid] < val: low = mid + 1 else: if mid == 0 or arr[mid-1] != val: return mid else: high = mid - 1 return -1 arr = [1, 2, 3, 4, 2, 2, 3, 5] v = Solution().bsearch(arr, len(arr), 4) print(v)
示例代码:
# 待修改 class Solution: def bsearch(self, arr: list, n: int, val: int): low, high = 0, n-1 while low <= high: mid = low + ((high-low) >> 1) if arr[mid] > val: high = mid - 1 elif arr[mid] < val: low = mid + 1 else: if mid == n-1 or arr[mid+1] != val: return mid else: low = mid + 1 return -1 arr = [1, 2, 3, 4, 2, 2, 3, 5] v = Solution().bsearch(arr, len(arr), 3) print(v)
示例代码:
# 待修改 class Solution: def bsearch(self, arr: list, n: int, val: int): low, high = 0, n-1 while low <= high: mid = low + ((high-low) >> 1) if arr[mid] >= val: if mid == 0 or arr[mid - 1] < val: return mid else: high = mid-1 else: low = mid + 1 return -1 arr = [1, 2, 3, 4, 2, 2, 3, 5] v = Solution().bsearch(arr, len(arr), 3) print(v)
示例代码:
# 待修改 class Solution: def bsearch(self, arr: list, n: int, val: int): low, high = 0, n-1 while low <= high: mid = low + ((high-low) >> 1) if arr[mid] > val: high = mid - 1 else: if mid == n - 1 or arr[mid + 1] > val: return mid else: low = mid + 1 return -1 arr = [1, 2, 3, 4, 2, 2, 3, 5] v = Solution().bsearch(arr, len(arr), 3) print(v)
Redis 的有序集合就是使用跳表来实现的。
跳表使用空间换时间的设计思路,经过后见多级索引来提升查询订单效率,实现了基于链表的 “二分查找”。调表是一种动态结构,支持快速的插入、删除、查找操做,时间复杂度都是 O(longn)
跳表的空间复杂度是 O(n),不过,跳表的实现很是灵活,能够经过改变索引构建策略,有效平衡执行效率和内存消耗。虽然跳表的代码实现起来并不简单,可是做为一种动态结构,比起红黑树来讲,实现要简单不少。因此不少时候,咱们为了代码的简单、易读,比起红黑树,咱们更倾向用跳表。
Word 文档中的单词拼写检查功能
散列表是由数组演化而来的,借助散列函数堆数组进行扩展,利用的是数组支持按照下标随机访问元素的特性。
散列冲突的解决方法:
散列表的查询效率不能笼统地说成是 O(1),它跟散列函数、装载因子、散列冲突等都有关系。若是散列函数涉及得很差,或者装载因子太高,均可能致使散列冲突发生的几率升高,查询效率降低。
如何设计散列函数?
直接寻址法、平方取中法、折叠法、随机数法等
装载因子过大怎么办?
装载因子阈值的设置要权衡时间、空间复杂度。若是内存空间没关系,对执行效率要求很高,能够下降负载因子的阀值;相反,若是内存空间紧张,对执行效率要求又不高,能够增长负载因子的值,甚至能够大于 1。
如何避免低效地扩容?
经过均摊的方法,将一次性扩容的代价,均摊到屡次插入操做中,就避免了一次性扩容耗时过多的状况。这种实现方式,任何状况下,插入一个数据的时间 复杂度都是O(1)。
工业级散列表分析要素:
工业级散列表特征:
工业级散列表设计思路:
将任意长度的二进制值串映射为固定长度的二进制值串,这个映射的规则就是哈希算法,而 经过原始数据映射以后获得的二进制值串就是哈希值。
知足以下几点要求:
应用场景:
想要存储一棵二叉树,咱们有两种方法,一种是基于指针或者引用的二叉链式存储法,一种是基于数组的顺序存储法。
二叉树的遍历:
实际上,二叉树的前、中、后序遍历就是一个递归的过程。
二叉查找树
二叉查找树是二叉树中最经常使用的一种类型,也叫二叉搜索树。顾名思义,二叉查找树是为了实现快速查找而生的。不过,它不只仅支持快速查找一个数据,还支 持快速插入、删除一个数据。
二叉查找树要求,在树中的任意一个节点,其左子树中的每一个节点的值,都要小于这个节点的值,而右子树节点的值都大 于这个节点的值。
知足要求:
红黑树是一种平衡二叉查找树,它是为了解决普通二叉查找树在数据更新的过程当中,复杂度退化的问题而产生的,红黑树的高度近似 log2n,因此它是近似平衡,插入、删除、查找操做的时间复杂度都是 O(logn)。
由于红黑树是一种性能很是稳定的二叉查找树,因此,在工程中,但凡是用到动态插入、删除、查找数据的场景,均可以用到它。不过,它实现起来比较复杂,若是本身写代码实现,难度会有些高,这个时候,咱们其实更倾向用跳表来代替它。
堆的特色:
对于每一个节点值都大于等于子树中每一个节点值的堆,咱们叫作 “大顶堆”;对于每一个节点的值都小于等于子树中每一个节点值的堆,咱们叫作 “小顶堆”。
为何快速排序要比堆排序性能好?
堆的应用:
非线性数据结构
相关概念:
存储方法:
邻接矩阵存储方法的缺点是比较浪费空间,可是优势是查询效率高,并且方便矩阵运算。邻接表存储方法中每一个顶点都对应一个链表,存储与其相链接的其余顶 点。尽管邻接表的存储方式比较节省存储空间,但链表不方便查找,因此查询效率没有邻接矩阵存储方式高。针对这个问题,邻接表还有改进升级版,即将链表换成更加高效的动态数据结构,好比平衡二叉查找树、跳表、散列表等。
搜索方法:
广度优先搜索和深度优先搜索是图上的两种最经常使用、最基本的搜索算法,比起其余高级的搜索算法,好比A、IDA等,要简单粗暴,没有什么优化,因此,也被 叫做暴力搜索算法。因此,这两种搜索算法仅适用于状态空间不大,也就是说图不大的搜索。 广度优先搜索,通俗的理解就是,地毯式层层推动,从起始顶点开始,依次往外遍历。广度优先搜索须要借助队列来实现,遍历获得的路径就是,起始顶点到终 止顶点的最短路径。深度优先搜索用的是回溯思想,很是适合用递归实现。换种说法,深度优先搜索是借助栈来实现的。在执行效率方面,深度优先和广度优先搜索的时间复杂度都是O(E),空间复杂度是O(V)。
匹配算法
BF 算法
全称叫 Brute Force 算法,中文叫做暴力匹配算法,也叫朴素匹配算法。
RK 算法
全称叫 Rabin-Karp 算法,是 BF 算法的改进版。
BM 算法
全称叫 Boyer-Moore 算法。是一种很是搞笑的字符串匹配算法。
BM 算法核心思想是,利用模式串自己的特色,在模式串中某个字符与主串不能匹配的时候,将模式串日后多滑动几位,以此来减小没必要要的字符比较,提升匹配的效率。BM算法构建的规则有两类,坏字符规则和好后缀规则。好后缀规则能够独立于坏字符规则使用。由于坏字符规则的实现比较耗内存,为了节省内存,咱们能够只用好后缀规则来实现 BM 算法。
MKP 算法
KMP算法的核心思想是:咱们假设主串是a,模式串是b。在模式串与主串匹配的过程当中,当遇到不可匹配的字符的时候,咱们但愿找到一些规律,能够将模式串日后多滑动几位,跳过那些确定不会匹配的状况。
BM算法有两个规则,坏字符和好后缀。KMP算法借鉴BM算法的思想,能够总结成好前缀规则。这里面最难懂的就是next数组的计算。若是用最笨的方法来计 算,确实不难,可是效率会比较低。因此,我讲了一种相似动态规划的方法,按照下标i从小到大,依次计算next[i],而且next[i]的计算经过前面已经计算出来 的next[0],next[1],……,next[i-1]来推导。 KMP算法的时间复杂度是O(n+m)。
Trie树,也叫“字典树”。顾名思义,它是一个树形结构。它是一种专门处理字符串匹配的数据结构,用来解决在一组字符串集合中快速查找某个字符串的问题。
若是用来构建Trie树的这一组字符串中,前缀重复的状况不是不少,那Trie树这种数 据结构整体上来说是比较费内存的,是一种空间换时间的解决问题思路。
尽管比较耗费内存,可是对内存不敏感或者内存消耗在接受范围内的状况下,在Trie树中作字符串匹配仍是很是高效的,时间复杂度是O(k),k表示要匹配的字符串的长度。 可是,Trie树的优点并不在于,用它来作动态集合数据的查找,由于,这个工做彻底能够用更加合适的散列表或者红黑树来替代。Trie树最有优点的是查找前缀匹配的字符 串,好比搜索引擎中的关键词提示功能这个场景,就比较适合用它来解决,也是Trie树比较经典的应用场景。
AC自动机是基于Trie树的一种改进算法,它跟Trie树的关系,就像单模式串中,KMP算法与BF算法的关系同样。KMP算法中有一个很是关键的next数组,类比 到AC自动机中就是失败指针。并且,AC自动机失败指针的构建过程,跟KMP算法中计算next数组极其类似。因此,要理解AC自动机,最好先掌握KMP算法,由于AC自动机其实就是KMP算法在多模式串上的改造。
整个AC自动机算法包含两个部分,第一部分是将多个模式串构建成AC自动机,第二部分是在AC自动机中匹配主串。第一部分又分为两个小的步骤,一个是将模 式串构建成Trie树,另外一个是在Trie树上构建失败指针。
贪心算法有不少经典的应用,好比霍夫曼编码(Huffman Coding)、Prim和Kruskal最小生成树算法、还 有Dijkstra单源最短路径算法。
实际上,贪心算法适用的场景比较有限。这种算法思想更多的是指导设计基础算法。好比最小生成树算法、单源最短路径算法,这些算法都用到了贪心算法。
分治算法(divide and conquer)的核心思想其实就是四个字,分而治之 ,也就是将原问题划分红n个规模较小,而且结构与原问题类似的子问题,递归地解决这些 子问题,而后再合并其结果,就获得原问题的解。
分治算法是一种处理问题的思想,递归是一种编程技巧。实际上,分治算法通常都比较适合用递归来实现。分治算法的递归实现中,每一层递归都会涉及这样三个操做:
分治算法能解决的问题,通常须要知足下面这几个条件:
回溯算法的思想很是简单,大部分状况下,都是用来解决广义的搜索问题,也就是,从一组可能的解中,选择出一个知足要求的解。回溯算法很是适合用递归来 实现,在实现的过程当中,剪枝操做是提升回溯效率的一种技巧。利用剪枝,咱们并不须要穷举搜索全部的状况,从而提升搜索效率。