算法的思路

比较排序

插入排序:对于已经遍历过的子数组维持一个顺序,将新遍历的数组插入已经排序好的子数组的对应的位置,支持原址,代价: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[i][j]=\left\{\begin{matrix} c[i-1][j-1]+1 (if Xi=Yj) \\ max(c[i-1,j],c[i,j-1]) (if Xi\neq Yi) \end{matrix}\right.

边界条件: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是最里层的循环。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

yuyuelongfly

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值