记录数据结构与算法的知识点以及常见题目,为以后复习做准备;
什么是数据结构与算法呢?
答:
算法:一系列程序指令,用以解决特定的运算和逻辑问题。
数据结构:数据结构就是一种存储和管理数据的逻辑结构。
1. 数据结构类型
数据结构类型非为:
线性
和非线性
线性:数组、链表、堆栈、队列
非线性:树、图
1.1 数组
数组:使用一组连续的内存空间,来存储一组具有相同类型的数据;
数组特性: 查找元素快,中间插入/删除元素慢
常见题目:
- 189. 轮转数组
- 66. 加一
1.2 链表
链表:使用一组任意的存储单元(可以是连续的,也可以是不连续的),来存储一组具有相同类型的数据。
链表特性:查找速度慢,中间插入\删除元素快
常见技巧:
- 哑巴节点(在head之前添加一个节点,方便对链表进行从头开始处理,比如删掉头结点)
- 快慢指针(解决寻找中心点和回环问题)
- 哑巴节点+双指针
常见例题:
- 合并有序链表
class Solution:
def ReverseList(self , head: ListNode) -> ListNode:
# 1. cur为原链表
# 2. pre为新链表,维护新链表的内容
# 2. temp暂存cur与cur.next断开时的cur.next剩下的原链表的顺序,
# 保持拼接
cur = head
# 保持顺序
pre = None
while cur:
temp = cur.next
# 反向拼接到pre上
cur.next = pre
# pre更新到新增加的位置
pre = cur
# cur 回到原始序列继续
cur = temp
return pre
- 206. 反转链表
- 148. 排序链表
- 234. 回文链表
- 83. 删除排序链表中的重复元素
- 160. 相交链表
1.3 堆栈
堆栈:一种只允许在表的一端进行插入和删除操作的线性表:
堆栈特性:后进先出
常见例题:
- 394. 字符串解码
- 232. 用栈实现队列
- 20. 有效的括号
- 17.14. 最小K个数
- 150. 逆波兰表达式求值
1.4 队列
队列:一种只允许在表的一端进行插入操作,而在表的另一端i进行删除操作的线性表。
队列特性:先进先出
常见例题:
- 设计循环队列
- 用两个队列实现一个栈
- 双端队列用于滑动窗口
- 优先队列用于寻求TOP-K(常考)
1.6 哈希表
哈希表:也叫做散列表。是根据关键码值(Key Value)直接进行访问的数据结构。也就是说,它通过键 key 和一个映射函数 Hash(key) 计算出对应的值 value,把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做「哈希函数(散列函数)」,存放记录的数组叫做「哈希表(散列表)」。
哈希特性:搜索速度快,但是存在哈希冲突(用链表存储可以解决)
常见题目:
1. 两数之和217. 存在重复元素219. 存在重复元素 II220. 存在重复元素 III
1.5 树
二叉树: 完全二叉树、完美二叉树、平衡二叉树AVL树、二叉搜索树BST
完全二叉树:堆heap、大顶堆和小顶堆平衡二叉树AVL树
:任何节点的左右子树的深度之差都不超过1.二叉搜索树BST
:左子树上的所有节点的值均比根节点小,右子树上的所有节点的值均比根节点大;中序遍历为单调序列。
常考的题目:
- 前序遍历(用宽度优先算法解决)
- 中序遍历(用宽度优先算法解决)
- 后序遍历 (用宽度优先算法解决)
- 层次遍历(用广度优先算法解决)
1.6 图
图:由顶点与边构成的结构;
按照是否有方向:分为有向图和无向图
按照是否由环:分为环形图和无环图
按照边是否有权重:分为环形图和无环图
涉及到:
1)最短路径
:迪杰斯特拉算(Dijkstra)
2)最小生成树
:Prim算法、Kruskal算法、Boruvka算法
3)并查集UnionFind
:用于划分集合,数据之间的关联。包括:初始化unionCreate、路径压缩find、合并两个集合unionTwo和判断两个数据是否属于同一个集合isConnected。
2. 基础算法
2.1 排序
名词解释:
n:数据规模
k:"桶"的个数
In-place:占用常数内存,不占用额外内存
Out-place:占用额外内存
稳定性:排序后 2 个相等键值的顺序和排序之前它们的顺序相同
关于稳定性:
稳定的排序算法:冒泡排序、插入排序、归并排序和基数排序。
不是稳定的排序算法:选择排序、快速排序、希尔排序、堆排序。
常考的排序算法:
掌握排序的复杂度,以及常用见的冒泡排序
、选择排序
、插入排序
、快速排序
、归并排序
、堆排序
;
# 假设n个元素待排序
# 1. 冒泡排序的步骤
# 1.1 从头到开始遍历列表进行交换元素
# 比较相邻两个元素,如果前面元素比后面元素大,则交换两个元素位置,将大的元素放到后面
# 从开始第一对元素到最后一对元素进行交换,最后的元素会使最大的数
# 1.2 重复第一步骤,总要进行n-1次;
def bubble_sort(arr):
# 遍历的次数
for i in range(1,len(arr)):
# 从头遍历到未进入排序的位置
for j in range(0,len(arr)-i):
if arr[j] > arr[j+1]:
arr[j], arr[j+1] = arr[j+1], arr[j]
return arr
if __name__ == '__main__':
s = [9,8,6,7,4,3,99,5,3]
new_s = bubble_sort(s)
print(new_s)
# 稳定性:稳定
# 最优时间复杂度:O(n^2)
# 最坏时间复杂度:O(n^2)
# 假设n个元素待排序
# 1. 选择排序步骤
# 1.1 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
# 1.2 再从剩余未排序元素中继续寻找着最小(大)元素,然后放到已排序的末尾。
# 1.3 重复第二步,直到所有元素排序完毕
def select_sort(arr):
# 待插入的位置
for i in range(0,len(arr)):
# 从待排序位置查找最小(大)值放到待插入的位置
min_index = i # 记录最小数的索引
for j in range(i+1, len(arr)):
if arr[j] < arr[min_index]:
min_index = j
# i不是最小数时,将i和最小数进行交换
if i != min_index:
arr[i], arr[min_index] = arr[min_index], arr[i]
return arr
if __name__ == '__main__':
s = [9,8,6,7,4,3,99,5,3]
new_s = select_sort(s)
print(new_s)
# 稳定性:稳定
# 最优时间复杂度:O(n^2)
# 最坏时间复杂度:O(n^2)
# 假设n个元素待排序
# 1. 插入排序步骤
# 1.1 将第一个元素看作有序序列,把第二个元素到最后一个元素当成是未排序序列
# 1.2 从未排序的初始位置开始扫描到结尾,将未排序的元素插入到有序序列的适当位置。
def insert_sort(arr):
# 待插入的元素
for i in range(len(arr)):
preIndex = i - 1 # 已经排好序的最后一个位置
current = arr[i] # 存储待插入的元素
while preIndex >= 0 and arr[preIndex] > current:
arr[preIndex+1] = arr[preIndex] # 将元素向后移
preIndex -= 1
arr[preIndex+1] = current
return arr
if __name__ == '__main__':
s = [9,8,6,7,4,3,99,5,3]
new_s = insert_sort(s)
print(new_s)
# 稳定性:稳定
# 最优时间复杂度:O(n^2)
# 最坏时间复杂度:O(n^2)
# 快速排序
def quick_sort(s, l, r):
if l >= r:
return
# 列表的最后一个元素,s[l]作为基准值
pivot = s[l]
left = l
right = r
# 一轮循环,有可能,不能将所有的大数都放到基准值的右边,小数放到基准值的左边,所以直到left>right 跳出循环;
while left < right:
# 找寻右边数列比基准值小的数的位置
while left < right and s[right] >= pivot:
right -= 1
# 此时有两种情况,第一种:left=right,下面操作可以说无意义;
# 第二种:找到了s[right] >= pivot 且 left<right,意义将较大或相等的值放到左边的坑位
s[left] = s[right]
# 找寻左边数列比基准值大的数的位置
while left < right and s[left] < pivot:
left += 1
# 此时有两种情况,第一种:left=right,下面操作可以说无意义;
# 第二种:找到了s[right] < pivot 且 left<right,意义将较小的值放到右边的坑位
s[right] = s[left]
# 将大的数放在基准值的右边,小的数放在基准值的左边
# while结束时候,left=right ,将基准值放到中间
s[left] = pivot
quick_sort(s, l, left-1)
quick_sort(s, left+1, r)
if __name__ == '__main__':
s = [9,8,6,7,4,3,99,5,3]
quick_sort(s,0,len(s)-1)
print(s)
# 稳定性:不稳定
# 最优时间复杂度:O(nlogn)
# 最坏时间复杂度:O(n^2)
# 归并排序
def merge(L_list, R_list): # 拼接
# 记录左右列表中元素位置情况
i, j = 0,0
res = []
while i<len(L_list) and j <len(R_list):
if L_list[i] < R_list[j]:
res.append(L_list[i])
i += 1
else:
res.append(R_list[j])
j += 1
# 两个列表中存在未合并完的数据
res += L_list[i:] if i < len(L_list) else R_list[j:]
return res
def merge_sort(lis): # 分离
length = len(lis)
# 将列表拆分到只有一个元素为止
if length <= 1:
return lis
else:
mid = length // 2
left = merge_sort(lis[:mid])
right = merge_sort(lis[mid:])
return merge(left,right)
if __name__ == '__main__':
s = [9,8,6,7,4,3,99,5,3]
new_s = merge_sort(s)
print(new_s)
2.2 查找
主要是有序数组进行二分查找和双指针
2.3 搜索
主要有深度优先搜索(DFS)和广度优先搜索(BFS):
经典BFS求最短路径的经典案例模板为:
二进制矩阵中的最短路径 思路:利用BFS一层一层进行所有方向的遍历,当某一层遇到目标值则停止搜索并返回当前位置:
# BFS
class Solution:
def shortestPathBinaryMatrix(self, grid: List[List[int]]) -> int:
def bfs(grid,start_x,start_y,target_x,target_y):
m = len(grid)
n = len(grid[0])
queue = [] # 声明队列
queue.append((start_x,start_y)) # 将初始点加入队列
visited = set() # 用于存储拜访过的点
visited.add((start_x,start_y))
# 从(0,0)第一步开始
steps = 1
dic = [(0,1),(0,-1),(1,0),(-1,0),(-1,-1),(-1,1),(1,-1),(1,1)] # 四个移动方向
while queue: # 队列不为空时
# 当前节点内容大小
size = len(queue)
for _ in range(size):
cur_x,cur_y = queue.pop(0)
# 如果当前点与目标点相同,返回当前步伐
if cur_x == target_x and cur_y == target_y:
return steps
# 遍历当前点,下个方向所有能走的点
for x,y in dic:
pre_x = cur_x + x
pre_y = cur_y + y
if 0<= pre_x < m and 0<= pre_y < n and grid[pre_x][pre_y] != 1 and (pre_x,pre_y) not in visited:
queue.append((pre_x,pre_y))
visited.add((pre_x,pre_y))
# 一层一层寻找目标位置,当没有找到目标位置,步伐+1,如果找到上面就已经退出
steps += 1
return -1
# 初始状态就走不通
if grid[0][0] == 1:
return -1
ans = bfs(grid,0,0,len(grid)-1,len(grid[0])-1)
if ans == -1:
return -1
else:
return an
经典DFS求树前序、中序、后序遍历的经典案例模板为:
144. 二叉树的前序遍历94. 二叉树的中序遍历145. 二叉树的后序遍历
pre_ans = [] # 遍历前序遍历答案 根左右
mid_ans = [] # 存储中序遍历答案 左根右
b_ans = [] # 存储后序遍历答案 左右根
def dfs(root):
if not root:
return
pre_ans.append(root.val)
dfs(root.left)
mid_ans.append(root.val)
dfs(root.right)
b_ans.append(root.val)
dfs(root)
return pre_ans # 返回前序遍历答案
# return mid_ans # 返回中序遍历答案
# return b_ans # 返回后序遍历答案
2.4 动态规划
动态规划题目特点:
计数
:有多少种方式走到右下角;有多少种方法选出k个数使得和Sum。求最值
:从左上角走到右下角路径的最大
数字和;最长
上升子序列的长度。求存在性
: 取石子游戏,先手是否取胜;能不能选出k个数使得和是Sum;动态规划四步解题法:
第一步:确认状态【最后一步是什么,优化成子问题】
第二步:转移方程
第三步:初始条件和边界情况
第四步:计算顺序【取决于当前i
依赖i-1
(从小到大)还是i+1
(从大到小)】
2.4.1 案列:322. 零钱兑换
class Solution:
def coinChange(self, coins: List[int], amount: int) -> int:
'''
思路:
第一步:确认状态
最后一步是:最优策略中使用的最后一枚硬币是ak
优化成子问题: 最少硬币个数凑成: 总金额 - ak
第二步: 转移方程
f(x) 表示当前状态最优的硬币数,x表示当前总金额
f(x) = min(f(x-1)+1, f(x-2)+1, f(x-5)+1)
第三步:初始条件和边界条件
初始条件:f(0) = 0
边界条件:金额不可能是负数,因为转移方程是求最小值,所以f(小于0) = 正无穷
第四步: 计算顺序
因为已知初始状态为f(0),且金额是增加的,所以顺序是f(0)、f(1)..f(x)
'''
# dp[i]:i表示金额,d[i]凑金额的最优硬币个数
# 开辟amount+1空间,多出来的1个空间是给f(0)的
dp = [float('inf')]*(amount+1)
# 初始化
dp[0] = 0
for i in range(1,amount+1):
# 三种不同的硬币
for j in coins:
if i - j >= 0:
dp[i] = min(dp[i-j]+1,dp[i])
# 最后输出状态时dp[-1],不存在dp[-1]依旧为正无穷
return dp[-1] if dp[-1] != float('inf') else -1
2.4.2 背包问题
01背包
思想:
# 测试案例
'''
输入:
3 5
2 10
4 5
1 4
输出:
14
'''
# n 表示物品的个数, v表示背包的体积
n, v = map(int,input().split())
# w[i]表示i物品的体积,c[i]表示i物品的价值
w, c = [],[]
# 获取物品信息情况
for i in range(n):
lis =list(map(int, input().split()))
w.append(lis[0])
c.append(lis[1])
dp = [0 for _ in range(v+1)]
# i遍历物品是否拿
for i in range(n):
# 从后向前跟新(滚动跟新),j为背包的容量
for j in range(v,-1,-1):
# 如果背包容量大于w[i]更新内容
if j >= w[i]:
# dp[j-w[i]] 表示 背包容量为j-w[i]时背包内的价值
dp[j] = max(dp[j],dp[j-w[i]]+c[i])
print(max(dp))
完全背包:
'''
输入:
2 6
5 10
3 1
输出:
10
'''
# n表示n个不同的物品,v表示背包的体积
n, v = map(int,input().split())
# w[i]表示物体i的体积,c[i]表示物品i的价值
w, c = [],[]
# 获取物品信息情况
for i in range(n):
lis = list(map(int, input().split()))
w.append(lis[0])
c.append(lis[1])
dp = [0 for _ in range(v+1)]
for i in range(n):
# 从前向更跟新
for j in range(v+1):
if j >= w[i]:
dp[j] = max(dp[j],dp[j-w[i]]+c[i])
print(max(dp))
2.4.3 最长公共子序列
class Solution:
def longestCommonSubsequence(self, text1: str, text2: str) -> int:
m, n = len(text1), len(text2)
dp = [[0] * (n + 1) for _ in range(m+1)]
for i in range(1, m + 1):
for j in range(1, n + 1):
if text1[i - 1] == text2[j - 1]:
dp[i][j] = dp[i - 1][j - 1] + 1
else:
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
return dp[m][n]
2.4.4 最长回文子串
class Solution:
def longestPalindrome(self, s: str) -> str:
n = len(s)
if n < 2:
return s
max_len = 1
begin = 0
# dp[i][j] 表示 s[i..j] 是否是回文串
dp = [[False] * n for _ in range(n)]
for i in range(n):
dp[i][i] = True
# 递推开始
# 先枚举子串长度
for L in range(2, n + 1):
# 枚举左边界,左边界的上限设置可以宽松一些
for i in range(n):
# 由 L 和 i 可以确定右边界,即 j - i + 1 = L 得
j = L + i - 1
# 如果右边界越界,就可以退出当前循环
if j >= n:
break
if s[i] != s[j]:
dp[i][j] = False
else:
if j - i < 3:
dp[i][j] = True
else:
dp[i][j] = dp[i + 1][j - 1]
# 只要 dp[i][L] == true 成立,就表示子串 s[i..L] 是回文,此时记录回文长度和起始位置
if dp[i][j] and j - i + 1 > max_len:
max_len = j - i + 1
begin = i
return s[begin:begin + max_len]
2.4.5 案例: 70. 爬楼梯
class Solution:
def climbStairs(self, n: int) -> int:
'''
* 动态规划四部曲:
* 1.确定dp[i]的下标以及dp值的含义: 爬到第i层楼梯,有dp[i]种方法;
* 2.确定动态规划的递推公式:dp[i] = dp[i-1] + dp[i-2];
* 3.dp数组的初始化:因为提示中,1<=n<=45 所以初始化值,dp[1] = 1, dp[2] = 2;
* 4.确定遍历顺序:分析递推公式可知当前值依赖前两个值来确定,所以递推顺序应该是从前往后;
解释为什么dp[i] = dp[i-1] + dp[i-2]:以1为结尾或以为2结尾时,前面不管怎么走,
这两种情况都不会出现相同的状态,所以要累加;
'''
if n <= 2:
return n
# 定义范围
dp = [0]*(n+1)
# 初始值
dp[1],dp[2]=1,2
# 迭代
for i in range(3,n+1):
dp[i] = dp[i-1] + dp[i-2]
return dp[-1]
参考
https://algo.itcharge.cn/00.Introduction/05.Categories-List/ https://blog.51cto.com/maxiaobian/3017516 https://www.runoob.com/w3cnote/ten-sorting-algorithm.html