【目录】数据结构与算法基础
一、哈希表
数组就是一张哈希表,哈希表中关键码就是数组的索引下标,通过下标直接访问数组中的元素,如下图所示:
一般解决:快速判断一个元素是否在集合里
枚举,时间复杂度是O(n);
使用哈希表, 只需要O(1)
哈希法以空间换取了时间,因为我们要使用额外的数组,set或者是map来存放数据,才能实现快速的查找。
二、 字符串
KMP算法
KMP主要应用在字符串匹配问题上
算法思想:当出现字符串不匹配时,可以记录一部分之前已经匹配的文本内容,利用这些信息避免从头再去做匹配,大大降低了时间复杂度,这也就是为什么这类问题我们选择用KMP算法。
1. 关于前缀表
1) 前缀表有什么用?
前缀表是用来回退的,它记录了模式串与主串(文本串)不匹配的时候,模式串应该从哪里开始重新匹配。
要在文本串:aabaabaafa 中查找是否出现过一个模式串:aabaaf。
过程如下图所示
上图使用了前缀表,就不会从头匹配,而是从上次已经匹配的内容开始匹配(动画中我们发现回退到了b,而不是最开始的字母a)
2) 那前缀表是如何记录的呢
记录下标 i 之前(包括i)的字符串中,有多大长度的相同前缀、后缀
-
前缀:不包含最后一个字符的所有以第一个字符开头的连续子串。
-
后缀:不包含第一个字符的所有以最后一个字符结尾的连续子串。
借助例子理解:
即 最长就是2, aa
3) 如何计算前缀表
记录下标 i 之前(包括i)的字符串中,有多大长度的相同前缀、后缀
最长相同前后缀的长度为0。(注意字符串的前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串;后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串。)
最长相等前后缀长度为1,a
前缀为aa,a
后缀为ab,b
最长相同前后缀长度为 0
以此类推前缀表如下图
再来看一下如何利用 前缀表找到 当字符不匹配的时候应该指针应该移动的位置。如动画所示:
找到的不匹配的位置, 那么此时我们要看它的前一个字符的前缀表的数值是多少。
前一个字符的前缀表的数值是2, 所有把下标移动到下标2的位置继续比配
最后找到了匹配的字符串了
2. 时间复杂度
其中n为文本串长度,m为模式串长度,在匹配的过程中,根据前缀表不断调整匹配的位置,可以看出匹配的过程是O(n),之前还要单独生成next数组,时间复杂度是O(m)。所以整个KMP算法的时间复杂度是O(n+m)的。
而暴力的解法两个for循环遍历,显而易见是O(n × m),所以KMP在字符串匹配中极大的提高的搜索的效率。
3. 构造next数组
构造next数组其实就是计算模式串s,前缀表的过程。 主要有如下三步:
1) 初始化
2) 处理前后缀不相同的情况
3) 处理前后缀相同的情况
1)初始化:
定义两个指针i 和 j,j 指向前缀末尾位置,i 指向后缀末尾位置。
然后还要对next数组进行初始化赋值,如下:
j = 0
next[0] = j
next[i] 表示 i(包括i)之前最长相等的前后缀长度(其实就是j)
2) 处理前后缀不相同的情况
因为j初始化为0,那么i就从1开始,进行s[i] 与 s[j]的比较。
所以遍历模式串s的循环下标i 要从 1开始
如果 s[i] 与 s[j+1]不相同,也就是遇到 前后缀末尾不相同的情况,就要向前回退。
next[j]就是记录着j(包括j)之前的子串的相同前后缀的长度。
那么 s[i] 与 s[j+1] 不相同,就要找 j+1前一个元素在next数组里的值(就是next[j])。
for i in range(len(s)):
j = next[j-1] //向前回退
3)前后缀相同的情况
如果 s[i] 与 s[j] 相同,那么就同时向后移动i 和j 说明找到了相同的前后缀,同时还要将j(前缀的长度)赋给next[i], 因为next[i]要记录相同前后缀的长度。
if s[i] == s[j]:
j += 1
next[i] = j-1
整体代码如下:
def getNext(next, s):
j = 0
next[0] = j
for i in range(len(s)):
while j > 0 and s[i] != s[j]: // 前后缀不相同
j = next[j] // 向前回退
if s[i] == s[j-1]: // 前后缀相同
j += 1
next[i] = j-1 // 将前缀长度赋值给next数组
return next
三、 链表
1. 什么是链表?
链表分为三种:
1) 单链表:指针域只能指向结点的下一个结点
链表是一种通过指针串联在一起的线性结构。每一个节点由两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null(空指针的意思),链接的入口节点称为链表的头结点也就是head。
如图所示(单链表):
2)双链表: 每个结点有两个指针域,一个指向下一个结点,一个指向上一个结点
如图所示:
3)循环链表:链表首尾相连
2. 链表的存储方式
数组是在内存中是连续分布的,但是链表在内存中可不是连续分布的,而是散乱分布在内存中的某地址上,分配机制取决于操作系统的内存管理。
这个链表起始节点为2, 终止节点为7, 各个节点分布在内存的不同地址空间上,通过指针串联在一起。
3. 链表的定义
C++
struct Node{
int val; //结点上的存储元素
ListNode *next // 指向下一个结点
ListNode(int x) : val(x), next(NULL){} // 结点的构造函数
}
Python
def __init__(self, val, next=None):
self.val = val
self.next = next
4. 链表的操作
- 删除结点
删除D结点,如图所示
只要将C节点的next指针 指向E节点就可以了。 但D结点还存留在内存中,只是不在这个链表里。
C++里最好手动释放该节点,释放内存。对于其他(python,java)有自己的自动回收机制,不用手动释放。
2) 添加结点
如图所示
3) 性能分析
数组与链表作对比:
-
数组在定义的时候,长度就是固定的,如果想改动数组的长度,就需要重新定义一个新的数组。
-
链表的长度可以是不固定的,并且可以动态增删, 适合数据量不固定,频繁增删,较少查询的场景
5. 设计链表
此题经典至极,熟练掌握此题可对链表的 增、删、改、查 操作跟深入了解。
class Node:
def __init__(self, val):
self.val = val
self.next = None
class MyLinkedList(object):
def __init__(self):
self._head = Node(0) # 虚拟头结点
self._count = 0 # 添加结点数
def get(self, index):
if index < 0 or index >= self._count:
return -1
else:
cur = self._head # 定义一个cur指针 指向虚拟头结点
for i in range(index+1):
cur = cur.next
return cur.val
def addAtHead(self, val):
"""
:type val: int
:rtype: None
"""
self.addAtIndex(0, val)
def addAtTail(self, val):
"""
:type val: int
:rtype: None
"""
self.addAtIndex(self._count, val)
def addAtIndex(self, index, val):
"""
:type index: int
:type val: int
:rtype: None
"""
if index > self._count:
return
elif index < 0:
index = 0
self._count += 1 # 计数+1
add_node = Node(val) # 定义新结点
pre_node, current_node = None, self._head
for _ in range(index + 1):
pre_node, current_node = current_node, current_node.next
else:
pre_node.next, add_node.next = add_node, current_node
def deleteAtIndex(self, index):
"""
:type index: int
:rtype: None
"""
if 0 <= index and index < self._count:
self._count -= 1 # 计数-1
pre_node, current_node = None, self._head
for _ in range(index+1):
pre_node, current_node = current_node, current_node.next
else:
pre_node.next, current_node.next = current_node.next, None
四、栈与队列
五、二叉树
1. 二叉树理论基础
1) 二叉树种类
-
1. 满二叉树:如果一棵二叉树只有度为0的结点和度为2的结点,并且度为0的结点在同一层上,则这棵二叉树为满二叉树。
-
2. 完全二叉树:除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层,则该层包含 1~ 2^(h-1) 个节点。
-
3. 二叉搜索树:二叉搜索树是一个有序树。(左小,右大)
-
4.平衡二叉搜索树:要么是空树,要么左右子树高度差绝对值不超过1,而且左右子树都是平衡二叉树
深度优先遍历:
广度优先遍历:(层序遍历)
2) 二叉树主要有两种遍历方式:
- 深度优先遍历:先往深走,遇到叶子节点再往回走
- 前序遍历(递归法,迭代法)
中序遍历(递归法,迭代法)
后序遍历(递归法,迭代法)
- 前序遍历(递归法,迭代法)
- 广度优先遍历:一层一层的去遍历
- 层次遍历(迭代法)
- 层次遍历(迭代法)
六、回溯算法(递归法)
1) 回溯算法解决的问题:
- 组合:N个数里面按一定规则找出k个数的集合
- 切割:一个字符串按一定规则有几种切割方式
- 子集:一个N个数的集合里有多少符合条件的子集
- 排列:N个数按一定规则全排列,有几种排列方式
- 棋盘问题:N皇后,解数独等等
2) 理解回溯:
回溯法解决的都是在集合中递归查找子集,集合的大小就构成了树的宽度,递归的深度构成了树的深度。
3) 回溯算法模板:
- 回溯三部曲:
- 确定递归函数的返回值以及参数
- 回溯函数终止条件
- 单层搜索的过程
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
七、 贪心算法
1 理论基础
贪心的本质:选择每一阶段的局部最优,从而达到全局最优。
举个例子:
有一堆钞票,你可以拿走十张,如果想达到最大的金额,你要怎么拿?
指定每次拿最大的,最终结果就是拿走最大数额的钱。每次拿最大的就是局部最优,最后拿走最大数额的钱就是推出全局最优。
再举一个例子:
如果是 有一堆盒子,你有一个背包体积为n,如何把背包尽可能装满?
如果还每次选最大的盒子,就不行了。这时候就需要动态规划。
持续更新中…