比较排序
插入排序:对于已经遍历过的子数组维持一个顺序,将新遍历的数组插入已经排序好的子数组的对应的位置,支持原址,代价:O(n^2)
归并排序:先递归分解成子问题,分解到每组只有一个数时进行归并,使得合并后的每一个数据都是排序好的子数组,将两个排序好的子数组进行合并仅需要依次比较队首的元素,合并成一个序列。
非原址,代价 O(nlgn)。
class Solution:
def merge(self,nums,left,mid,right):
numstmp=nums[left:right+1]
i,j=0,mid+1-left
k=left
while i<=mid-left and j<=right-left:
if numstmp[i]<numstmp[j]:
nums[k]=numstmp[i]
i+=1
else:
nums[k]=numstmp[j]
j+=1
k+=1
while i<=mid-left:
nums[k]=numstmp[i]
i+=1
k+=1
while j<=right-left:
nums[k]=numstmp[j]
j+=1
k+=1
def mergesort(self, nums, left, right):
if left>=right:
return
mid=(left+right)//2
self.mergesort(nums,left, mid)
self.mergesort(nums,mid+1,right)
self.merge(nums,left,mid,right)
def sortArray(self, nums: List[int]) -> List[int]:
self.mergesort(nums,0,len(nums)-1)
return nums
快速排序:取一个主元,每次把主元放到合适位置,原址,代价最差O(n^2), 最优O(nlgn)
class Solution:
def partition(self,nums, left, right):
pivot=random.randint(left, right)
nums[pivot], nums[right]=nums[right],nums[pivot]
pivotvalue=nums[right]
i=left-1
for j in range(left,right):
if nums[j]<pivotvalue:
nums[i+1],nums[j]=nums[j],nums[i+1]
i+=1
nums[right],nums[i+1]=nums[i+1],nums[right]
return i+1
def quicksort(self,nums,left, right):
if left>=right:
return
mid=self.partition(nums,left,right)
self.quicksort(nums,left,mid-1)
self.quicksort(nums,mid+1,right)
def sortArray(self, nums: List[int]) -> List[int]:
self.quicksort(nums,0,len(nums)-1)
return nums
在递归调用时,快速排序和归并排序类似,都是先“分”,再“递归调用”,区别在于:
- 归并排序的“分”的步骤非常简单,主要工作是在递归调用后的“合”上;
- 快速排序的主要工作是在“分”步骤完成的,完成递归调用就完成了排序,无需再“合”。
堆排序:隐式地把数组看成一颗完全二叉树,第一个数为树根,每个节点i的左孩子的index为2*i+1, 右孩子的index为2*i+2。
堆排序的基本操作是维持最大堆的操作:最大堆是指根节点的值大于左子树和右子树所有节点的值。假设一个节点的左子树和右子树都满足最大堆的条件,只有该节点和其左孩子、右孩子的局部最大堆特性被破坏,则将该节点值与其值最大的孩子的值进行交换,修复改局部最大堆的特性,但可能导致被交换值的子树的局部最大堆特性被破坏,通过递归地调用维持最大堆的特性,直到节点及其左孩子、右孩子满足最大堆的特性或者到达叶节点。
具体的操作顺序:
a. 建堆(从倒数第二层有孩子的节点开始(len(nums)-1//2),index自后向前进行维持最大堆的操作,则实现建堆);
b. 依次取堆顶的最大的数,并进行维持最大堆的操作。为了保持原址操作,另堆顶数与堆最后一个数进行交换,并将堆的长度-1。
原址,代价O(nlgn)。
class Solution:
def fixheap(self,nums,root,length):
maxroot=root
if 2*root+1<length and nums[2*root+1]>nums[root]:
maxroot=2*root+1
if 2*root+2<length and nums[2*root+2]>nums[maxroot]:
maxroot=2*root+2
if maxroot != root:
nums[root], nums[maxroot]=nums[maxroot], nums[root]
self.fixheap(nums,maxroot,length)
else:
return
def buildheap(self, nums,n):
for i in range((n-1)//2,-1,-1):
self.fixheap(nums,i,n)
def sortArray(self, nums: List[int]) -> List[int]:
n=len(nums)
self.buildheap(nums,n)
for i in range(n-1,-1,-1):
nums[0],nums[i]=nums[i],nums[0]
self.fixheap(nums,0,i)
return nums
二叉树遍历
1. 深度优先搜索
深度优先搜索一般可以使用递归,也可以显式维护栈。前序遍历,中序遍历,后序遍历 都属于深度优先搜索。都可以使用递归法;也可以使用迭代法。
1.1. 递归法:先定义退出条件,然后在适当位置访问节点的值,并递归节点的左子树和右子树。
- 前序遍历则先访问节点值,递归左子树,递归右子树;
- 中序遍历先递归左子树,访问节点值,递归右子树;
- 后序遍历先递归左子树,递归右子树,访问节点值。
以后序遍历为例:
class Solution:
def postorder(self, root, results):
if not root:
return
self.postorder(root.left, results)
self.postorder(root.right,results)
results.append(root.val)
def postorderTraversal(self, root: TreeNode) -> List[int]:
results=[]
self.postorder(root, results)
return results
1.2. 迭代法:借用stack显式地维护遍历的次序。可以使用统一的模板:依次从当前node向下遍历,把沿途的所有节点都入栈stack;直到节点无左子树时,stack弹出最新加入的节点,并以该节点的右孩子为节点进行迭代遍历。对于前序遍历和中序遍历,只需要调整在适当的位置访问节点的值,以前序遍历为例:
class Solution:
def preorderTraversal(self, root: TreeNode) -> List[int]:
cur,stack,results=root,[],[]
while stack or cur:
while cur:
results.append(cur.val)
stack.append(cur)
cur=cur.left
cur=stack.pop()
#results.append(cur.val) #若中序遍历,则改为在此处访问节点值
cur=cur.right
return results
后序遍历需要一点trick,可以按照前序遍历的思路,但转而每次向右节点深入,变成前-右-左,最后把列表结果反向输出即为后续遍历:
class Solution:
def postorderTraversal(self, root: TreeNode) -> List[int]:
cur, stack, result=root, [], []
while stack or cur:
while cur:
result.append(cur.val)
stack.append(cur)
cur=cur.right
cur=stack.pop()
cur=cur.left
return result[::-1]
2. 广度优先搜索
广度优先搜索一般借用deque数据结构。层序遍历 属于广度优先搜索,迭代法使用deque显式维护遍历的次序。
class Solution:
def levelOrder(self, root: TreeNode) -> List[List[int]]:
if not root:
return []
deque=[root]
result=[]
while deque:
length=len(deque)
curline=[]
for i in range(length):
cur=deque.pop(0)
curline.append(cur.val)
if cur.left:
deque.append(cur.left)
if cur.right:
deque.append(cur.right)
result.append(curline)
return result
回溯法
使用递归法实现(递归函数内部都是先写退出条件),递归实现属于深度优先搜索,满足条件时继续调用递归深入搜索,不满足条件时就回退到上一步的状态。
八皇后问题
动态规划
先定义状态(一般为待求的变量),再写状态转移方程。若求解一维问题dp[i],则假设dp[0]~dp[i-1]已知;若求解二维问题dp[i][j],则假设dp[i][0]~dp[i][j-1]和dp[0][j]~dp[i-1][j]已知。然后建立状态转移方程,通过一步推出dp[j] (一维问题)或dp[i][j] (二维问题)。
具体编程时写循环,先确定边界条件,随后根据状态转移方程自底向上地扩展已知的状态。对于二维动态规划问题,需要考虑二维的边界(维度1的index=0情况下,维度2的所有index;维度2的index=0情况下,维度1的所有index)
如需构造动态规划的最优解方案,需要建立另一张表,在动态规划迭代时记录下取得dp[i]或dp[i][j]的解,如上面钢条切割问题的n,矩阵链乘法问题中的k;或者标注出推进迭代的假设,如构造最长公共子序列的不同分支用不同数字标注。然后通过递归获得最优解。
爬楼梯
已知上楼梯可以走1阶或2阶,问上到i阶有多少种走法?
构造最优子问题:若已知上到前1~i-1阶的走法有dp[1]~dp[i-1]种,问题转化为求上到i阶时最后一步是通过走1阶还是2阶的。
简历状态转移方程:dp[i]=dp[i-1]+dp[i-2]
边界条件:dp[1]=1,dp[2]=2
程序实现:对i循环
切钢条
已知不同长度的钢条Wn的价值为Vn,对于总长度为W的钢条,如何分割能使得总价值最大?(Wn和W均为正数)
构造最优子问题:若已知长度为0~i-1的钢条在第n (0<=n<=i-1)个位置切或不切的组合能达到的最大价值dp[0]~dp[i-1],问题转化为对于长度为i的钢条在第n (0<=n<=i)个位置是否切割才能获得最大的价值dp[j]。
建立状态转移方程:dp[i]=max(Vn+dp[i-n]) (where 1<=n<=i)
边界条件:dp[0]=0
程序实现:对i,j进行循环(j为外循环,i为内循环)。
01背包问题(0-1 Knapsack problem)
有N件物品,每件重量为Wi,每件价值为Vi,背包能够容纳的总重量为W,求在背包中装哪些物品可以达到最大的价值?(Wi和W都是整数)
构造最优子问题:若已知容纳重量0~ j-1的背包内前i件物品组合能达到的最大价值为dp[i,0]~dp[i,j-1],以及容纳重量 j的背包内前0~i-1件物品组合能达到的最大价值为dp[0,j]~dp[i-1,j], 则问题转化为是否要将第i件物品放入容纳重量为j背包以达到的最大价值dp[i,j]。
建立状态转移方程:dp[i,j]=max(dp[i-1,j],vi+dp[i-1,j-wi]).
边界条件: dp[0,j]=0, dp[i,0]=0
程序实现:对i,j进行循环(j是外循环,i为内循环)。
LCS问题(最长公共子序列)
已知两个序列X,Y,求它们的最长公共子序列长度,子序列即元素同时出现在X和Y,且顺序相同,但不要求连续。
构造最优子问题: 若已知序列X的前0~i-1个元素和Y的前j个元素的最长公共子序列长度为dp[0][j]~dp[i-1][j],以及序列X的前i个元素和Y的前0~j-1个元素的最长公共子序列长度为dp[i][0]~dp[i][j-1], 问题转化为序列X的第i个元素和Y的第j个元素是否相同。
建立状态转移方程:
边界条件:dp[0,j]=0, dp[i,0]=0
矩阵链乘法
已知n个矩阵序列A1,A2,...,An,第i个矩阵的规模为pi-1*pi (1<=i<=n),计算其乘积A1A2...An。求完全括号化方案,使得计算乘积A1A2...An所需标量乘法次数最少。
构造最优子问题:若已知最后的矩阵为Aj的矩阵链所需标量乘法次数最少为dp[0][j]~dp[i-1][j], 以及第一个矩阵为Ai的矩阵链所需标量乘法次数最少为dp[i][0]~dp[i][j-1], 问题转化为求第一个矩阵为Ai、最后一个矩阵为Aj的矩阵链所需标量乘法次数最少的dp[i][j]。
建立状态转移方程:dp[i][j]=min(dp[i][k]+dp[k+1][j]+pi-1*pk*pj) ( i<=k<=j)
边界条件:dp[i][i]=0
程序实现:引入哑变量l控制矩阵链的长度,l为最外层循环(用l控制j的循环),i是最中间层的循环,k是最里层的循环。