Leetcode | 算法

文章目录

贪心算法

分发饼干 454

在这里插入图片描述- 方法:找到饥饿度最小的孩子,再找到能满足他的最小的饼干。以此类推

class Solution(object):
    def findContentChildren(self, g, s):
        g.sort()
        s.sort()
        m, n = len(g), len(s)
        i, j, num = 0, 0, 0
        while i<m and j<n:
            if g[i]<=s[j]:
                i += 1
                num += 1
            j += 1
            
        return num

Java数组排序:Arrays.sort(A);

分发糖果 135

在这里插入图片描述

  • 方法:在每次遍历中,只考虑并更新相邻一侧的大小关系
  1. 首先,每个孩子都分到1个糖果,因此初始化每个孩子的糖果都为1
  2. 其次,从左往右遍历,如果右边孩子的评分比左边的高,则右边孩子的糖果数更新为左边孩子的糖果数加 1;
  3. 最后,从右往左遍历,如果左边孩子的评分比右边的高,且左边孩子当前的糖果数大于右边孩子的糖果数,则左边孩子的糖果数更新为max{从左往右遍历后得到的数,右边孩子的糖果数加 1}。

举例:我们初始化糖果分配为 [1,1,1],第一次遍历更新后的结果为 [1,1,2],第二次遍历更新后的结果为 [2,1,2]

class Solution(object):
    def candy(self, ratings):
        n  = len(ratings)
        apple = [1 for _ in range(n)]
        # 从左往右遍历,右边是否大于左边
        for i in range(n-1):
            if ratings[i+1]>ratings[i]:
                apple[i+1] = apple[i]+1
        
        # 从右往左遍历,左边是否大于右边
        for i in range(n-1,0,-1):
            if ratings[i-1]>ratings[i]:
                apple[i-1] = max(apple[i-1], apple[i]+1)
            
        return sum(apple)
无重叠区间 435

在这里插入图片描述

  • 思路:优先保留结尾小且不相交的区间

在选择要保留区间时,区间的结尾十分重要:选择的区间结尾越小,余留给其它区间的空间就越大,就越能保留更多的区间。因此,我们采取的贪心策略为,优先保留结尾小且不相交的区间。

具体实现方法为,先把区间按照结尾的大小进行增序排序,新建一个list称为res用来保存最后留下的区间,初始化在res中加入结尾最小的区间。遍历所有区间,如果当前区间结尾>res中最大的结尾,且当前区间开头也大于res中最大的结尾的开头,说明区间不重叠,加入到res中。最后返回不在res中的个数

例如,排序后的数组为 [ [ 1 , 2 ] , [ 2 , 3 ] , [ 1 , 3 ] , [ 3 , 4 ] ] [[1,2], [2,3],[1,3], [3,4]] [[1,2],[2,3],[1,3],[3,4]]。按照我们的贪心策略,首先初始化为区间[1,2];由于[2,3]与[1,2]不交保留, [1,3] 与 [1,2] 相交,我们跳过该区间;由于 [3,4] 与 [2,3] 不相交,我们将其保留。因此最终保留的区间为 [[1,2], [2,3],[3,4]]。

  • 按照结尾大小进行升序
intervals = sorted(intervals,key = lambda x:x[1]) 
class Solution(object):
    def eraseOverlapIntervals(self, intervals):
        intervals = sorted(intervals,key = lambda x:x[1]) 
        res =[]
        for i in range(len(intervals)):
            if i==0: res.append(intervals[0])
            if intervals[i][1] != res[len(res)-1][1] and intervals[i][0] >= res[len(res)-1][1] :
                res.append(intervals[i])
                
        return len(intervals)-len(res)

java

class Solution {
    public int eraseOverlapIntervals(int[][] intervals) {
        if (intervals.length == 0) {
            return 0;
        }
        
        Arrays.sort(intervals, new Comparator<int[]>() {
            public int compare(int[] interval1, int[] interval2) {
                return interval1[0] - interval2[0];
            }
        });

        int n = intervals.length;
        int[] f = new int[n];
        Arrays.fill(f, 1);
        for (int i = 1; i < n; ++i) {
            for (int j = 0; j < i; ++j) {
                if (intervals[j][1] <= intervals[i][0]) {
                    f[i] = Math.max(f[i], f[j] + 1);
                }
            }
        }
        return n - Arrays.stream(f).max().getAsInt();
    }
}
Python sorted排序
# ------------------按照value进行排序,(正序:从小到大)
new_value = sorted(key_value.items(), key=lambda kv:(kv[1], kv[0]))
# new_value = sorted(key_value.items(), key=lambda kv: kv[1], reverse=True)
print('-- new_value: ', new_value)
# ------------------按照value进行排序,(倒序:从大到小)
new_value1 = sorted(key_value.items(), key=lambda kv: kv[1], reverse=True)
print('-- new_value: ',new_value1)
用最少数量的箭引爆气球 452

在这里插入图片描述

  • 思路:以区间结尾为射击点
  1. 先按照结尾,从小到大排序
  2. 射击点shoot初始化为第1个区间的结尾
  3. 遍历区间,如果区间开头大于射击点,即意味着射击点不在这个区间内,就需要另一只箭。射击点更新为当前区间结尾

实例1排序后的数组为, [ [ 1 , 6 ] , [ 2 , 8 ] , [ 7 , 12 ] , [ 10 , 16 ] ] [[1,6], [2,8],[7,12], [10,16]] [[1,6],[2,8],[7,12],[10,16]],射击点分别为6和12

class Solution(object):
    def findMinArrowShots(self, points):
        points = sorted(points,key = lambda x:x[1]) 
        if len(points)==0: return 0
        num, shoot = 1, points[0][1]
        for i in range(1,len(points)):
            if points[i][0]>shoot:
                num += 1
                shoot = points[i][1]
        return num

Java
注意,排序的时候自定义比较器,差值太大溢出
[[-2147483646,-2147483645],[2147483646,2147483647]] 就过不了了,这是因为差值过大而产生溢出。sort的时候不要用a-b来比较,要用Integer.compare(a, b)

比较器返回-1,表示保持这个顺序;返回1,表示交换顺序;返回0,什么都不做

class Solution {
    public int findMinArrowShots(int[][] points) {
        int ans = 1;
        Arrays.sort(points, new Comparator<int[]>() {
            @Override
            public int compare(int[] o1, int[] o2) {
                if(o1[1]<o2[1]) return -1;
                else if(o1[1]>o2[1]) return 1;
                else return o1[0]-o2[0];
            }
        });
        
        int cur = points[0][1];
        for(int i =1;i<points.length;i++){
            if(points[i][0]>cur){
                ans++;
                cur = points[i][1];
            }
        }
        return ans;
    }
}

也可以这么写:

Arrays.sort(points, new Comparator<int[]>() {
    @Override
    public int compare(int[] o1, int[] o2) {
        return Integer.compare(o1[1], o2[1]);
    }
});
划分字母区间 763

在这里插入图片描述

  • 思路:建立字典存储每个字母最后出现的位置,接着,划分点初始化为第一个字母出现的最后位置。
  • 然后,遍历s中所有字符,
  1. 如果当前字符s[i]最后出现的位置在划分点end前,说明s[i]出现的所有位置都在end前,不用动。
  2. 如果s[i]最后出现的位置在划分点end后,则划分点end后移到s[i]最后出现位置。
  3. 如果i==end,说明划分点end后没有出现s[i]之前的字符,这就是最佳划分点。
class Solution(object):
    def partitionLabels(self, s):
        # 建立字典存储每个字母最后出现的位置
        dic = {str(char): index for index, char in enumerate(s)}
        end, num = dic[s[0]], 0
        res = []
        for i in range(0,len(s)):
            num += 1
            curloc = dic[s[i]]
            # 当前字母最后一个位置超过区间,划分区间扩大
            if curloc>end:
                end = curloc
            # 当前字母位置就等于区间尾巴,说明该区间包含了前面所有出现过的
            if i == end:
                res.append(num)
                num = 0

        return res

根据身高重建队列 406

在这里插入图片描述
-思路:首先,对身高降序,对人数升序。

对身高进行降序,这样对于每个元素,在其之前的元素的个数,就是身高比自己高的人数。

其次,用res记录排队过程,遍历所有排序好的元素,如果当前元素p[1]即排队时前面比他高的人数>len(res),就把他加入到队伍末尾;

反之,说明把他放到队尾前面比他高的人数太多了,因此得把他放在p[1]处。

举例说明,示例1排序后是 [ 7 , 0 ] , [ 7 , 1 ] , [ 6 , 1 ] , [ 5 , 0 ] , [ 5 , 2 ] , [ 4 , 4 ] [7,0],[7,1],[6,1],[5,0],[5,2],[4,4] [7,0],[7,1],[6,1],[5,0],[5,2],[4,4],遍历到 [ 6 , 1 ] [6,1] [6,1]时, r e s = [ [ 7 , 0 ] , [ 7 , 1 ] ] res=[[7,0],[7,1]] res=[[7,0],[7,1]]这时候如果把 [ 6 , 1 ] [6,1] [6,1]加到队尾,比6高的前面就有两人了,所以得把 [ 6 , 1 ] [6,1] [6,1]加到队伍第二个位置处,也就是res[1]的地方。以此类推

class Solution(object):
    def reconstructQueue(self, people):
        # 对身高降序,对人数升序
        people = sorted(people, key = lambda x: (-x[0], x[1]))
        res = []
        for p in people:
            if len(res)<=p[1]:
                res.append(p)
            elif len(res)>p[1]:
                res.insert(p[1],p)
        return res
非递减数列 665

在这里插入图片描述

  • 自己(错误):一开始简单的认为出现两个非递减的就False,一个以下就True
class Solution(object):
    def checkPossibility(self, nums):
        count = 0
        for i in range(0,len(nums)-1):
            if nums[i]>nums[i+1]:
                count += 1
        return False if count>1 else True

在这里插入图片描述
但是遇到了 [ 3 , 4 , 2 , 3 ] [3,4,2,3] [3,4,2,3],可以看到识别出只有4是非递减的,但是没法修改。因此遇到非递减的数,需要修改后再判断下,如何修改?

遇到递减的情况时(nums[i] > nums[i + 1]),要么将这个元素nums[i]缩小,要么将后面的元素nums[i+1]放大。 我们需要注意:

-需要尽可能不放大nums[i + 1],这样会让后续非递减更困难;
-如果缩小nums[i],但不破坏前面的子序列的非递减性;

算法步骤:

遍历数组,如果遇到递减:
    还能修改:
        修改方案1:将nums[i]缩小至nums[i + 1];
        修改方案2:将nums[i + 1]放大至nums[i];
    不能修改了:直接返回false;
class Solution {
public:
    bool checkPossibility(vector<int>& nums) 
    {
        if (nums.size() == 1)   return true;
        bool flag = nums[0] <= nums[1] ? true : false; // 标识是否还能修改
        // 遍历时,每次需要看连续的三个元素
        for (int i = 1; i < nums.size() - 1; i++)
        {
            if (nums[i] > nums[i + 1])  // 出现递减
            {
                if (flag)   // 如果还有修改机会,进行修改
                {
                    if (nums[i + 1] >= nums[ i - 1])// 修改方案1
                        nums[i] = nums[i + 1];
                    else                            // 修改方案2
                        nums[i + 1] = nums[i];      
                    flag = false;                   // 用掉唯一的修改机会
                }   
                else        // 没有修改机会,直接结束
                    return false;
            }
        }
        return true;
    }
};

  • 自己:如果出现两个非递减就返回false,如果出现一个非递减,有两种修改情况,[1,2,7,4,5,6]和[5,7,1,8],前者我们遇到nums[i]>nums[i+1]的时候修改nums[i]也就是7,后者修改nums[i+1]也就是1,那么如何判断我们需要修改哪一个呢?

根据nums[i-1]和nums[i+1]判断,如果nums[i-1]<=nums[i+1]改nums[i],如果nums[i-1]>=nums[i+1]改nums[i+1]。

特殊情况收尾边界点在分情况处理

class Solution:
    def checkPossibility(self, nums: List[int]) -> bool:
        res = 0
        cut = 0
        for i in range(len(nums)-1):
            if nums[i]<=nums[i+1]:
                continue
            else:
                # 选择修改点
                if i==0 or nums[i-1]<=nums[i+1]:
                    cut = i
                else:
                    cut = i+1
                res += 1
        # 两次修改返回错误
        if res>=2:
            return False
        else:
            # 收尾直接改
            if cut==0 or cut==len(nums)-1:
                return True
            # 中间点需要判断相邻两端大小
            else:
                if nums[cut-1]<=nums[cut+1]:
                    return True
                else:
                    return False

DP问题

解析:https://leetcode-cn.com/leetbook/read/path-problems-in-dynamic-programming/rtd7d2/

不同路径

在这里插入图片描述

动态规划

定义 f [ i ] [ j ] f[i][j] f[i][j]为到达位置 (i,j) 的不同路径数量。

那么 f [ m − 1 ] [ n − 1 ] f[m−1][n−1] f[m1][n1]就是我们最终的答案,而 f [ 0 ] [ 0 ] = 1 f[0][0]=1 f[0][0]=1 是一个显而易见的起始条件。

由于题目限定了我们只能 往下 或者 往右 移动,因此我们按照当前可选方向进行分析:

  1. 当前位置只能「往下」移动,即有 f [ i ] [ j ] = f [ i − 1 ] [ j ] f[i][j]=f[i−1][j] f[i][j]=f[i1][j]

  2. 当前位置只能「往右」移动,即有 f [ i ] [ j ] = f [ i ] [ j − 1 ] f[i][j]=f[i][j−1] f[i][j]=f[i][j1]

  3. 当前位置即能「往下」也能「往右」移动,即有 f [ i ] [ j ] = f [ i ] [ j − 1 ] + f [ i − 1 ] [ j ] f[i][j]=f[i][j−1]+f[i−1][j] f[i][j]=f[i][j1]+f[i1][j]

class Solution(object):
    def uniquePaths(self, m, n):
        import numpy as np
        f = np.ndarray(shape=(m,n), dtype=np.object)
        f[0][0] = 1

        for i in range(m):
            for j in range(n):
                if i>0 and j>0:
                    f[i][j] = f[i-1][j]+f[i][j-1]
                # 最左侧只能往右移动
                elif i>0:
                    f[i][j] = f[i-1][j]
                elif j>0:
                    f[i][j] = f[i][j-1]
        return f[m-1][n-1]
不同路径 II

在这里插入图片描述

  • 关键:有障碍物的位置(i,j)的路为0,f[i][j]=0
class Solution(object):
    def uniquePathsWithObstacles(self, obstacleGrid):
        import numpy as np
        m, n = len(obstacleGrid),len(obstacleGrid[0])
        
        f = np.ndarray(shape=(m,n), dtype=np.object)
        f[0][0] = 0 if obstacleGrid[0][0]==1 else 1

        for i in range(m):
            for j in range(n):
                if obstacleGrid[i][j]!=1:
                    if i>0 and j>0:
                        f[i][j] = f[i-1][j]+f[i][j-1]
                    # 最左侧只能往右移动
                    elif i>0:
                        f[i][j] = f[i-1][j]
                    elif j>0:
                        f[i][j] = f[i][j-1]
                else:
                    f[i][j] = 0
        return f[m-1][n-1]
最小路径和

在这里插入图片描述

  • 解析
    在这里插入图片描述
class Solution(object):
    def minPathSum(self, grid):
        import numpy as np
        m = len(grid) 
        n = len(grid[0])
        f = np.ndarray(shape=(m,n),dtype=np.object)
        f[0][0]=grid[0][0]
        for i in range(m):
            for j in range(n):
                if i>0 and j>0:
                    f[i][j] = min(f[i-1][j],f[i][j-1])+ grid[i][j]
                elif j>0:
                    f[i][j] = f[i][j-1]+grid[i][j]
                elif i>0:
                    f[i][j] = f[i-1][j]+grid[i][j]
        return f[m-1][n-1]
三角形路径最小和

在这里插入图片描述

  • 这行的值由上一行更新而来
class Solution(object):
    def minimumTotal(self, triangle):
        import numpy as np
        n = len(triangle)
        f = np.ndarray(shape=(n,n),dtype=np.object)
        f[0][0] = triangle[0][0]

        for i in range(n):
            for j in range(len(triangle[i])):
                if i>0 and j>0 and j<len(triangle[i])-1:
                    f[i][j] = min(f[i-1][j],f[i-1][j-1])+triangle[i][j]
                # 最右列 
                elif i>0 and j>0 and j==len(triangle[i])-1:
                    f[i][j] = f[i-1][j-1]+triangle[i][j]
                # 最左列
                elif i>0 and j==0:
                    f[i][j] = f[i-1][j]+triangle[i][j]
        min_ = f[n-1][0]
        
        for j in range(len(triangle[n-1])):
            min_ = min(min_,f[n-1][j])
        return min_
三角形路径最小和(进阶)

你可以只使用 O(n) 的额外空间(n 为三角形的总行数)来解决这个问题吗?

也就是说,我们之前的算法,存储f的数据用了n*n的矩阵,但是其实在求第 i行的状态时只依赖于第 i−1 行的状态,我们不需要存这么多的状态值,可以对空间进行优化。

  • 方法
    通常 DP 的空间优化思路有两种:

    1. 滚动数组

    2. 根据状态依赖调整迭代/循环的方向

比较推荐的是滚动数组,例子如斐波那契数列,

int ff(int n)
{
	f[0] = 0;
	f[1] = 1;
	f[2] = 1;
	for(int i = 3; i <= n; ++i)
		f[i] = f[i - 1] + f[i - 2];
	return f[n];
}

滚动数组下:

int ff(int n)
{
	f[1] = 0;
	f[2] = 1;
	for(int i = 2; i <= n; ++i)
	{
		f[0] = f[1];
		f[1] = f[2];
		f[2] = f[0] + f[1];
	}
	return f[2];
}

进一步可以修改为

void ff()
{
    int f[3];
    f[0] = 1;
    f[1] = 1;
    for (int i = 0; i < 100; i++)
    {
        f[i % 3] = f[(i - 1) % 3] + f[(i - 2) % 3];
    }
    printf("%d", f[99 % 3]);
}

对于DP问题,只需要将其中一维直接改成 %2,任何在将维的 f[i]改成 f[i%2] 即可,例如

int d[100][100];
for(int i = 100;i<100;i++){
    for(int j = 0;j<100;j++){
        d[i][j] = d[i-1][j]+d[i][j-1];
}
}

变为如下:

int d[2][100];
for(int i = 0;i<100;i++){
    for(int j = 0;j<100;j++){
        d[i%2][j] = d[(i-1)%2][j]+d[i%2][j-1];
}
}

因此,本题在滚动数组下,空间复杂度可以进一步下降,但是时间复杂度没有改变,代码如下:

class Solution(object):
    def minimumTotal(self, triangle):
        import numpy as np
        n = len(triangle)
        # f改为2*n维
        f = np.ndarray(shape=(2,n),dtype=np.object)
        f[0][0] = triangle[0][0]

        for i in range(n):
            for j in range(len(triangle[i])):
                if i>0 and j>0 and j<len(triangle[i])-1:
                    f[i%2][j] = min(f[(i-1)%2][j],f[(i-1)%2][j-1])+triangle[i][j]
                # 最右列 
                elif i>0 and j>0 and j==len(triangle[i])-1:
                    f[i%2][j] = f[(i-1)%2][j-1]+triangle[i][j]
                # 最左列
                elif i>0 and j==0:
                    f[i%2][j] = f[(i-1)%2][j]+triangle[i][j]
        min_ = f[(n-1)%2][0]
        
        for j in range(len(triangle[n-1])):
            min_ = min(min_,f[(n-1)%2][j])
        return min_
  • 方法二:倒过来推导(空间复杂度为O(1))
var minimumTotal = function(triangle) {
    const length = triangle.length;
    for(let i=length-2; i>=0; i--){
        const len = triangle[i].length;
        for(let j=0; j<len; j++){
            triangle[i][j] += Math.min(triangle[i+1][j],triangle[i+1][j+1])
        }
    }
    return triangle[0][0]
};
下降路径最小和

在这里插入图片描述
在这里插入图片描述

  • 自己:迭代修改matrix,原地递归

第一行不变,第二行变为自己+离自己最小的,第三行变为自己+前一行离自己最小的,以此类推,直到最后一行,返回最后一行中最小的

class Solution(object):
    def minFallingPathSum(self, matrix):
        m,n = len(matrix), len(matrix[0])
        for i in range(1,m):
            for j in range(n):
                if j==0:
                    matrix[i][j] = min(matrix[i-1][j], matrix[i-1][j+1]) + matrix[i][j]
                elif j==n-1:
                    matrix[i][j] = min(matrix[i-1][j], matrix[i-1][j-1]) + matrix[i][j]
                else:
                    matrix[i][j] = min(min(matrix[i-1][j], matrix[i-1][j-1]),matrix[i-1][j+1])+ matrix[i][j]
        return min(matrix[m-1])
下降路径最小和 II

在这里插入图片描述

  • 动态规划
    当我们在计算某行的状态值的时候,只会用到「上一行」的两个值:最小值和次小值,用i1 保存上一行的最小值对应的列下标,用 i2 保存次小值对应的列下标

下面代码出错了,咱也不知道为什么,调不好了

class Solution(object):
    def minFallingPathSum(self, grid):
        m,n = len(grid), len(grid[0])
        i1,i2 = -1,-1
        for j in range(n):

            if grid[0][j]<(100 if i1==-1 else grid[0][i1]):
                i2 = i1
                i1 = j
            elif grid[0][j]<(100 if i2==-1 else grid[0][i2]):
                i2 = j

        for i in range(1,m):
            ti1, ti2 = -1,-1
            # 找i行最小和次小过程中防止grid变化
            a = grid[i][:]
            for j in range(n):
                if a[j]<(100 if ti1==-1 else a[ti1]):
                    ti2 = ti1
                    ti1 = j
                elif a[j]<(100 if ti2==-1 else a[ti2]):
                    ti2 = j
                grid[i][j] = (grid[i-1][i1] if j!=i1 else grid[i-1][i2])+grid[i][j]
            
            i1,i2 = ti1,ti2
            
        return min(grid[m-1])

上述这些题目本质上对应的模型其实是:特定「起点」,明确且有限的「移动方向」(转移状态),求解所有状态中的最优值。而接下去我们要做的题目,只是告诉了我们移动规则,没有告诉我们具体该如何移动。可以当做是一类【路径问题】

统计所有可行路径

在这里插入图片描述
解释:有 x 0 , x 1 , . . . x 4 x_0,x_1,...x_4 x0,x1,...x4这四个地方,现在根据location可知 l o c ( x 0 ) = 2 loc(x_0)=2 loc(x0)=2 x 0 x_0 x0在位置2, x 1 x_1 x1在位置3,… x 4 x_4 x4在位置4。
则第一条路直接从 x 1 x_1 x1 x 3 x_3 x3距离为 ∣ l o c ( x 3 ) − l o c ( x 1 ) ∣ = 8 − 3 = 5 |loc(x_3)-loc(x_1)|=8-3=5 loc(x3)loc(x1)=83=5

  • 方法一:根据转移方程,递归
    f [ i ] [ f u e l ] f[i][fuel] f[i][fuel]为从位置i,剩余油量为 f u e l fuel fuel,到达end点的不同路径数量,则 f [ i ] [ f u e l ] = ∑ f [ k ] [ f u e l − n e e d ] f[i][fuel]=\sum f[k][fuel-need] f[i][fuel]=f[k][fuelneed],其中need是位置i到k消耗的油量,即距离。

我们要求的实际上就是 f [ s t a r t ] [ f u e l ] f[start][fuel] f[start][fuel],然后递归中间的位置

class Solution(object):
    def countRoutes(self, locations, start, finish, fuel):
        
        def dfs(pos,rest):
            if locations[pos]-locations[finish]>rest:
                return 0
            ans = 0
            for i in range(len(locations)):
                need = abs(locations[pos]-locations[i])
                if i != pos and need<=rest:
                    ans += dfs(i,rest-need)
            
            # 当前就在终点,不移动也算是一种方案
            if pos == finish:
                ans += 1
            return ans % 1000000007

        return dfs(start,fuel)
  • 方法二:转移方程,转移状态

f[i][j] 代表从位置 i 出发,当前剩余油量为 j的前提下,到达目的地的路径数量。

在这里插入图片描述
与方法一不同的是,这里我们是需要建立一个二维矩阵 f f f来存储所有变化,是直接记录更新在f中,而方法一是递归

class Solution(object):
    def countRoutes(self, locations, start, finish, fuel):
        import numpy as np
        n = len(locations)
        f = np.zeros(shape=(n,fuel+1))

        # 对于本身位置就在目的地的状态,路径数为 1
        # 这一步很重要
        for i in range(fuel+1):
            f[finish][i] = 1

        # f[i][fuel]=f[i][fuel]+f[k][fuel-need]
        for cur in range(fuel+1):
            for i in range(n):
                for k in range(n):
                    need = abs(locations[i]-locations[k])
                    if i!=k and need<=cur:
                        f[i][cur] += f[k][cur-need] 
                        f[i][cur] = f[i][cur]%1000000007
        return int(f[start][fuel])

当然也可以令f[i][j] 表示从Satrt出发,在当前剩余油量为 j的前提下到达i 的数目

那就得令

for i in range(fuel+1):
    f[start][i] = 1

  • 方法二:记忆搜索法
class Solution {
    int mod = 1000000007;
    
    // 缓存器:用于记录「特定状态」下的结果
    // cache[i][fuel] 代表从位置 i 出发,当前剩余的油量为 fuel 的前提下,到达目标位置的「路径数量」
    int[][] cache;
    
    public int countRoutes(int[] ls, int start, int end, int fuel) {
        int n = ls.length;
        
        // 初始化缓存器
        // 之所以要初始化为 -1
        // 是为了区分「某个状态下路径数量为 0」和「某个状态尚未没计算过」两种情况
        cache = new int[n][fuel + 1];
        for (int i = 0; i < n; i++) {
            Arrays.fill(cache[i], -1);
        }
        
        return dfs(ls, start, end, fuel);
    }
    
    /**
     * 计算「路径数量」
     * @param ls 入参 locations
     * @param u 当前所在位置(ls 的下标)
     * @param end 目标哦位置(ls 的下标)
     * @param fuel 剩余油量
     * @return 在位置 u 出发,油量为 fuel 的前提下,到达 end 的「路径数量」
     */
    int dfs(int[] ls, int u, int end, int fuel) {
        // 如果缓存器中已经有答案,直接返回
        if (cache[u][fuel] != -1) {
            return cache[u][fuel];
        }
        
        int n = ls.length;
        // base case 1:如果油量为 0,且不在目标位置
        // 将结果 0 写入缓存器并返回
        if (fuel == 0 && u != end) {
            cache[u][fuel] = 0;
            return 0;
        } 
        
        // base case 2:油量不为 0,且无法到达任何位置
        // 将结果 0 写入缓存器并返回
        boolean hasNext = false;
        for (int i = 0; i < n; i++) {
            if (i != u) {
                int need = Math.abs(ls[u] - ls[i]);    
                if (fuel >= need) {
                    hasNext = true;
                    break;
                }
            }
        }
        if (fuel != 0 && !hasNext) {
            cache[u][fuel] = u == end ? 1 : 0;
            return cache[u][fuel];
        }
        
        // 计算油量为 fuel,从位置 u 到 end 的路径数量
        // 由于每个点都可以经过多次,如果 u = end,那么本身就算一条路径
        int sum = u == end ? 1 : 0;
        for (int i = 0; i < n; i++) {
            if (i != u) {
                int need = Math.abs(ls[i] - ls[u]);
                if (fuel >= need) {
                    sum += dfs(ls, i, end, fuel - need);
                    sum %= mod;
                }
            }
        }
        cache[u][fuel] = sum;
        return sum;
    }
}

出界的路径数 576

在这里插入图片描述

  • 方法一:递归,终止条件是在界外或者在界内但是不能继续移动了
    这个逻辑没问题,但是超时了!好气,没办法只能继续换方法
class Solution(object):
    def findPaths(self, m, n, maxMove, startRow, startColumn):
        mod = 1000000007
        # 终止条件:移动一次就出界
        def dfs(m,n,cur_move,cur_i,cur_j):
            
            # 在界外
            if (cur_i>=m or cur_j>=n or cur_i<0 or cur_j<0):
                return 1
            # 在界内但是不能移动
            if cur_move == 0:
                return 0
            return dfs(m,n,cur_move-1,cur_i-1,cur_j)+dfs(m,n,cur_move-1,cur_i,cur_j-1)+\
                    dfs(m,n,cur_move-1,cur_i+1,cur_j)+dfs(m,n,cur_move-1,cur_i,cur_j+1)
        
        
        return int(dfs(m,n,maxMove,startRow,startColumn)%mod)
  • 方法二:DFS+剪枝
    既然普通的DFS无法满足条件,肯定是需要加上一些剪枝的技巧的,那我们来看看哪些地方可以剪枝呢?

试想,给定如下网络,小球在中间的位置,给定的移动次数为2,可以看到这时候小球不管怎么移动,都不会超出网格。

在这里插入图片描述
所以,剪枝技巧就是每次DFS的时候判断如果小球不管怎么移动都无法超出网格,那从这个点开始往后的枝就都可以剪掉了,简单修改下代码即可:

class Solution(object):
    def findPaths(self, m, n, maxMove, startRow, startColumn):
        mod = 1000000007
        # 终止条件:移动一次就出界
        def dfs(m,n,cur_move,cur_i,cur_j):
            
            # 在界外
            if (cur_i>=m or cur_j>=n or cur_i<0 or cur_j<0):
                return 1
            # 在界内但是不能移动
            if cur_move == 0:
                return 0

          	#剪枝:如果小球不管怎么移动都无法越出网格,那就剪掉这个枝
	        if (i - cur_move>= 0 && j - cur_move>= 0 && i + cur_move< m && j + cur_move< n):
	        	return 0
            
            return dfs(m,n,cur_move-1,cur_i-1,cur_j)+dfs(m,n,cur_move-1,cur_i,cur_j-1)+\
                    dfs(m,n,cur_move-1,cur_i+1,cur_j)+dfs(m,n,cur_move-1,cur_i,cur_j+1)
        
        
        return int(dfs(m,n,maxMove,startRow,startColumn)%mod)

但是,依然超时,还是只能另想方法

  • 方法三:记忆化搜索

剪枝也不行,我们再深入思考一下。

请看下图,假设我们的起始位置是在 A 位置,最多可以移动 6 步,我们可以很容易地发现,会有很多次经过B位置的情况,而从 B 位置出去我们只需要计算一次就可以了,比如,图中列举了三种到达 B 位置的情况。

在这里插入图片描述
所以,第三种方法,我们需要增加一个缓存,记录下来从每个位置在给定移动次数的范围内可以越界的次数,这就是记忆化搜索。也就是说,我们之前DFS是一个个深入搜索到边界才开始返回,我们现在其实可以增加矩阵稍微记录下每个位置在不同次数下到边界的位置,这样中间位置不用走到边界,只需要搜索到离边界较近的点就行。

请看代码,我们增加了一个三维数组作为缓存,前两维表示位置,第三维表示移动次数,例如 m e m o [ i ] [ j ] [ k ] memo[i][j][k] memo[i][j][k]表示位置(i,j)在移动次数为k时出界的路径数

class Solution(object):
    def findPaths(self, m, n, maxMove, startRow, startColumn):
        mod = 1000000007
        # f.shape=(m,n,maxMove+1)
        f = [[[0] * (maxMove + 1) for _ in range(n)] for _ in range(m)]
        
        for i in range(m):
            for j in range(n):
                for k in range(maxMove+1):
                    f[i][j][k] = -1
        
        def dfs(m,n,cur_move,cur_i,cur_j):
            # 上下左右移动方向
            dirs = [[-1,0],[0,-1],[1,0],[0,1]]
            
            # 在界外
            if (cur_i>=m or cur_j>=n or cur_i<0 or cur_j<0):
                return 1
            # 在界内但是不能移动
            if cur_move == 0:
                return 0

            # 剪枝:如果小球不管怎么移动都无法越出网格,那就剪掉这个枝
	        if (cur_i - cur_move>= 0 and cur_j- cur_move>= 0 and \
                        cur_i + cur_move< m and cur_j + cur_move< n):
	        	return 0

            # 缓存中存在,这个是关键!
            # 已经算过就不用重新再递归
            if (f[cur_i][cur_j][cur_move] != -1):
                return f[cur_i][cur_j][cur_move]
            

            # 边界内,且可以继续移动
            sum_ = 0
            for dir in dirs:
                sum_ = (sum_+dfs(m,n,cur_move-1,cur_i+dir[0],cur_j+dir[1]))%mod
            f[cur_i][cur_j][cur_move] = sum_
            return sum_
        
        return int(dfs(m,n,maxMove,startRow,startColumn))
  • 方法四:动态规划,返回f[startRow][startColumn][maxMove]

d p [ i ] [ j ] [ k ] dp[i][j][k] dp[i][j][k]表示从 [ i , j ] [i,j] [i,j] 位置最多移动 k k k 次能够把小球移出去的最大路径数量;
d p [ i ] [ j ] [ k ] = d p [ i − 1 ] [ j ] [ k − 1 ] + d p [ i + 1 ] [ j ] [ k − 1 ] + d p [ i ] [ j − 1 ] [ k − 1 ] + d p [ i ] [ j + 1 ] [ k − 1 ] dp[i][j][k] = dp[i-1][j][k-1] + dp[i+1][j][k-1] + dp[i][j-1][k-1] + dp[i][j+1][k-1] dp[i][j][k]=dp[i1][j][k1]+dp[i+1][j][k1]+dp[i][j1][k1]+dp[i][j+1][k1]
注意边界条件,如果是正方形的四个顶点,有两种方法越界,其他边上的位置只有一种方法越界
最后返回的是f[startRow][startColumn][maxMove]

class Solution(object):
    def findPaths(self, m, n, maxMove, startRow, startColumn):
        mod = 1000000007
        # 上下左右四个方向
        dirs = [[-1,0],[0,-1],[1,0],[0,1]]
        
        f = [[[0] * (maxMove + 1) for _ in range(n)] for _ in range(m)]

        for k in range(1,maxMove+1):
            for i in range(m):
                for j in range(n):
                    # 边界上,其中四个角是加了两次
                    if i==0:
                        f[i][j][k] += 1
                    if j==0:
                        f[i][j][k] += 1
                    if i==m-1:
                        f[i][j][k] += 1
                    if j==n-1:
                        f[i][j][k] += 1
                    
                    # 中间位置向四个方向移动
                    for dir in dirs:
                        next_i, next_j = i+dir[0], j+dir[1]
                        if next_i>= 0 and next_i < m and next_j >= 0 and  next_j < n:
                            f[i][j][k] = (f[i][j][k]+f[next_i][next_j][k-1])%mod
                            
        return f[startRow][startColumn][maxMove]
最大得分的路径数目 1301

在这里插入图片描述
-方法一(自己):动态规划,用 f [ i ] [ j ] [ k ] f[i][j][k] f[i][j][k]表示从(i,j)到E的每条路长度,是一个列表形式

对于位置(i,j)用列表记录下,从(i,j)到E的每条路长度,例如(i,j)到E
3条路,长度为2,1,4,那我就记 f [ i ] [ j ] = [ 2 , 1 , 4 ] f[i][j]=[2,1,4] f[i][j]=[2,1,4]

求任意位(i,j)路长度列表,就是考察前一个,也就是左、上、左上位置上有几条路,分别长度是多少,再加上(i,j)当前的值。注意遇到X,就略过这个点。

但是,超时了!

class Solution(object):
    def pathsWithMaxScore(self, board):
        mod = 1000000007
        dirs = [[-1,0],[0,-1],[-1,-1]]
        m, n = len(board), len(board[0])
        f = [[[ ] for _ in range(n)] for _ in range(m)]
        f[0][0]=[0]
        board[0] = board[0].replace('E','0')
        board[m-1] = board[m-1].replace('S','0')


        for i in range(m):
            for j in range(n):
                if board[i][j]!='X':
                    for dir in dirs:
                        nextI,nextJ = i+dir[0], j+dir[1]
                        if nextI>=0 and nextJ>=0:
                            # 避开X,因为遇到X,路径列表为[ ]
                            if len(f[nextI][nextJ])>0:
                                for path in f[nextI][nextJ]:
                                    f[i][j].append((int(board[i][j])+path)%mod)
        if len(f[m-1][n-1])==0:
            return [0,0]
        else:
            long = max(f[m-1][n-1])
            count = 0
            for i in range(len(f[m-1][n-1])):
                if f[m-1][n-1][i]==long:
                    count += 1
            return [long, count]
  • 方法二:考虑优化,我们不需要记录下(i,j)到E的所有路径长度,只需要记录下最长的就行。用两个矩阵分别记录下(i,j)到E的最大路径长度,以及数量。

    f : f [ i ] [ j ] f:f[i][j] ff[i][j]表示从[i,j]位置到E经过路径的最大和
    c o u n t : c o u n t [ i ] [ j ] count:count[i][j] countcount[i][j]表示[i,j]位置到E经过路径的最大和的数目

首先初始化f和cost数组,f中所有值默认-inf,count中所有值默认0

同时, i = 0 i=0 i=0 j = 0 j=0 j=0两条边界上的点,一个只能向左一个只能向上,如果遇到了’X’,直接break。
状态转移时,比较上方、左方和左上方的大小,更新f数组,如果得到的f[i][j]为-inf,说明当前位置不能转移得到,continue,否则根据前一步转移过来的值更新count数组

class Solution(object):
    def pathsWithMaxScore(self, board):
        inf = float('inf')
        mod = 1000000007
        dirs = [[-1,0],[0,-1],[-1,-1]]
        n = len(board)
        f = [[-inf]*n for _ in range(n)]
        count = [[0]*n for _ in range(n)]

        board[0] = board[0].replace('E','0')
        board[n-1] = board[n-1].replace('S','0')
        
        # 初始化[0,0]位置
        f[0][0] = 0
        count[0][0]=1

        # 初始化第一行和第一列
        for j in range(1,n):
            if board[0][j]=='X':
                break
            f[0][j]= (f[0][j-1]+int(board[0][j]))%mod
            count[0][j]=1

        for i in range(1,n):
            if board[i][0]=='X':
                break
            f[i][0] = (f[i-1][0]+int(board[i][0]))%mod
            count[i][0] = 1
        
        # 其余点
        for i in range(1,n):
            for j in range(1,n):
                if board[i][j]=='X':
                    continue
                # 计算最大值  
                f[i][j]=max(f[i-1][j-1],f[i][j-1],f[i-1][j])
                if f[i][j]==-inf:continue
                if f[i][j]==f[i-1][j-1]:
                    count[i][j]+=count[i-1][j-1]
                if f[i][j]==f[i][j-1]:
                    count[i][j]+=count[i][j-1]
                if f[i][j]==f[i-1][j]:
                    count[i][j]+=count[i-1][j]
                f[i][j] = (f[i][j]+int(board[i][j]))%mod
                count[i][j] = count[i][j]%mod

        return [f[n-1][n-1] if f[n-1][n-1]!=-inf else 0, count[n-1][n-1]]

双指针

双指针主要用于遍历数组,两个指针指向不同的元素,从而协同完成任务。也可以延伸到多个数组的多个指针。

若两个指针指向同一数组,遍历方向相同且不会相交,则也称为滑动窗口(两个指针包围的区域即为当前的窗口),经常用于区间搜索。

若两个指针从两端开始,但是遍历方向相反,则可以用来进行搜索,待搜索的数组往往是排好序的。

15. 三数之和

在这里插入图片描述注意:不含重复元素

数组排序,再遍历i,思路就是固定i,然后再用双指针遍历i后面的数,找到相加和为0的

class Solution(object):
    def threeSum(self, nums):
        n, res = len(nums), []
        if n<3:
            return res
        nums.sort()

        for i in range(n):
            # nums[i]>0,排序后后面的肯定也比0大,找不到返回
            if nums[i]>0: 
                return res

            # 与前一个重复,跳过
            if i>0 and nums[i] == nums[i-1]:
                continue

            # 双指针找和
            L, R = i+1, n-1
            while L<R:
                # 三数和为0,L往后到与当前不同的,R往左移
                if nums[i]+nums[L]+nums[R] == 0:
                    res.append([nums[i],nums[L],nums[R]])
                    while L<R and nums[L]==nums[L+1]:
                        L += 1
                    while L<R and nums[R]==nums[R-1]:
                        R -= 1
                    L += 1
                    R -= 1
                    
                elif nums[i]+nums[L]+nums[R] > 0:
                    R -= 1
                else:
                    L += 1 

        return res
167. 两数之和 II - 输入有序数组

在这里插入图片描述

  • 思路:双指针分别指向两端,如果和太大,则右指针前移,太小左指针往后,刚好就返回。

这比较简单的原因在于,题目假设一定会找到有且仅有一对满足条件的和

class Solution(object):
    def twoSum(self, numbers, target):
        left, right = 0, len(numbers)-1
        while left<right:
            if numbers[left]+numbers[right] == target:
                return [left+1,right+1]
            elif numbers[left]+numbers[right] > target:
                right -= 1
            else:
                left += 1
88. 合并两个有序数组(Easy)

在这里插入图片描述

  • 因为 nums1 的空间都集中在后面,所以从后向前处理排序的数据会更好,节省空间,一边遍历一边将值填充进去

  • 设置指针 p1 和 p2 分别指向 nums1 和 nums2 的有数字尾部,再设置pos指向合并的最尾端,即p1=m-1,p2=n-1,pos=m+n-1

  • 比较nums[p1]和nums[p2],哪个大就填入到nums[pos]中,例如nums[p2]大,则p2–,pos–

举例说明,nums1=[1,3,5,0,0,0],nums2=[1,2,3]
初始时p1,p2,pos=2,2,5。比较两个数组末端5>3,所以nums[5]=5,然后p1往前移,接着就是比较3和3,我们算第二个大,就是nums[4]=3,然后p2前移,比较的是1和3,后者大,所以nums[3]=3,p2往前移,然后比较的是1和2,后者大,所以nums[2]=2,p2往后移,然后比较的是1和1,后者大,所以nums[1]=1,p2往前移,p1已经到-1了,结束。

注意:如果nums1的p1遍历完了,那后面都是加nums2的数,所以当p1<0的时候,nums[pos]=nums2[p2]

class Solution(object):
    def merge(self, nums1, m, nums2, n):
        
        p1, p2,pos = m-1,n-1,m+n-1
        while p2>=0:
            if p1<0 or nums2[p2]>=nums1[p1]:
                nums1[pos] = nums2[p2]
                p2 -= 1
            elif p1>=0 and nums2[p2]<nums1[p1]:
                nums1[pos] = nums1[p1]
                p1 -= 1
            pos -= 1
            
        return nums1
633. 平方数之和(Easy)

在这里插入图片描述

class Solution(object):
    def judgeSquareSum(self, c):
        
        left, right = 0, int(c**0.5)

        while left<=right:
            if left*left+right*right==c:
                return True
            elif left*left+right*right>c:
                right -= 1
            else:
                left += 1
        return False
680. 验证回文字符串 Ⅱ(Easy)

在这里插入图片描述一左一右向中间遍历字符串,如果遇到不等的判断去掉左边或者去掉右边是否为回文。

class Solution {
public:
    bool validPalindrome(string s) {

        int l=0,r=s.size()-1;

        while(l<=r){
            if(s[l]!=s[r]){
                    return huiwen(l,r-1,s)||huiwen(l+1,r,s);
            
            }
            ++l;
            --r;
        }

        return true;

    }
private:
bool huiwen(int l,int r,const string &s){
    while(l<=r){
        if(s[l]!=s[r]){
            return false;
        }
        ++l;
        --r;
    }
    return true;
}
};

142. 环形链表 II - 快慢指针

在这里插入图片描述

  • 快慢指针(Floyd 判圈法)

给定两个指针,分别命名为 slow 和 fast,起始位置在链表的开头。每次 fast 前进两步,slow 前进一步。如果 fast可以走到尽头,那么说明没有环路;如果 fast 可以无限走下去,那么说明一定有环路,且一定存在一个时刻 slow 和 fast 相遇。

当 slow 和 fast 第一次相遇时,我们将 fast 重新移动到链表开头,并让 slow 和 fast 每次都前进一步。当 slow 和 fast 第二次相遇时,相遇的节点即为环路的开始点。

原理:https://blog.csdn.net/qq_45928520/article/details/118787532

# Definition for singly-linked list.
# class ListNode(object):
#     def __init__(self, x):
#         self.val = x
#         self.next = None

class Solution(object):
    def detectCycle(self, head):
        fast, slow = head, head
        while True:
            if not (fast and fast.next): return
            fast, slow = fast.next.next, slow.next
            if fast == slow: break
        fast = head
        while fast != slow:
            fast, slow = fast.next, slow.next
        return fast

java

public class Solution {
    public ListNode detectCycle(ListNode head) {
        ListNode fast = head;
        ListNode slow = head;
        while (fast != null && fast.next != null) {
            fast = fast.next.next;
            slow = slow.next;
            if (fast == slow) {
                fast = head;
                while (fast != slow) {
                    fast = fast.next;
                    slow = slow.next;
                }
                return slow;
            }
        }
        return null;
    }
}
76. 最小覆盖子串 (Hard)- 滑动窗口

在这里插入图片描述
思路:https://leetcode-cn.com/problems/minimum-window-substring/solution/tong-su-qie-xiang-xi-de-miao-shu-hua-dong-chuang-k/

class Solution:
    def minWindow(self, s: str, t: str) -> str:
        need_dict = collections.defaultdict(int) #用来装分别需要的字符数量,包括必要的和非必要的,需要的必要字符数量>0,非必要的字符数量<0
        for ch in t:   #初始化需要的必要字符数量
            need_dict[ch] += 1
        need_cnt = len(t) #用来判断总共需要多少字符才能达到要求
        i = j = 0
        res = [0,float('inf')]
        for j in range(len(s)):
            if need_dict[s[j]] > 0: #只有必要字符的数量才可能>0
                need_cnt -= 1
            need_dict[s[j]] -= 1  #任意值都可以装进need_dict,但是非必要字符只可能<0
            if need_cnt == 0:  # 需要的字符都足够了
                while need_cnt == 0: #开始准备右移左指针,缩短距离
                    if j-i < res[1] - res[0]: #字符串更短,替换答案
                        res = [i,j]
                    need_dict[s[i]] += 1    #在移动左指针之前先将左指针的值加回来,这里可以是非必要字符
                    if s[i] in t and need_dict[s[i]] > 0: #确认是必要字符且不多于所需要的数量(有多余的话只可能<=0,因为上一句我们已经将字符+1了)后,将need_cnt+1
                        need_cnt += 1    
                    i += 1   #右移左指针,寻找下一个符合的子串
        return s[res[0]:res[1]+1] if res[1]-res[0]<len(s) else '' 

二分查找

「二分」的本质是二段性,只要一段满足某个性质,另外一段不满足某个性质,就可以用「二分」。

34. 在排序数组中查找元素的第一个和最后一个位置

在这里插入图片描述- 自己:双指针,分别从两端遍历

class Solution(object):
    def searchRange(self, nums, target):
        if len(nums)==0:
            return [-1,-1]
        
        if len(nums)==1:
            if nums[0]==target:
                return [0,0]
            else:
                return [-1,-1]
        
        left, right = 0, len(nums)-1
        while left<=right:
            if nums[left]!=target:
                left += 1
            if nums[right]!=target:
                right -= 1
            if nums[left]==target and nums[right]==target:
                return [left,right]
        
        return [-1,-1]

  • 方法二:二分法

注意:这里题目给的数组是升序的,所以可以用二分法

两次二分,第一次查找符合target的最右边的位置,第二次查找符合target的最左边的位置。查找成功则返回两个位置下标,否则返回[−1,−1]。

二分法如何查找最左边的位置?两端节点设为left和right,划分为两个区间,区间中点是left+right取下整,即mid=int(left+right/2),这样左子区间更小。

判断target和中间大小,如果中间点大于等于tagert这说明target在左子区间,令right=mid,否则说明target在左子区间,且mid对应的值比target小,所以left=mid+1。这里强调中间点大于等于tagert,是遍历结束后,可以取到符合target的最左边端点,否则取到等号的就是左边端点。这样得到的就是target的最左边端点

二分法如何查找最右边的位置?两端节点设为left和right,划分为两个区间,区间中点是left+right取上整,即mid=int(left+right+1/2),这样右子区间更小。

判断target和中间大小,如果中间点大于等于tagert这说明target在右子区间,令left=mid,否则说明target在左子区间,所以right=mid-1。

  • 不分左右边界
int binary_search(int[] nums, int target) {
    int left = 0, right = nums.length - 1; 
    while(left <= right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid - 1; 
        } else if(nums[mid] == target) {
            // 直接返回
            return mid;
        }
    }
    // 直接返回
    return -1;
}
  • 左边界
  1. 两端都闭
  2. right = nums.length - 1
  3. 由于 while 的退出条件是 left == right + 1 ,所以当 target ⽐ nums 中
    所有元素都⼤时,会存在以下情况使得索引越界:
int left_bound(int[] nums, int target) {
    int left = 0, right = nums.length - 1;
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid - 1;
        } else if (nums[mid] == target) {
            // 别返回,锁定左侧边界
            right = mid - 1;
        }
    }
    // 最后要检查 left 越界的情况
    if (left >= nums.length || nums[left] != target)
        return -1;
    return left;
}
  • 右边界:
int right_bound(int[] nums, int target) {
    int left = 0, right = nums.length - 1;
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid - 1;
        } else if (nums[mid] == target) {
            // 别返回,锁定右侧边界
            left = mid + 1;
        }
    }
    // 最后要检查 right 越界的情况
    if (right < 0 || nums[right] != target)
        return -1;
    return right;
}

81. 搜索旋转排序数组 II

在这里插入图片描述
即使数组被旋转过,我们仍然可以利用这个数组的递增性,使用二分查找。对于当前的中点,如果它指向的值小于等于右端,那么说明右区间是排好序的;反之,那么说明左区间是排好序的。如果目标值位于排好序的区间内,我们可以对这个区间继续二分查找;反之,我们对于另一半区间继续二分查找。

注意,因为数组存在重复数字,如果中点和左端的数字相同,我们并不能确定是左区间全部相同,还是右区间完全相同。在这种情况下,我们可以简单地将左端点右移一位,然后继续进行二分查找。

class Solution(object):
    def search(self, nums, target):
        left, right = 0, len(nums)-1

        while left<=right:
            print(left,right)
            mid = int((left+right)/2)
            if nums[mid]==target:
                return True
            # 找不出哪个区间是排序好的
            if nums[mid]==nums[left] and nums[mid]==nums[right]:
                left += 1
            # 右边是排序好的,对右边进行二分查找
            elif nums[mid]<=nums[right]:
                if nums[mid]<target and nums[right]>=target:
                    left = mid+1
                else:
                    right = mid-1
            # 左边是排序好的,对左边进行二分查找
            else:
                if nums[mid]>target and nums[left]<=target:
                    right = mid-1
                else:
                    left = mid+1
            
        return False
154. 寻找旋转排序数组中的最小值 II

在这里插入图片描述
首先,比较迷惑的点是可以旋转多次,会让人误以为旋转多次后的数字呈现出几段几段的升序,但是我们注意到其实旋转多次,最终只会分为两段升序。如[0,1,2,3,4,5,6]先随便按照数字3划分旋转为[4,5,6,0,1,2,3],再根据数字2划分旋转为[3,4,5,6,0,1,2],再根据数字5划分旋转为[6,0,1,2,3,4,5],可以发现多次旋转后得到的还是最多还是两段,且划分点就是最小点。

那么,我们的问题就变成了找到不同两段的划分点,但这里还有个难点就是本题元素并不唯一。因为「二分」的本质是二段性,并非单调性。只要一段满足某个性质,另外一段不满足某个性质,就可以用「二分」。

首先,取中点mid为(left + right) // 2取下整,划分为左右两个子区间。

  • 如果nums[mid] > nums[right],说明要找的划分点在右子区间,将区间调整到右子区间,即left = mid + 1;
  • 如果nums[mid] < nums[right],说明右子区间是升序的,要找的划分点在左边,将区间调整到左子区间,即right = mid;
  • 如果nums[mid] =nums[right],我们并不能确定划分点是在左区间全部右区间,这时关键做法是令right = right - 1

这样会不会正好划分点是right,从而遗漏掉呢?并不会,若划分点是right,那么由于nums[mid] =nums[right],说明别的地方还有这个划分点的数值。

最后返回nums[left],这是为什么呢?也可以返回nums[right],因为循环结束的时候是left=right

class Solution:
    def findMin(self, nums):
        left, right = 0, len(nums) - 1
        while left < right:
            mid = (left + right) // 2
            if nums[mid] > nums[right]: left = mid + 1
            elif nums[mid] < nums[right]: right = mid
            else: right = right - 1 # key
        return nums[left]

540. 有序数组中的单一元素

在这里插入图片描述

  • 思路:根据划分区间为奇偶判断,如果划分区间为奇, eg[3,3,7, 7 ,10,11,11],则如果区间中点和右邻点不同,则仅出现一次的点在右区间中间点。
class Solution(object):
    def singleNonDuplicate(self, nums):
        left, right = 0, len(nums)-1
        # 区间实行左闭右开,即取得到左边取不到右边
        while left<right:
            mid = (left+right)//2
            
            if (mid-left)%2==0:
                # eg[1,1,2,4, 4 ,6,6,7,7],在右区间取不到右端点
                if nums[mid]==nums[mid-1]:
                    right = mid-1
                # eg[1,1,2,2, 4 ,4,6,7,7]
                elif nums[mid]==nums[mid+1]:
                    left = mid+2
                else:
                    return nums[mid]
            else:
                
                # eg[3,3,7, 7 ,10,11,11],在右区间
                if nums[mid]==nums[mid-1]:
                    left = mid+1
                # eg[3,3,6, 10 ,10,11,11]
                elif nums[mid]==nums[mid+1]:
                    right = mid-2
                else:
                    return nums[mid]
        return nums[left]
  • 也可以先根据中间点和邻点是否相同,再根据划分区间为奇偶判断
class Solution {
    public int singleNonDuplicate(int[] nums) {
        int left=0;
	    int right=nums.length-1;
	    while(left<right){
	        int mid=left+(right-left)/2;
	        if(nums[mid] == nums[mid-1]){//中点跟左边的相等,则判断除开中点,左边还剩几位数;
                if((mid-left)%2 == 0){//若为偶数,则说明左边的存在答案值,改变right的值
                   right = mid-2;
               } else {//若为奇数,则说明右边的存在答案值,改变left的值
                   left = mid+1;
               }
           } else if(nums[mid] == nums[mid+1]){//中点跟右边的相等,则判断除开中点,右边还剩几位数;
               if((right-mid)%2 == 0){//若为偶数,则说明右边的存在答案值,改变left的值
                   left = mid+2;
               } else {//若为奇数,则说明左边的存在答案值,改变right的值
                   right = mid-1;
               }
           } else{//中点跟左右都不相等,直接返回
        	  return nums[mid];
           }
	    }
        
        return nums[right];
    }
}
4. 寻找两个正序数组的中位数(Hard)

在这里插入图片描述
注意,这里规定了:算法的时间复杂度应该为 O(log (m+n))

思路:nums1和nums2拼起来构成了长度为m+n的数组,那么要找到新数组的中位数,当 m+n 是奇数时,中位数是两个有序数组中的第 (m+n)/2 个元素,当 m+n是偶数时,中位数是两个有序数组中的第 (m+n)/2个元素和第 (m+n)/2+1个元素的平均值。所以,我们就是找到其中第 k小的数,其中 k为 (m+n)/2 或 (m+n)/2+1,当m+n为偶数的时候,这个k就需要找两个然后求平均。

  • 如何找到第k小的数字?

举例说明,现在m+n=14,所以我们需要找到第7和第8小的数字。

我们令k=7,nums1和nums2先各看k/2=3个数字,比较nums1[2]和nums2[2]哪个大。显然,nums1里面更大,这说明nums2中前3个都比nums1[2]=4来的小。说明我们排序的时候,肯定是123…4,那么nums2中的123就肯定不是第7小的数字。
在这里插入图片描述
这样,我们排除123三个数字之后,还需要再找到4个数字,就能找到第7小的数字。所以下一步,两个数组中各看2个数,也就是到了如下图所示

在这里插入图片描述
5比3大,说明nums1中的1和3,也是排在前面的,这样一来,我们就找到了5个数字,只需要再找到2个就行,所以下面我们两个数组各看一个数,如下图所示

在这里插入图片描述4和4相同,随便去掉一个就行,我们假设去掉下面的4。那么就剩下最后一个数字没有找到了,我们只要比较哪个小就行。

在这里插入图片描述如果某个数组长度过小,没有k/2个数字,那么我们就看他整个长度就行,不用看满k/2

public double findMedianSortedArrays(int[] nums1, int[] nums2) {
    int n = nums1.length;
    int m = nums2.length;
    int left = (n + m + 1) / 2;
    int right = (n + m + 2) / 2;
    //将偶数和奇数的情况合并,如果是奇数,会求两次同样的 k 。
    return (getKth(nums1, 0, n - 1, nums2, 0, m - 1, left) + getKth(nums1, 0, n - 1, nums2, 0, m - 1, right)) * 0.5;  
}
    
    private int getKth(int[] nums1, int start1, int end1, int[] nums2, int start2, int end2, int k) {
        int len1 = end1 - start1 + 1;
        int len2 = end2 - start2 + 1;
        //让 len1 的长度小于 len2,这样就能保证如果有数组空了,一定是 len1 
        if (len1 > len2) return getKth(nums2, start2, end2, nums1, start1, end1, k);
        if (len1 == 0) return nums2[start2 + k - 1];

        if (k == 1) return Math.min(nums1[start1], nums2[start2]);

        int i = start1 + Math.min(len1, k / 2) - 1;
        int j = start2 + Math.min(len2, k / 2) - 1;

        if (nums1[i] > nums2[j]) {
            return getKth(nums1, start1, end1, nums2, j + 1, end2, k - (j - start2 + 1));
        }
        else {
            return getKth(nums1, i + 1, end1, nums2, start2, end2, k - (i - start1 + 1));
        }
    }
    

排序算法

python中多个字段Sorted

l = [ [1,6],[2,4],[1,3]]按照第一个字段升序,按照第二个字段降序

l = sorted(l, key = lambda x:(x[0],-x[1]), reverse=False)

常见的排序算法

  • 常见算法可以分为两大类:

1. 非线性时间比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此称为非线性时间比较类排序。

2 线性时间非比较类排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此称为线性时间非比较类排序。
在这里插入图片描述在这里插入图片描述

冒泡排序(Bubble Sort)

思路:两层循环,外层循环从1到n-1,内层循环比较相邻两个元素 ,直到第n-1-i为止,如果逆序就交换。外层第一趟循环就把最大的放到了最前面,所以第二次循环的时候就不需要再和最后一个比较。时间复杂度 O ( n ) O(n) O(n),空间复杂度 O ( 1 ) O(1) O(1)

for(int i=0;i<arr.length-1;i++){//外层循环控制排序趟数
      for(int j=0;j<arr.length-1-i;j++){//内层循环控制每一趟排序多少次
        if(arr[j]>arr[j+1]){
          int temp=arr[j];
          arr[j]=arr[j+1];
          arr[j+1]=temp;
        }
      }
    }
选择排序(Selection Sort)

思路:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

具体操作:

  1. 初始i=0,假设最小点min=i,找出后面最小的数位置记为j,交换min和j
  2. 在i时,假设最小点min=i,找出后面最小的数位置记为j,交换min和j

时间复杂度 O ( n 2 ) O(n^2) O(n2),空间复杂度 O ( 1 ) O(1) O(1)

private static void sort(int[] array) {
        int n = array.length;
        for (int i = 0; i < n-1; i++) {
            int min = i;
            for (int j = i+1; j < n; j++) {
                if (array[j] < array[min]){//寻找最小数
                    min = j;                      //将最小数的索引赋值
                 }
            }
            int temp = array[i];
            array[i] = array[min];
            array[min] = temp;

        }
    }
插入排序(Insertion Sort)
  • 思路:假设前面n-1(其中n>=2)个数已经是排好顺序的,现将第n个数插到前面已经排好的序列中,然后找到合适自己的位置,使得插入第n个数的这个序列也是排好顺序的。

  • 具体:从第二位数字开始,每一个数字都试图跟它的前一个比较并交换,并重复;直到前一个数字不存在或者比它小或相等时停下来。

  • 举例: [ 4 , 1 , 10 , 3 , 8 ] [4,1,10,3,8] [4,1,10,3,8]
    i=1,key=1,从后往前遍历key前面的数,如果比key大,就说明key要插到他们前面去,所以他们下标后移让个位置,直到不能在移动,就找到了要插入的位置,令 array[j+1] = key

i=3,key=3,此时3前面的数字是[1,4,10],从后往前遍历,array[3]=10,array[2]=4,到array[0]为止,所以array[1]=3插入结束。

  • 时间复杂度 O ( n 2 ) O(n^2) O(n2),空间复杂度 O ( 1 ) O(1) O(1)
private static void sort(int[] array) {
        int n = array.length;
    /**
    *从第二位数字开始,每一个数字都试图跟它的前一个比较并交换,并重复;直到前一个数字不存在或者比它小或相等时停下来
    **/
        for (int i = 1; i < n; i++) {//从第二个数开始
            int key = array[i];
            int j = i -1;
            while (j >= 0 && array[j]>key) {
                array[j + 1] = array[j];     //交换
                j--;                                //下标向前移动
            }
            array[j+1] = key;
        }
    }
希尔排序

https://blog.csdn.net/qq_37266079/article/details/104942787

希尔排序是直接插入排序的改进,传统的之间插入排序对小规模数据或是基本有序数据时十分高效,而希尔排序对于直接插入排序的改进使其对于中等规模的数据的性能表现还不错。

  • 思路
    把较大的数据集合分割成若干个小组(逻辑上分组,用gap),然后对每一个小组分别进行插入排序,此时,插入排序所作用的数据量比较小(每一个小组),插入的效率比较高。

对于数列(5,7, 8,3, 1,2, 4,6),共8个数,gap从8/2=4开始,即把下标相差4的分到一组,比如这个例子中a[0]与a[4]是一组、a[1]与a[5]是一组…,这里的差值(距离)被称为gap。然后对这一组的数据进行直接插入排序

while (j >= 0 && temp < data[j])
			{    
				//temp小于前面有序区的元素,需要继续往前寻找合适位置插入 
				//所以有序区的和temp比较过的元素需要跟着向后移动 
				data[j+gap] = data[j];
			    j = j-gap;
			}
			//在合适位置插入temp 
			data[j+gap] = temp;

gap从最大4开始递减直到1


/*
希尔排序的排序思路是:
把较大的数据集合分割成若干个小组(逻辑上分组),
然后对每一个小组分别进行插入排序,
此时,插入排序所作用的数据量比较小(每一个小组),插入的效率比较高
*/
#include <stdio.h>

void ShellSort(int data[],int n)
{   
    int gap = n/2;	  //增量设置初值
    
    int i,j;
    while (gap>0)
    {   
		for (i=gap; i<n; i++)
	    {    
			//对相隔gap位置的元素组直接插入排序(temp就是我们要插入的元素)
		    int temp = data[i];
			j = i-gap;
			 
			while (j >= 0 && temp < data[j])
			{    
				//temp小于前面有序区的元素,需要继续往前寻找合适位置插入 
				//所以有序区的和temp比较过的元素需要跟着向后移动 
				data[j+gap] = data[j];
			    j = j-gap;
			}
			//在合适位置插入temp 
			data[j+gap] = temp;
	    }
	    gap = gap/2;	 //减小增量
    }
}

int main()
{
    int value[] = {8,3,6,2,4,5,7,1,9,0};
    
    printf("排序前的数据为:\n");
    for(int i=0; i<10;i++)
        printf("%d  ",value[i]);
    printf("\n\n");
    
    ShellSort(value,10);
    
    printf("排序后的结果为:\n");
    for(int i=0; i<10;i++)
        printf("%d  ",value[i]);
    printf("\n");
    
    return 0;
}

快速排序(Quicksort)
  • 思路:递归的思想,随便在数组中选择一个数作为基准,然后双指针遍历两端,把比基准大的数都放在基准右边,基准小的都放左边,然后再用同样的方法遍历基准两边。

难点:怎么把数放在基准两端,见下图动画,key是基准,low和high是两端指针,如果lo指向的数大于key,就把值赋给high,否则不动。直到low和high移动到同一位置,这个位置填基准。
在这里插入图片描述


def quick(nums,left,right):
       if left >= right:
           return
       l = left
       r = right
       key = nums[l]
       while l<r:
       	# 两个while顺序不能换
           while l<r and nums[r]>=key :
               r-=1
           nums[l] = nums[r]
           while l<r and  nums[l]<key:
               l+=1
           nums[r] = nums[l]
       
       nums[r]=key
       quick(nums,left,l-1)
       quick(nums,l+1,right)

  • 为什么快速排序是平均性能最好的排序算法,其优渥之处体现在哪?

回答:不需要多余的比较,例如我们已经知道a<b<c,那么a和c就不会再比较,因为b是递归排序的基准,所以a和c只会在自己的区间比较,这种情况在别的排序中可能会存在,但是在快排中能避免这种冗余的比较。

  • 时间复杂度平均和最好是 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n),最差是 O ( n 2 ) O(n^2) O(n2),空间复杂度 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)
  1. 什么时候时间复杂度达到 O ( n 2 ) O(n^2) O(n2)是最差的?
    最差的情况,每次选基准都选到了最小的

  2. 什么时候达到最好

最好的是,每次选择基准都选到了中点

在这里插入图片描述

参考:https://zhuanlan.zhihu.com/p/93129029

归并排序(Merge Sort)

-思路:它使用了递归分治的思想,分阶段可以理解为就是递归拆分子序列的过程,递归深度为log2n。再来看看治阶段,我们需要将两个已经有序的子序列合并成一个有序序列,比如上图中的最后一次合并,要将[4,5,7,8]和[1,2,3,6]两个已经有序的子序列,合并为最终序列[1,2,3,4,5,6,7,8],来看下实现步骤。

治阶段举例:合并[4,5,7,8]和[1,2,3,6],双指针i指向4,j指向1,然后分别比较i和j对应的大小,把小的填入到合并数组中。例如4>1,所以ans=[1],接着j往后移,2<4,所以ans=[1,2],接着j往后移,3<4,所以ans=[1,2,3]…以此类推

注意合并的时候merge(s1,s2,s),这里的s实际上是递归后的s,不是原始的s。比如说原始s=[1,7,3,5,4],那么递归到最小的时候,s1=[1],s2=[7],s=[1,7]


def merge(s1,s2,s):
    """将两个列表是s1,s2按顺序融合为一个列表s,s为原列表"""
    # j和i就相当于两个指向的位置,i指s1,j指s2
    i = j = 0
    while i+j<len(s):
        # j==len(s2)时说明s2走完了,或者s1没走完并且s1中该位置是最小的
        if j==len(s2) or (i<len(s1) and s1[i]<s2[j]):
            s[i+j] = s1[i]
            i += 1
        else:
            s[i+j] = s2[j]
            j += 1

def merge_sort(s):
    """归并排序"""
    n = len(s)
    # 剩一个或没有直接返回,不用排序
    if n < 2:
        return
    # 拆分
    mid = n // 2
    s1 = s[0:mid]
    s2 = s[mid:n]
    # 子序列递归调用排序
    merge_sort(s1)
    merge_sort(s2)
    # 合并
    merge(s1,s2,s)

if __name__ == '__main__':
    s = [1,7,3,5,4]
    merge_sort(s)
    print(s)
  • 时间复杂度 n l o g 2 n nlog_2n nlog2n,空间复杂度 O ( n ) O(n) O(n)

解析:假设一个序列有n个数的排序时间为T(n),我们将n个数划分为两半,然后每半排序时间为 T ( n 2 ) T(\frac{n}{2}) T(2n),我们有T(n)=2*T(n/2)+合并时间,合并时间就是遍历两半数组,一共n个所以用了时间n。

因此 T ( n ) = 2 T ( n 2 ) + n T(n)=2T(\frac{n}{2})+n T(n)=2T(2n)+n

T ( n / 2 ) = 2 T ( n 4 ) + n 2 T(n/2)=2T(\frac{n}{4})+\frac{n}{2} T(n/2)=2T(4n)+2n,所以 T ( n ) = 4 T ( n 4 ) + 2 n T(n)=4T(\frac{n}{4})+2n T(n)=4T(4n)+2n T ( n ) = 8 T ( n 8 ) + 3 n T(n)=8T(\frac{n}{8})+3n T(n)=8T(8n)+3n
以此类推直到不能再分为止,即 T ( n ) = n T ( 1 ) + n ∗ ( 层数 − 1 ) T(n)=nT(1)+n*(层数-1) T(n)=nT(1)+n(层数1)

可以看出这是二叉树结构,一个n个结点的二叉树层数为(log2n)+1。 T ( 1 ) = 0 T(1)=0 T(1)=0因此 T ( n ) = n T ( 1 ) + n l o g n = n l o g n T(n)=nT(1)+nlogn=nlogn T(n)=nT(1)+nlogn=nlogn

  • 缺点:因为是Out-place sort,因此相比快排,需要很多额外的空间。

  • 为什么归并排序比快速排序慢?

答:虽然渐近复杂度一样,但是归并排序的系数比快排大。

  • 对于归并排序有什么改进?

答:就是在数组长度为k时,用插入排序,因为插入排序适合对小数组排序。在算法导论思考题2-1中介绍了。复杂度为O(nk+nlg(n/k)) ,当k=O(lgn)时,复杂度为O(nlgn)

堆排序

思路:堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。

接着进行如下步骤:

a.将无需序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆;

b.将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;

c.重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。

具体代码解释见: https://www.jianshu.com/p/d174f1862601
写的真的很清楚!务必认真看下去

  • 主要难点在于如何构建堆

比如原始序列排序如下表所示,然后我们可以把他想象成一棵二叉树
在这里插入图片描述
可以看到L_length / 2=4,即1,2,3,4位置是根节点,下面都有别的叶子结点。初始构建堆我们主要对这四个节点,所以在heap_sort程序下,生成最初堆,heap_adjust输入的第二个变量分别是是4,3,2,1

for i in range(first_sort_count):
    heap_adjust(L, first_sort_count - i, L_length)

然后我们在考察heap_adjust如何调整父节点,先对于4位置,我们选择它两个子节点中较大的,即下标为j=2i和2i+1的位置中较大的数,判断是否比4位置大,如果是就让位置4等于这个较大值,然后让较大值位置等于位置4的数。

再对于3位置,同样如此

在对于2位置,也是如此,但是注意的是,我们让位置2等于max{其子树中较大值,位置2值}之后,还需要再往下更改位置4,即heap_adjust代码中i = j, j = 2 * i这句话。

while j <= end:
    if (j < end) and (L[j] < L[j + 1]):
        j += 1
    if temp < L[j]:
        L[i] = L[j]
        i = j
        j = 2 * i
    else:
        break

最后遍历得到初始堆位置如下

在这里插入图片描述


from collections import deque


def swap_param(L, i, j):
    L[i], L[j] = L[j], L[i]
    return L

# 生成堆
def heap_adjust(L, start, end):
    temp = L[start]

    i = start
    j = 2 * i

    while j <= end:
        if (j < end) and (L[j] < L[j + 1]):
            j += 1
        if temp < L[j]:
            L[i] = L[j]
            i = j
            j = 2 * i
        else:
            break
    L[i] = temp


def heap_sort(L):
    L_length = len(L) - 1

    first_sort_count = L_length / 2
    # 最初生成堆
    for i in range(first_sort_count):
        heap_adjust(L, first_sort_count - i, L_length)
	 # 交换堆和尾结点,再生成堆,再交换
    for i in range(L_length - 1):
        L = swap_param(L, 1, L_length - i)
        heap_adjust(L, 1, L_length - i - 1)

    return [L[i] for i in range(1, len(L))]


def main():
    L = deque([50, 16, 30, 10, 60,  90,  2, 80, 70])
    L.appendleft(0)
    print heap_sort(L)


if __name__ == '__main__':
    main()


  • 时间复杂度 O ( n l o g n ) O(nlogn) O(nlogn)

堆排序是一种选择排序,整体主要由构建初始堆+交换堆顶元素和末尾元素并重建堆两部分组成。其中构建初始堆经推导复杂度为O(n),在交换并重建堆的过程中,需交换n-1次,而重建堆的过程中,根据完全二叉树的性质,[log2(n-1),log2(n-2)…1]逐步递减,近似为nlogn。所以堆排序时间复杂度一般认为就是O(nlogn)级。

具体计算见:https://blog.csdn.net/weixin_39988331/article/details/111685084

计数排序
  1. 找出待排序数组A最大值max,新建长度为max+1的全零数组B
  2. 遍历B,令 B [ i ] = A B[i]=A B[i]=A中出现数字 i i i的次数
  3. 按照B下标和次数输出排序,例如B长为5,B=[0,1,2,1,1]表示A中有0个0,1个1,2个2,1个3和1个4,所以输出的就是1,2,2,3,4
   def sortArray(self, nums):
        # 计数排序
        max_, min_ = max(nums),min(nums)
        B = [0 for _ in range(max_-min_+1)]
        
        for i in range(len(nums)):
            # A中出现B的下标值,负变非负
            B[nums[i]-min_] += 1
        
        j = 0 
        for i in range(len(B)):
            while B[i]>0:
                nums[j] = i+min_
                B[i] -= 1
                j += 1
        
        return nums
  • 注意!如果存在负数例如nums=[-1,3],我们需要减去最小值变为非负处理
  • 时间复杂度 O ( n + k ) O(n+k) O(n+k),空间复杂度 O ( n + k ) O(n+k) O(n+k)
桶排序

参考:https://www.jianshu.com/p/204ed43aec0c

  1. 确定桶的个数,一般在5-10个较好
  2. 遍历每个数,看放进哪个桶=int [(x-min)/ 桶数]
  3. 再对每个桶进行排序,可以是快排或者插入排序,排序结束后按桶的顺序输出
def bucketSort(arr):
    maximum, minimum = max(arr), min(arr)
    bucketArr = [[] for i in range(maximum // 10 - minimum // 10 + 1)]  # set the map rule and apply for space
    for i in arr:  # map every element in array to the corresponding bucket
        index = i // 10 - minimum // 10
        bucketArr[index].append(i)
    arr.clear()
    for i in bucketArr:
        heapSort(i)   # sort the elements in every bucket
        arr.extend(i)  # move the sorted elements in bucket to array

  • 优势
  1. 快速排序可以视为两个桶的排序。不同的是,快速排序没有用额外的空间,是在集合本身排序属于原地排序,且每个子排序还是快速排序。而桶排序在额外空间上对桶进行排序,避免了构成桶过程的元素比较和交换操作,同时可以自主选择恰当的排序算法对桶进行排序。
  2. 桶排序是计数排序的改进。计数排序申请的额外空间跨度从最小元素值到最大元素值,若待排序集合中元素不是依次递增的,则必然有空间浪费情况。桶排序则是弱化了这种浪费情况,把原来计数排序每个数都申请一个空间,更新为最小值到最大值之间每一个固定区域申请空间,尽量减少了元素值大小不连续情况下的空间浪费情况。
  • 时间复杂度 O ( n + k ) O(n+k) O(n+k),空间复杂度 O ( n + k ) O(n+k) O(n+k),其中k是桶的个数

在这里插入图片描述

搜索

深度优先搜索(DFS)和广度优先搜索是两种最常见的优先搜索方法,它们被广泛地运用在图和树等结构中进行搜索。

深度优先搜索(DFS)

深度优先搜索(depth-first seach,DFS)在搜索到一个新的节点时,立即对该新节点进行遍历;因此遍历需要用先入后出的栈来实现

有时我们可能会需要对已经搜索过的节点进行标记,以防止在遍历时重复搜索某个节点,这种做法叫做状态记录或记忆化(memoization)

一套模板解决五个岛屿问题

一套模板解决五个岛屿问题:https://leetcode-cn.com/problems/number-of-islands/solution/by-zhang-xiao-lang-2-zdke/

  • 通用模板
class Solution {
public:
    int dfs(vector<vector<char>>& grid, int i, int j) {
        // 递归中,写出结束条件,例如
        if (i < 0 || j < 0 || i >= grid.size() || j >= grid[0].size() || grid[i][j] == 0) {
            return 0;
        }
        // 定义两个数组,用于访问当前节点的上下左右的四个节点,进行递归调用
        int di[4] = {-1,0,1,0};
        int dj[4] = {0,1,0,-1};
        // 遍历临近四个节点,进行递归调用
        for (int index = 0; index < 4; ++index) {
            int next_i = di[index];
            int next_j = dj[index];
            // 此处根据题目具体需求进行操作,这里只是给出一个示例
            df(grid, next_i,next_j);
        }

        return xxx;
    } 

    int numIslands(vector<vector<char>>& grid) {
        // 遍历每个节点,每个节点都调用 dfs,从 dfs 中获取想要的结果
        for (int i = 0; i < grid.size(); ++i) {
            for (int j = 0; j < grid[0].size(); ++j) {
                df(grid, i, j); 
            }
        }
        return xxx;
    }
};

695. 岛屿的最大面积

在这里插入图片描述值得注意的是,如果已经遍历过需要进行“沉船”处理,标记为不为1的数字

class Solution(object):
    def maxAreaOfIsland(self, grid):
        """
        :type grid: List[List[int]]
        :rtype: int
        """
        
        def dfs(grid,i,j):

            if i>=len(grid) or j>=len(grid[0]) or i<0 or j<0 or grid[i][j]!=1:
                return 0

            # 标记遍历过的为2
            grid[i][j] = 2

            count = 1
            di = [-1,0,1,0]
            dj = [0,1,0,-1]
            for index in range(len(di)):
                nexti = i+di[index]
                nextj = j+dj[index]
                count += dfs(grid,nexti,nextj)
            return count

        ans = 0
        for i in range(len(grid)):
            for j in range(len(grid[0])):
                ans = max(ans,dfs(grid,i,j))

        return ans
1020. 飞地的数量

在这里插入图片描述- 思路:从四条边走,把能走到的1都变成0,再统计走不到的1的数量

class Solution(object):
    def numEnclaves(self, grid):
        """
        :type grid: List[List[int]]
        :rtype: int
        """
        def dfs(grid,i,j):
            if i<0 or j<0 or i>=len(grid) or j>=len(grid[0]) or grid[i][j]==0:
                return 
            
            grid[i][j] = 0
            di = [0,0,1,-1]
            dj = [1,-1,0,0]
            for index in range(len(di)):
                nexti = i+di[index]
                nextj = j+dj[index]
                dfs(grid,nexti,nextj)
        
        # 从四条边走,把能走到的1都变成0,再统计走不到的1的数量
        ans = 0
        for i in range(len(grid)):
            dfs(grid,i,0)
            dfs(grid,i,len(grid[0])-1)
        for j in range(len(grid[0])):
            dfs(grid,0,j)
            dfs(grid,len(grid)-1,j)
        # 统计从边缘走不到的1
        for i in range(len(grid)):
            for j in range(len(grid[0])):
                if grid[i][j]==1:
                        ans += 1

        return ans
200. 岛屿数量

在这里插入图片描述

class Solution(object):
    def numIslands(self, grid):
        
        def dfs(grid,i,j):

            if i>=len(grid) or j>=len(grid[0]) or i<0 or j<0 or grid[i][j]!="1":
                return 0

            # 标记遍历过的为2
            grid[i][j] = 2

            count = 1
            di = [-1,0,1,0]
            dj = [0,1,0,-1]
            for index in range(len(di)):
                nexti = i+di[index]
                nextj = j+dj[index]
                count += dfs(grid,nexti,nextj)
            return count

        ans = 0
        for i in range(len(grid)):
            for j in range(len(grid[0])):
                if dfs(grid,i,j)>0:
                    ans += 1
        return ans
1254. 统计封闭岛屿的数目

在这里插入图片描述- 思路:首先,沿着四条边界走,把能走到的0(陆地)都变成1(水),说明这些0构成的陆地能连接到边界,所以肯定有一个方向是不被水包含,所以这些陆地构成的岛屿不能计算进去。

接着,再剩下的岛屿数量,这就和和第200题查找岛屿数量相同。然后每次查找0,找到后计数将紧靠的元素置为1.

class Solution {
public:
    int dfs(vector<vector<int>>& grid, int i, int j) {
        if (i < 0 || j < 0 || i >= grid.size() || j >= grid[0].size() || grid[i][j] == 1) {
            return 0;
        }
        int di[4] = {-1,0,1,0};
        int dj[4] = {0,1,0,-1};
        grid[i][j] = 1;
        for (int index = 0; index < 4; ++index) {
            int next_i = i + di[index];
            int next_j = j + dj[index];
            dfs(grid, next_i,next_j);
        }

        return 1;
    } 

    void dfs_bound(vector<vector<int>>& grid, int i, int j) {
        if (i < 0 || j < 0 || i >= grid.size() || j >= grid[0].size() || grid[i][j] == 1) {
            return;
        }
        int di[4] = {-1,0,1,0};
        int dj[4] = {0,1,0,-1};
        grid[i][j] = 1;
        for (int index = 0; index < 4; ++index) {
            int next_i = i + di[index];
            int next_j = j + dj[index];
            dfs_bound(grid, next_i,next_j);
        }
    } 

    int closedIsland(vector<vector<int>>& grid) {
        // 先把周围的不封闭的点处理掉
        for (int i = 0; i < grid.size(); ++i) {
            dfs_bound(grid, i, 0);
            dfs_bound(grid, i, grid[0].size() - 1);
        }
        // 先把周围的不封闭的点处理掉
        for (int j = 0; j < grid[0].size(); ++j) {
            dfs_bound(grid, 0, j);
            dfs_bound(grid, grid.size() - 1, j);
        }
        // 接下来实际就是求岛屿的数量了
        int num = 0;
        for (int i = 0; i < grid.size(); ++i) {
            for (int j = 0; j < grid[0].size(); ++j) {
                num += dfs(grid, i, j); 
            }
        }
        return num;
    }
};
130. 被围绕的区域

在这里插入图片描述这与上一题很类似,但是需要注意的是,上一题中我们dfs_bound函数把边界上能走过去的点也变为了相同的1,但是这里不行,我们得重新赋值我们把它变为N。

! 特别注意:[[“O”,“O”],[“O”,“O”]]

class Solution(object):
    def solve(self, grid):

        def dfs(grid,i,j):

            if i>=len(grid) or j>=len(grid[0]) or i<0 or j<0 or grid[i][j]!='O':
                return 

            # 边界上能走到的点标记为N
            grid[i][j] = 'N'
            
            di = [-1,0,1,0]
            dj = [0,1,0,-1]
            for index in range(len(di)):
                nexti = i+di[index]
                nextj = j+dj[index]
                dfs(grid,nexti,nextj)
        

        for i in range(len(grid)):
            dfs(grid,i,0)
            dfs(grid,i,len(grid[0])-1)
        for j in range(len(grid[0])):
            dfs(grid,0,j)
            dfs(grid,len(grid)-1,j)

        # 修改N为X,O为X
        for i in range(len(grid)):
            for j in range(len(grid[0])):
                if grid[i][j]=="O":
                    grid[i][j]="X"
                if grid[i][j]=="N":
                    grid[i][j]="O"

深度搜索

547. 省份数量

在这里插入图片描述
新增一个visited矩阵来标记是否已被查找过。深度搜索是遍历与i相邻的j,然后再通过j遍历相邻的,同时把相邻的点在visited矩阵标记

class Solution(object):
    def findCircleNum(self, isConnected):
    
        def dfs(isConnected,i,visited):
            for j in range(len(isConnected)):
                # 继续遍历与顶点 i 相邻的顶点(使用 visited 数组防止重复访问)
                if isConnected[i][j]==1 and visited[j]==0:
                    visited[j] = 1
                    dfs(isConnected,j,visited)

        ans = 0
        visited = [0]*len(isConnected)
        for i in range(len(isConnected)):
            # 若当前顶点 i 未被访问,说明又是一个新的连通域,则遍历新的连通域且ans+=1.
            if visited[i]==0:
                dfs(isConnected,i,visited)
                ans += 1
        return ans
417. 太平洋大西洋水流问题

在这里插入图片描述
注意题目要找的是能同时流到大西洋和太平洋的位置,例如说[1,2]这个位置就只能流到太平洋,没法去大西洋,所以不选他。

  • 思路:对所有位置进行搜索,不剪枝的话复杂度太高。我们反过来考虑,可以从两个大洋开始向上流,这样我们只需要对矩形四条边进行搜索,分别找出大西洋和太平洋能到的位置,然后取交集。
  • 这里不能和之前一样直接修改原矩阵,而是得新增两个visite矩阵
class Solution {
public:
    vector<vector<int>> P, A, ans;
    int n, m;
    vector<vector<int>> pacificAtlantic(vector<vector<int>>& M) {
        n = M.size(), m = M[0].size();
        P = A = vector<vector<int>>(n, vector<int>(m, 0));
        //左右两边加上下两边出发深搜
        for(int i = 0; i < n; ++i) dfs(M, P, i, 0), dfs(M, A, i, m - 1);
        for(int j = 0; j < m; ++j) dfs(M, P, 0, j), dfs(M, A, n - 1, j);             
        return ans;
    }
    void dfs(vector<vector<int>>& M, vector<vector<int>>& visited, int i, int j){        
        if(visited[i][j]) return;
        visited[i][j] = 1;

        if(P[i][j] && A[i][j]) ans.push_back({i,j}); 

        //上下左右深搜
        if(i-1 >= 0 && M[i-1][j] >= M[i][j]) dfs(M, visited, i-1, j);
        if(i+1 < n && M[i+1][j] >= M[i][j]) dfs(M, visited, i+1, j); 
        if(j-1 >= 0 && M[i][j-1] >= M[i][j]) dfs(M, visited, i, j-1);
        if(j+1 < m && M[i][j+1] >= M[i][j]) dfs(M, visited, i, j+1); 
    }
};


作者:Xiaohu9527
链接:https://leetcode-cn.com/problems/pacific-atlantic-water-flow/solution/shui-wang-gao-chu-liu-by-xiaohu9527-xxsx/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

回溯法

顾名思义,回溯法的核心是回溯。在搜索到某一节点的时候,如果我们发现目前的节点(及其子节点)并不是需求目标时,我们回退到原来的节点继续搜索,并且把在目前节点修改的状态还原。这样的好处是我们可以始终只对图的总状态进行修改,而非每次遍历时新建一个图来储存状态。在具体的写法上,它与普通的深度优先搜索一样,都有 [修改当前节点状态]→[递归子节点] 的步骤,只是多了回溯的步骤,变成了 [修改当前节点状态]→[递归子节点]→[回改当前节点状态]

回溯法修改一般有两种情况,一种是修改最后一位输出,比如排列组合;一种是修改访问标记,比如矩阵里搜字符串。

46. 全排列

在这里插入图片描述加入path和visited,分别记录走的路径以及是否遍历过,走到最深处,回溯,path.pop(),再把这个点的visted值变为0

class Solution(object):
    def permute(self, nums):

        def dfs(nums,depth,path,visited,ans):
            if depth == len(nums):
                # path会改变,所以不能直接加入path而应该加入path的copy
                ans.append(path[:])
                return 
            
            for i in range(len(visited)):
                if visited[i]==0:
                    visited[i] = 1
                    path.append(nums[i])
                    # 继续 dfs,直到最后一层
                    dfs(nums,depth+1,path,visited,ans)
                    # 回溯
                    path.pop()
                    visited[i] = 0
        
        visited = [0 for _ in range(len(nums))]
        ans = []
        dfs(nums,0,[],visited,ans)
        
        return ans 
77. 组合

在这里插入图片描述注意,这里的选择与上述题目不一样,上题nums[1,2,3]选择节点2后,接着再[1,3]中选一个,而这里选择节点2后,只能从[3]中选一个。因为[1,2]和[2,1]是视为重复的。因此我们需要加入begin参数,从begin后开始选择。

class Solution(object):
    def combine(self, n, k):
        
        def dfs(nums,k,begin,depth,path,visited,ans):
            if depth == k:
                # path会改变,所以不能直接加入path而应该加入path的copy
                ans.append(path[:])
                return 
            
            for i in range(begin,len(visited)):
                if visited[i]==0:
                    visited[i] = 1
                    path.append(nums[i])
                    # 继续 dfs,直到最后一层
                    dfs(nums,k,i+1,depth+1,path,visited,ans)
                    # 回溯
                    path.pop()
                    visited[i] = 0
        nums = [i+1 for i in range(n)]
        visited = [0 for _ in range(len(nums))]
        ans = []
        dfs(nums,k,0,0,[],visited,ans)
        
        return ans 
79. 单词搜索

在这里插入图片描述
注意:这里定义dfs的时候不能加入depth变量,用来记录我们搜索到word中词的位置,因为depth会根据搜索回溯而变小。例如我们找到了找到了前3个,但是找第四个的时候,前后左右都没找到,需要往前回溯从别的路径找,这样depth就不会变成4而是变成3。下面是错误代码

def dfs(board,word,visited,depth,i,j):
        
        
        if i<0 or j<0 or i>=len(board) or j>=len(board[0])
                 or visited[i][j]==1  or depth >= len(word):
            return 0
        
        
        count = 0
        visited[i][j]=1
        if board[i][j]==word[depth]:
            depth += 1
            count = 1 if depth<len(word) else 0
        di = [0,0,1,-1]
        dj = [1,-1,0,0]
        for index in range(4):
            nexti = i+di[index]
            nextj = j+dj[index]
            
            count += dfs(board,word,visited,depth,nexti,nextj)
        # 回溯,如果已经找到了就不用回溯
        if depth < len(word):
            visited[i][j] = 0 
        return count

而是用word[1:]

class Solution(object):
    
    # 定义上下左右四个行走方向
    directs = [(0, 1), (0, -1), (1, 0), (-1, 0)]
    
    def exist(self, board, word):
        """
        :type board: List[List[str]]
        :type word: str
        :rtype: bool
        """
        m = len(board)
        if m == 0:
            return False
        n = len(board[0])
        mark = [[0 for _ in range(n)] for _ in range(m)]
                
        for i in range(len(board)):
            for j in range(len(board[0])):
                if board[i][j] == word[0]:
                    # 将该元素标记为已使用
                    mark[i][j] = 1
                    if self.backtrack(i, j, mark, board, word[1:]) == True:
                        return True
                    else:
                        # 回溯
                        mark[i][j] = 0
        return False
        
        
    def backtrack(self, i, j, mark, board, word):
        if len(word) == 0:
            return True
        
        for direct in self.directs:
            cur_i = i + direct[0]
            cur_j = j + direct[1]
            
            if cur_i >= 0 and cur_i < len(board) and cur_j >= 0 and cur_j < len(board[0]) and board[cur_i][cur_j] == word[0]:
                # 如果是已经使用过的元素,忽略
                if mark[cur_i][cur_j] == 1:
                    continue
                # 将该元素标记为已使用
                mark[cur_i][cur_j] = 1
                if self.backtrack(cur_i, cur_j, mark, board, word[1:]) == True:
                    return True
                else:
                    # 回溯
                    mark[cur_i][cur_j] = 0
        return False



广度优先搜索

一般用栈结构进行操作

934. 最短的桥

在这里插入图片描述
本题实际上是求两个岛屿间的最短距离,因此我们可以先通过任意搜索方法找到其中一个岛
屿,然后利用广度优先搜索,查找其与另一个岛屿的最短距离。

  • 具体做法DFS+BFS

先找到其中一个岛屿中的一个点,然后进行DFS,把遍历到的所有点都变成2,并且加入队列

然后使用该队列进行BFS,同时把遍历到的点0变成2,并且记录遍历的步数,然后当有一个碰到1时,返回步数(桥长度)

在这里插入图片描述

class Solution(object):
    def shortestBridge(self, grid):
        def dfs(grid,i,j,que):
            if i<0 or j<0 or i>=len(grid) or j>=len(grid[0]) or grid[i][j]!=1:
                return
            que.append([i,j])
            grid[i][j] = 2
            di = [0,0,1,-1]
            dj = [1,-1,0,0]
            for index in range(4):
                nexti = i+di[index]
                nextj = j+dj[index]
                
                dfs(grid,nexti,nextj,que)
            
        flag = 0
        que = []
        # 找到第一个岛屿,并把岛屿上的1改为2
        for i in range(len(grid)):
            for j in range(len(grid[0])):
                if grid[i][j]==1:
                    dfs(grid,i,j,que)
                    flag = 1
                    break
            if flag:
                break
        
        # BFS
        count = 0
        while len(que)>0:
            size = len(que)
            # 遍历一层,每个元素周围0元素变为2加入队列
            for _ in range(size):
                cur = que.pop(0) #弹出第一个元素
                i,j = cur[0],cur[1]
                di = [0,0,1,-1]
                dj = [1,-1,0,0]
                
                for index in range(4):
                    nexti = i+di[index]
                    nextj = j+dj[index]
                    
                    if nexti>=0 and nextj>=0 and nexti<len(grid) and nextj<len(grid[0]):
                        if grid[nexti][nextj]==1:
                            return count
                        if grid[nexti][nextj]==0:
                            grid[nexti][nextj] = 2
                            que.append([nexti,nextj])
            count += 1

257. 二叉树的所有路径

输入:root = [1,2,3,null,5]
输出:[“1->2->5”,“1->3”]

class Solution:
    def binaryTreePaths(self, root):
        """
        :type root: TreeNode
        :rtype: List[str]
        """
        paths = []
        def dfs(root,path):
            if root.left==None and root.right==None:
                path+= str(root.val)
                paths.append(path)
                return
            path+= str(root.val)
            
            if root.left!=None:
                dfs(root.left,path+'->')
            if root.right!=None:
                dfs(root.right,path+'->')
        dfs(root,'')
        return paths
47. 全排列 II

在这里插入图片描述- 自己:加入visited集,加入没有遍历的点,然后回溯
例如,nums[1,2,3]初始化visited=[0,0,0]。
开始DFS,看到path长度不符合return要求,遍历visited集,找到没有遍历的点1,加入path,接着DFS,看到path长度不符合return要求,遍历visited集,找到没有遍历的点2,加入path,接着DFS,看到path长度不符合return要求,遍历visited集,找到没有遍历的点3,加入path。接着在遍历visited集,找到没有遍历的点3,加入path。

接着DFS,看到path长度符合return要求,将path=[1,2,3]加入paths集,返回。

把path中的3去掉,把visted中3对应的遍历状态改为未遍历。因为此时已经遍历完整个visted集合了,因此再返回上一层函数,也就是path=[1,2],遍历了1,2的。把path中的2去掉,把visted中2对应的遍历状态改为未遍历。这时还没遍历3,于是遍历3加入到path中,path=[1,3],接着继续加入2,然后path长度符合要求返回上一层…

class Solution(object):
    def permuteUnique(self, nums):
        visited = [0 for _ in range(len(nums))]
        paths = []
        def dfs(nums,visited,path):
            if len(path)==len(nums):
                if path  not in paths:
                    paths.append(path[:])
                return 
            for i in range(len(visited)):
                if visited[i]==0:
                    visited[i]=1
                    path.append(nums[i])
                    dfs(nums,visited,path)
                    # 回溯
                    visited[i]=0
                    path.pop()
        dfs(nums,visited,[])
        return paths
40. 组合总和 II

在这里插入图片描述

  • 首先,我们使用一个哈希映射(HashMap)统计数组中每个数出现的次数。在统计完成之后,我们将结果放入一个列表 freq中,方便后续的递归使用。

  • 接着,定义 d f s ( p o s , r e s t ) dfs(pos,rest) dfs(pos,rest)表示递归的函数,其中 pos表示数组中不重复的第pos个数,rest表示剩余和。

如果rest刚好为0则说明我们已经找到了满足和的排列,返回。否则,遍历到数组中不重复的第pos个数字时,我们有以下几种选择,不选这个数,选一个这个数,选多个这个数。

  1. 如果不选,则调用 d f s ( p o s + 1 , r e s t ) dfs(pos + 1, rest) dfs(pos+1,rest)
  2. 如果选多个(含1个),则 d f s ( p o s + 1 , r e s t − i ∗ f r e q [ p o s ] [ 0 ] ) dfs(pos + 1, rest - i * freq[pos][0]) dfs(pos+1,restifreq[pos][0]),其中 f r e q [ p o s ] [ 0 ] ) freq[pos][0]) freq[pos][0])表示这个数大小, f r e q [ p o s ] [ 1 ] ) freq[pos][1]) freq[pos][1])表示这个数个数。并加入这些数

PS:统计数组A中每个数出现的次数

freq = sorted(collections.Counter(A).items())
class Solution(object):
    def combinationSum2(self, candidates, target):
    
        paths = []
        def dfs(pos,rest,path):
            if rest==0:
                paths.append(path[:])
                return 
            if pos == len(freq) or rest<freq[pos][0] :
                return 
            
            dfs(pos+1,rest,path)
            most = min(rest // freq[pos][0], freq[pos][1])
            for i in range(1, most + 1):
                path.append(freq[pos][0])
                
                dfs(pos + 1, rest - i * freq[pos][0],path)
            
            # 回溯
            for i in range(1, most + 1):
                path.pop()
           # 如果写path=path[:-most]会导致返回上一层时path没有删干净
        
        freq = sorted(collections.Counter(candidates).items())
        paths = []
        dfs(0,target,[])
        
        return paths

动态规划

动态规划和其它遍历算法(如深/广度优先搜索)都是将原问题拆成多个子问题然后求解,他们之间最本质的区别是,动态规划保存子问题的解,避免重复计算。解决动态规划问题的关键是找到状态转移方程,这样我们可以通过计算和储存子问题的解来求解最终问题。

  • 什么时候用动态规划,什么时候用搜索?

动态规划是自下而上的,即先解决子问题,再解决父问题;而用带有状态记录的优先搜索是自上而下的,即从父问题搜索到子问题,若重复搜索到同一个子问题则进行状态记录,防止重复计算。

如果题目需求的是最终状态,那么使用动态搜索比较方便;如果题目需要输出所有的路径,那么使用带有状态记录的优先搜索会比较方便

一维

70. 爬楼梯(Easy)

在这里插入图片描述

  • 方法一:递归

f ( n ) f(n) f(n)表示n阶时不同种方法,则有等式 f ( n ) = f ( n − 1 ) + f ( n − 2 ) f(n)=f(n-1)+f(n-2) f(n)=f(n1)+f(n2)

class Solution(object):
    def climbStairs(self, n):
        def f(n):
            if n==1:
                return 1
            if n==2:
                return 2
            return f(n-1)+f(n-2)

        return f(n)

但是超出时间限制,于是我们转换思路,应该用动态规划存储状态
在这里插入图片描述

  • 方法二:动态规划,用dp数组记录状态
class Solution(object):
    def climbStairs(self, n):
        dp = [0 for _ in range(n)]
        if n==1: 
            return 1
        dp[0], dp[1] = 1, 2
        for i in range(2,n):
            dp[i] = dp[i-1]+dp[i-2]

        return dp[n-1]
  • 方法三:优化的动态规划

进一步的,我们可以对动态规划进行空间压缩。因为 dp[i] 只与 dp[i-1] 和 dp[i-2] 有关,因此可以只用两个变量来存储 dp[i-1] 和 dp[i-2],使得原来的 O(n) 空间复杂度优化为 O(1) 复杂度。

class Solution(object):
    def climbStairs(self, n):
        
        if n<=2: 
            return n
        per1, per2, per = 1, 2,0
        for i in range(2,n):
            per = per1+per2
            per1 = per2
            per2 = per

        return per
198. 打家劫舍(Easy)

在这里插入图片描述
d p [ i ] dp[i] dp[i]表示抢劫到第 i 个房子时,可以抢劫的最高金额,我们可以选择抢也可以不抢这个房子。 d p [ i ] = m a x ( d p [ i − 1 ] , n u m s [ i − 1 ] + d p [ i − 2 ] ) dp[i] = max(dp[i-1],nums[i-1] + dp[i-2]) dp[i]=max(dp[i1],nums[i1]+dp[i2])

class Solution(object):
    def rob(self, nums):
        if len(nums)==1:
            return nums[0]
        pper,per,cur = 0,0,0

        for i in range(len(nums)):
            cur = max(per,pper+nums[i])
            pper = per
            per = cur
        return cur
413. 等差数列划分

在这里插入图片描述
https://leetcode-cn.com/problems/arithmetic-slices/solution/fu-xue-ming-zhu-bao-li-shuang-zhi-zhen-d-fc1l/

暴力 => 双指针 => 递归 => 动态规划

  • 方法一:暴力

最容易想到的就是暴力解法:判断所有的子数组是不是等差数列,如果是的话就累加次数。

怎么遍历所有子数组?遍历子数组起点 i i i和终点 j j j,两个循环

        for (int i = 0; i < N - 2; i++) {
            for (int j = i + 1; j < N; j++) {
                if (isArithmetic(A, i, j))

怎么判断子数组是不是等差数列?A[i + 1] * 2 == A[i] + A[i + 2]

整体代码如下,先遍历找到所有子数组区间,然后挨个判断是否等差,是则计数+1

class Solution {
public:
    int numberOfArithmeticSlices(vector<int>& A) {
        const int N = A.size();
        int res = 0;
        for (int i = 0; i < N - 2; i++) {
            for (int j = i + 1; j < N; j++) {
                if (isArithmetic(A, i, j))
                    res ++;
            }
        }
        return res;
    }
private:
    bool isArithmetic(vector<int>& A, int start, int end) {
        if (end - start < 2) return false;
        for (int i = start; i < end - 1; i++) {
            if (A[i + 1] * 2 != A[i] + A[i + 2])
                return false;
        }
        return true;
    }
};
  • 方法二:双指针

关键点,如果我们已经知道一个子数组的前面部分不是等差数列以后,那么后面部分就不用判断了。

因此,对于每个起始位置,我们只需要向后进行一遍扫描,直到不再构成等差数列为止,此时已经没有必要再向后扫描。

这个思路其实就是双指针(滑动窗口) 的解法

class Solution {
public:
    int numberOfArithmeticSlices(vector<int>& A) {
        const int N = A.size();
        int res = 0;
        for (int i = 0; i < N - 2; i++) {
            int d = A[i + 1] - A[i];
            for (int j = i + 1; j < N - 1; j++) {
                if (A[j + 1] - A[j] == d) 
                    res ++;
                else
                    break;
            }
        }
        return res;
    }
};
  • 方法三:递归或者动态规划

在这里插入图片描述
这里关键点是以end为终点,例如[1,2,3,4]以3为终点的有[1,2,3],以4为终点的有[1,2,3,4],[2,3,4],这是因为2,3,4构成等差数列,所以把以3为终点的等差数组继承过来加上数字4就是新的等差数组,然后再新加一个2,3,4这个等差数组。因此一共有2个。

最后,整个数组的等差数列数目,就是以0,1,2,3,…为结尾的等差数组和

class Solution(object):
    def numberOfArithmeticSlices(self, A):
        N = len(A)
        dp = [0] * N
        for i in range(1, N - 1):
            if A[i - 1] + A[i + 1] == A[i] * 2:
                dp[i] = dp[i - 1] + 1
        return sum(dp)

改进的动态规划

class Solution(object):
    def numberOfArithmeticSlices(self, A):
        count = 0
        k = 0
        for i in xrange(len(A) - 2):
            if A[i + 1] - A[i] == A[i + 2] - A[i + 1]:
                k += 1
                count += k
            else:
                k = 0
        return count

二维

64. 最小路径和

在这里插入图片描述
定义dp表示为到i,j点最短的距离,对于最上边和最左边的,只有一个方向可以走,所以先算好,对于其余点, d p [ i ] [ j ] = m i n ( d p [ i − 1 ] [ j ] , d p [ i ] [ j − 1 ] ) + g r i d [ i ] [ j ] dp[i][j] = min(dp[i-1][j],dp[i][j-1])+grid[i][j] dp[i][j]=min(dp[i1][j],dp[i][j1])+grid[i][j]

class Solution(object):
    def minPathSum(self, grid):

        dp = [[0 for _ in range(len(grid[0]))] for _ in range(len(grid))]
        dp[0][0] = grid[0][0]
        for j in range(1,len(grid[0])):
            dp[0][j] = dp[0][j-1]+grid[0][j]
        
        for i in range(1,len(grid)):
            dp[i][0] = dp[i-1][0]+grid[i][0]
        
        for i in range(len(grid)):
            for j in range(len(grid[0])):
                if i!=0 and j!=0:
                    dp[i][j] = min(dp[i-1][j],dp[i][j-1])+grid[i][j]
                    
        return dp[len(grid)-1][len(grid[0])-1]

一般来说,因为这道题涉及到四个方向上的最近搜索,所以很多人的第一反应可能会是广度优先搜索。但是对于一个大小 O(mn) 的二维数组,对每个位置进行四向搜索,最坏情况的时间复杂度(即全是 1)会达到恐怖的 O(m2n2)。一种办法是使用一个 dp 数组做 memoization,使得广度优先搜索不会重复遍历相同位置;另一种更简单的方法是,我们从左上到右下进行一次动态搜索,再从右下到左上进行一次动态搜索。两次动态搜索即可完成四个方向上的查找。

在这里插入图片描述

221. 最大正方形

在这里插入图片描述
在这里插入图片描述
关键: d p [ i ] [ j ] = m i n ( d p [ i − 1 ] [ j − 1 ] , m i n ( d p [ i ] [ j − 1 ] , d p [ i − 1 ] [ j ] ) ) + 1 dp[i][j] = min(dp[i-1][j-1], min(dp[i][j-1], dp[i-1][j])) + 1 dp[i][j]=min(dp[i1][j1],min(dp[i][j1],dp[i1][j]))+1

其中, d p [ i ] [ j ] dp[i][j] dp[i][j]表示满足题目条件的、以 (i, j) 为右下角的正方形个数

class Solution(object):
    def maximalSquare(self, matrix):
        """
        :type matrix: List[List[str]]
        :rtype: int
        """
        m,n = len(matrix),len(matrix[0])
        dp = [[0 for _ in range(n)] for _ in range(m)]
        edge = 0
        for i in range(m):
            for j in range(n):
                if matrix[i][j]=='1':
                    if i==0 or j==0:
                        dp[i][j] = 1
                    else:
                        dp[i][j] = min(dp[i-1][j-1], min(dp[i][j-1], dp[i-1][j])) + 1
                    edge = max(edge,dp[i][j])
        return edge*edge

分割类型题

279. 完全平方数

给定一个正整数,求其最少可以由几个完全平方数相加构成。
输入:n = 12
输出:3
解释:12 = 4 + 4 + 4

对于分割类型题,动态规划的状态转移方程通常并不依赖相邻的位置,而是依赖于满足分割条件的位置。我们定义一个一维矩阵 dp,其中 dp[i] 表示数字 i 最少可以由几个完全平方数相加构成。在本题中,位置 i 只依赖 i - k2 的位置,如 i - 1、i - 4、i - 9 等等,才能满足完全平方分割的条件。因此 dp[i] 可以取的最小值即为 1 + min(dp[i-1], dp[i-4], dp[i-9] · · · )。

在这里插入图片描述

91. 解码方法

已知字母 A-Z 可以表示成数字 1-26。给定一个数字串,求有多少种不同的字符串等价于这个数字串

Input: “226”
Output: 3
在这个样例中,有三种解码方式:BZ(2 26)、VF(22 6) 或 BBF(2 2 6)。

在这里插入图片描述

class Solution {
public:
    int numDecodings(string s) {
        int n = s.size();
        s = " " + s;
        vector<int> f(n + 1,0);
        f[0] = 1;        
        for(int i = 1; i < n + 1; i++) {
            int a = s[i] - '0', b = (s[i - 1] - '0') * 10 + s[i] - '0';
            if(1 <= a && a <= 9) f[i] = f[i - 1];
            if(10 <= b && b <= 26) f[i] += f[i - 2];
        }
        return f[n];
    }
};

139. 单词拆分

给定一个字符串和一个字符串集合,求是否存在一种分割方式,使得原字符串分割后的子字符串都可以在集合内找到。

在这里插入图片描述

输入: s = “catsandog”, wordDict = [“cats”, “dog”, “sand”, “and”, “cat”]

输出: false

在这里插入图片描述

class Solution:
    def wordBreak(self, s: str, wordDict: List[str]) -> bool:       
        n=len(s)
        dp=[False]*(n+1)
        dp[0]=True
        for i in range(n):
            for j in range(i+1,n+1):
                if(dp[i] and (s[i:j] in wordDict)):
                    dp[j]=True
        return dp[-1]

举例 s = “catsandog”, wordDict = [“cats”, “dog”, “sand”, “and”, “cat”]
i=0,遍历j,找到cat和cats的切分点,分别是dp[4]=True,dp[3]=True
i=1,遍历j,找到前面的切分点,比如说dp[3]=True,从这里开始,看后面有没有单词被包含,找到新的切分点

如果dp[i]=True说明0到i之间可以由word词组成,如果s[i:j]有word词组成,就说明0到j之间可以由word词组成,所以dp[j]=True,一直遍历到最后

子序列问题

300. 最长递增子序列

在这里插入图片描述
注意:按照 LeetCode 的习惯,子序列(subsequence)不必连续,即可以跳着取,子数组(subarray)或子字符串(substring)必须连续。

  • 自己:定义dp[i]表示以nums[i]为结尾的递增子序列最长大小

例如nums=[10,9,2,5,3,7,101,18]
我们怎么算以7为结尾的最长子序列大小,我们遍历7之前的数,如果这个数比7小,说明我们可以在以这个数为结尾的子序列上加上7,就变成了以7为结尾的序列,序列长度+1,然后求出其中的最长值。

class Solution(object):
    def lengthOfLIS(self, nums):
        dp = [1 for _ in range(len(nums))]
        for i in range(len(nums)):
            for j in range(i-1,-1,-1):
                if nums[j]<nums[i]:
                    dp[i] = max(dp[i],dp[j]+1)
        return max(dp)

背包问题

背包问题是一种组合优化的 NP 完全问题:有 N 个物品和容量为 W 的背包,每个物品都有自己的体积 w 和价值 v,求拿哪些物品可以使得背包所装下物品的总价值最大。如果限定每种物品只能选择 0 个或 1 个,则问题称为 0-1 背包问题;如果不限定每种物品的数量,则问题称为无界背包问题或完全背包问题。

0-1 背包问题求解

定义一个二维数组 dp存储最大价值,其中 d p [ i ] [ j ] dp[i][j] dp[i][j] 表示考察到i-1件物品,体积不超过j下能达到的最大价值。在我们遍历到第 i 件物品时,在当前背包总容量为 j 的情况下,如果我们不将物品 i 放入背包,那么 d p [ i ] [ j ] = d p [ i − 1 ] [ j ] dp[i][j] = dp[i-1][j] dp[i][j]=dp[i1][j],即前 i 个物品的最大价值等于只取前 i-1 个物品时的最大价值;如果我们将物品 i 放入背包,假设第 i 件物品体积为 w,价值为 v,那么我们得到 d p [ i ] [ j ] = d p [ i − 1 ] [ j − w ] + v dp[i][j] = dp[i-1][j-w] + v dp[i][j]=dp[i1][jw]+v。我们只需在遍历过程中对这两种情况取最大值即可,总时间复杂度和空间复杂度都为 O(NW)。

在这里插入图片描述
我们可以进一步对 0-1 背包进行空间优化,将空间复杂度降低为 O(W)。

如图所示,假设我们目前考虑物品 i = 2,且其体积为 w = 2,价值为 v = 3;对于背包容量 j,我们可以得到 dp[2][j]= max(dp[1][j], dp[1][j-2] + 3)。这里可以发现我们永远只依赖于上一排 i -1 的信息,之前算过的其他物品都不需要再使用。因此我们可以去掉 dp 矩阵的第一个维度,在考虑物品 i 时变成 d p [ j ] = m a x ( d p [ j ] , d p [ j − w ] + v ) dp[j]= max(dp[j], dp[j-w] + v) dp[j]=max(dp[j],dp[jw]+v)

这里要注意我们在遍历每一行的时候必须逆向遍历,这样才能够调用上一行物品 i-1 时 dp[j-w] 的值;若按照从左往右的顺序进行正向遍历,则 dp[j-w] 的值在遍历到j 之前就已经被更新成物品 i 的值了。即j遍历从后往前。

int knapsack(vector<int> weights, vector<int> values, int N, int W) {
vector<int> dp(W + 1, 0);
for (int i = 1; i <= N; ++i) {
	int w = weights[i-1], v = values[i-1];
	for (int j = W; j >= w; --j) {
		dp[j] = max(dp[j], dp[j-w] + v);
	}
}
return dp[W];

举个例子:物品的价值和重量如下图所示,最多可以装W=11重量。左图是0-1背包 d p [ i ] [ j ] dp[i][j] dp[i][j] 的转移矩阵,我们现在要考虑把 d p [ i ] [ j ] dp[i][j] dp[i][j] 变为一维。

在这里插入图片描述放入物品1之后,dp=[1,1,.1]共W个数据,dp[j]表示不超过j重量的最大价值

考虑物品2,重量为6,所以再算dp[3]的时候,dp[3]=max(1+6,dp[3])=7,(放和不放取最大)后面也都是3,注意这里强调的是先应该从后往前遍历,即先算dp[W],在往前遍历,最后算到dp[3],dp[2]这样,否则dp[j] = max(dp[j], dp[j-w] + v),会使得dp[4]=dp[3]+6=13

完全背包求解

在这里插入图片描述在这里插入图片描述
同样的,我们也可以利用空间压缩将时间复杂度降低为 O(W)。这里要注意我们在遍历每一行的时候必须正向遍历,因为我们需要利用当前物品在第 j-w 列的信息。

在这里插入图片描述

绝杀口诀

0-1 背包对物品的迭代放在外层,里层的体积或价值逆向遍历;

完全背包对物品的迭代放在里层,外层的体积或价值正向遍历。

416. 分割等和子集

在这里插入图片描述
这可以用0-1背包问题,0-1背包问题是每次只能拿一样且不能重复拿,设dp[i][j]表示遍历到物品i时,存储量不超过j的最大价值

这里我们没有物品价值,并且我们需要的是恰好等于j,因此我们定义dp[i][j]表示遍历到物品i时,存储量是否恰好等于j,所以dp[i][j]代表的不是数值而是布尔值。

具体做法:

  1. 设置dp数组,是len 行,target + 1 列的矩阵。这里 len 是物品的个数,target 是背包的容量。len 行表示一个一个物品考虑,target + 1多出来的那 1 列,表示背包容量从 0 开始考虑。很多时候,我们需要考虑这个容量为 0 的数值。

  2. 初始化dp数组,dp[i][0] = true,i=0…len(nums)-1,因为遍历到下标i时,不选数就可以使得此时背包数量恰好为0。

  3. 考虑状态转移方程dp[i][j]:「当前考虑到的数字选与不选」。

    不选择 nums[i],如果在 [0, i - 1] 这个子区间内已经有一部分元素,使得它们的和为 j ,那么 dp[i][j] = true;

    选择 nums[i],如果在 [0, i - 1] 这个子区间内就得找到一部分元素,使得它们的和为 j - nums[i]。所以转移状态如下:

dp[i][j] = dp[i - 1][j] or dp[i - 1][j - nums[i]]
class Solution(object):
    def canPartition(self, nums):

        Sum = sum(nums)
        if Sum%2!=0:
            return False
        target = Sum//2

        # 多加和为0这一列
        dp = [[False for  _ in range(target+1)] for _ in range(len(nums))]

        # 初始化,可以全不选
        for i in range(len(nums)):
            dp[i][0] = True
        
        for i in range(len(nums)):
            for j in range(target+1):
                # 只能不选
                if j<nums[i]:
                    dp[i][j] = dp[i-1][j] 
                else:
                # 不选or选
                    dp[i][j] = dp[i-1][j] or dp[i-1][j-nums[i]]
        
        return dp[len(nums)-1][target]
474. 一和零

在这里插入图片描述

  • 多维背包问题

最简单的就是设置多维矩阵,有两个背包大小,0 的数量和 1 的数量。因此要设置三维矩阵,可以进行空间优化,将三维空间压缩到二维后的写法。

  • 简单粗暴版:三维空间

设置dp[i][j][k]表示遍历到i时,1和0不超过j,k的最多字符串个数,注意这里dp的维度是len(strs)+1,m+1,n+1维,len +1行多出来的行表示物品从什么都不选开始考虑,m+1 多出来的那 1 列和n+1多的列,表示背包容量从 0 开始考虑。

class Solution(object):
    def findMaxForm(self, strs, m, n):
        dp = [[[0 for _ in range(m+1)]for _ in range(n+1)]for _ in range(len(strs)+1)]

        for i in range(1,len(strs)+1):
            count0, count1 = strs[i-1].count('0'),strs[i-1].count('1')
            for j in range(n+1):
                for k in range(m+1):
                    # 不选
                    dp[i][j][k] = dp[i-1][j][k]
                    #选
                    if k>=count0 and j>=count1:
                        dp[i][j][k] = max(dp[i-1][j][k],dp[i-1][j-count1][k-count0]+1)
        return dp[len(strs)][n][m]
  • 优化版的:三维降二维

还记得我们的口诀吗?

0-1 背包对物品的迭代放在外层,里层的体积或价值逆向遍历;

完全背包对物品的迭代放在里层,外层的体积或价值正向遍历。

class Solution(object):
    def findMaxForm(self, strs, m, n):
        dp = [[0 for _ in range(m+1)]for _ in range(n+1)]

        for i in range(1,len(strs)+1):
            count0, count1 = strs[i-1].count('0'),strs[i-1].count('1')
            # 逆向遍历
            for j in range(n,-1,-1):
                for k in range(m,-1,-1):
                    if k>=count0 and j>=count1:
                        dp[j][k] = max(dp[j][k],dp[j-count1][k-count0]+1)
                    
        return dp[n][m]
322. 零钱兑换

在这里插入图片描述
因为每个硬币可以用无限多次,这道题本质上是完全背包。完全背包问题和0-1背包问题最大的不同就是,完全背包问题把dp[i][j] = min(dp[i-1][j],dp[i-1][j-coins[i]]+1),变为dp[i][j] = min(dp[i-1][j],dp[i][j-coins[i]]+1)

  • 还有比较重要的一点是,我们不可以初始化dp数组为-1,而应初始化为amount + 2,因为在动态规划过程中有求最小值的操作,如果初始化成-1 则会导致结果始终为-1。至于为什么取这个值,是因为 i 最大可以取 amount + 1,而最多的组成方式是只用 1 元硬币,因此 amount + 2 一定大于所有可能的组合方式,取最小值时一定不会是它。
class Solution(object):
    def coinChange(self, coins, amount):
        dp = [[amount + 2 for _ in range(amount+1)] for _ in range(len(coins))]
        
        for i in range(len(coins)):
            dp[i][0] = 0
        for i in range(len(coins)):
            for j in range(amount+1):
                dp[i][j] = dp[i-1][j]
                if j>=coins[i]:
                    dp[i][j] = min(dp[i-1][j],dp[i][j-coins[i]]+1)
        if dp[len(coins)-1][amount]==amount + 2:
            return -1
        else:
            return dp[len(coins)-1][amount]
  • 优化空间,降二维为一维

还记得我们的口诀吗?0-1 背包对物品的迭代放在外层,里层的体积或价值逆向遍历;完全背包对物品的迭代放在里层,外层的体积或价值正向遍历。

class Solution(object):
    def coinChange(self, coins, amount):
        dp = [amount + 2 for _ in range(amount+1)] 
        dp[0] = 0
        for j in range(amount+1):
            for i in range(len(coins)):
                if j>=coins[i]:
                    dp[j] = min(dp[j],dp[j-coins[i]]+1)

        
        if dp[amount]==amount + 2:
            return -1
        else:
            return dp[amount]

72. 编辑距离

在这里插入图片描述
构造数组dp[i][j],表示将第一个字符串word1从0到位置i 为止,转换成第二个字符串word2到位置 j 为止,最少需要的操作数。注意维度是len(word1)+1,len(word2)+1,多出来的维度方便迭代

但我们可以发现,如果我们有单词 A 和单词 B:

对单词 A 删除一个字符和对单词 B 插入一个字符是等价的。例如当单词 A 为 doge,单词 B 为 dog 时,我们既可以删除单词 A 的最后一个字符 e,得到相同的 dog,也可以在单词 B 末尾添加一个字符 e,得到相同的 doge;

同理,对单词 B 删除一个字符和对单词 A 插入一个字符也是等价的;

对单词 A 替换一个字符和对单词 B 替换一个字符是等价的。例如当单词 A 为 bat,单词 B 为 cat 时,我们修改单词 A 的第一个字母 b -> c,和修改单词 B 的第一个字母 c -> b 是等价的。

这样以来,本质不同的操作实际上只有三种:

在单词 A 中删除一个字符; 

在单词 B 中删除一个字符;

修改单词 A 的一个字符。

状态转移方程:若word1[i]≠word2[j],有三种修改方式:删除word1[i]

则dp[i][j]=dp[i-1][j]+1;删除word2[j],则dp[i][j]=dp[i][j-1]+1;修改单词 word1[i],设word1[:i] = horse,word2[:j]= ros,我们已知hors 到 ro 的编辑次数为dp[i-1][j-1] ,那么显然 horse 到 ros 的编辑距离不会超过 dp[i-1][j-1] +1。因此,我们可以得到如下转移矩阵

dp[i][j]=min(dp[i-1][j-1]+1,dp[i][j-1]+1,dp[i-1][j]+1)

动态规划过程如下
在这里插入图片描述

class Solution(object):
    def minDistance(self, word1, word2):
        m,n = len(word1),len(word2)
        dp = [[0 for _ in range(n+1)] for _ in range(m+1)]

        for i in range(m+1):
            for j in range(n+1):
                # 一直删2的字符,即在word1中插入字符
                if i==0:
                    dp[i][j]=j
                # 一直在Word1中删字符
                elif j==0:
                    dp[i][j]=i
                else:
                    if word1[i-1]!=word2[j-1]:
                        dp[i][j] = min(dp[i-1][j-1]+1, min(dp[i][j-1]+1,dp[i-1][j]+1))
                    else:
                        dp[i][j]=dp[i-1][j-1]
        return dp[m][n]
650. 只有两个键的键盘

在这里插入图片描述
诶,突然发现动态规划就是个找规律的题,小脑袋突然开窍?

让我们来研究下,不同的n需要多少次,如下表所示

|n| 0| 1|2 |3 |4 |5 |6 |7 |8 | 9|
|–|–|–|–|–|–|–|–|–|–|–|–|
| 次数| 0 |0 |2| 3| 4|5 |5 |7 |5 |6 |

n=1:本来就有A,所以不用操作
n=2:复制全部,即复制了1个A,粘贴全部。共操作2次
n=3:A->AA(复制+粘贴)->AAA(粘贴)。共操作3次
n=4:A->AA(复+粘)->AA AA (复+粘)。共操作4次
n=5:A->AA(复制+粘贴)->AAA(贴)->AAAA(贴)->AAAA(贴)。共操作5次
n=6:A->AA(复+粘)->AAA (粘)->AAA AAA(复+粘)。共操作5次。在AAA的基础上复制加粘贴,比n=3多了2次
n=7:一直粘贴单个A
n=8:在AAAA的基础上复制加粘贴,比n=4多了2次
n=9:在AAA的基础上复制加粘贴,在粘贴,比n=3多了2+1次

总结,n=i时,我们要找到i的最大因子j,i=j*k,在这个因子,基础上复制粘贴,消耗2次,再粘贴k-2次

class Solution(object):
    def minSteps(self, n):

        dp = [0 for _ in range(n+1)]
        if n>=2:
            dp[2]=2  
        if n>=3:
            dp[3]=3 
        
        if n>=4:
            for i in range(4,n+1):
                for j in range(i-1,2,-1):
                    if i%j==0:
                        dp[i] = dp[j]+2+(i//j-2) if i//j>=2 else dp[j]+2
                        break
                    dp[i] = i
        return dp[n]
  • 优化的解法

在这里插入图片描述
递推公式推导过程如下
在这里插入图片描述

10. 正则表达式匹配

在这里插入图片描述
股票交易

股票交易类问题通常可以用动态规划来解决。对于稍微复杂一些的股票交易类问题,比如需
要冷却时间或者交易费用,则可以用通过动态规划实现的状态机来解决。

121. 买卖股票的最佳时机
  • 题目描述

给定一段时间内每天的股票价格,已知你只可以买卖各 k 次,且每次只能拥有一支股票,求
最大的收益。

  • 输入输出样例

输入一个一维整数数组,表示每天的股票价格;以及一个整数,表示可以买卖的次数 k。输
出一个整数,表示最大的收益。

Input: [3,2,6,5,0,3], k = 2
Output: 7

在这个样例中,最大的利润为在第二天价格为 2 时买入,在第三天价格为 6 时卖出;再在第
五天价格为 0 时买入,在第六天价格为 3 时卖出。

在这里插入图片描述注意:由于在所有的 n天结束后,手上不持有股票对应的最大利润一定是严格由于手上持有股票对应的最大利润的,然而完成的交易数并不是越多越好(例如数组 prices单调递减,我们不进行任何交易才是最优的),因此最终的答案即为 sell[n−1][0…k]中的最大值。

边界处理:

class Solution:
    def maxProfit(self, k: int, prices: List[int]) -> int:
        if not prices:
            return 0

        n = len(prices)
        k = min(k, n // 2)
        buy = [[0] * (k + 1) for _ in range(n)]
        sell = [[0] * (k + 1) for _ in range(n)]

        buy[0][0], sell[0][0] = -prices[0], 0
        for i in range(1, k + 1):
            buy[0][i] = sell[0][i] = float("-inf")

        for i in range(1, n):
        		// 买的初始值是前i天最便宜的股票
            buy[i][0] = max(buy[i - 1][0], sell[i - 1][0] - prices[i])
            for j in range(1, k + 1):
                buy[i][j] = max(buy[i - 1][j], sell[i - 1][j] - prices[i])
                sell[i][j] = max(sell[i - 1][j], buy[i - 1][j - 1] + prices[i]);  

        return max(sell[n - 1])


作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-iv/solution/mai-mai-gu-piao-de-zui-jia-shi-ji-iv-by-8xtkp/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
class Solution {
public:
    int maxProfit(int k, vector<int>& prices) {
        if (prices.empty()) {
            return 0;
        }

        int n = prices.size();
        k = min(k, n / 2);
        vector<vector<int>> buy(n, vector<int>(k + 1));
        vector<vector<int>> sell(n, vector<int>(k + 1));

        buy[0][0] = -prices[0];
        sell[0][0] = 0;
        for (int i = 1; i <= k; ++i) {
            buy[0][i] = sell[0][i] = INT_MIN / 2;
        }

        for (int i = 1; i < n; ++i) {
            buy[i][0] = max(buy[i - 1][0], sell[i - 1][0] - prices[i]);
            for (int j = 1; j <= k; ++j) {
                buy[i][j] = max(buy[i - 1][j], sell[i - 1][j] - prices[i]);
                sell[i][j] = max(sell[i - 1][j], buy[i - 1][j - 1] + prices[i]);   
            }
        }

        return *max_element(sell[n - 1].begin(), sell[n - 1].end());
    }
};


作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-iv/solution/mai-mai-gu-piao-de-zui-jia-shi-ji-iv-by-8xtkp/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
class Solution {
public:
    int maxProfit(int k, vector<int>& prices) {
        int n=prices.size();
        //n天最多进行n/2次交易
        k=min(n/2,k);
        vector<int> buy(k+1,INT_MIN),sell(k+1,0);
        for(int i=0;i<n;i++){
            for(int j=1;j<=k;j++){
                buy[j]=max(buy[j],sell[j-1]-prices[i]);
                sell[j]=max(sell[j],buy[j]+prices[i]);
            }
        }
        return sell[k];
    }
};

309. 最佳买卖股票时机含冷冻期

在这里插入图片描述解析:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-with-cooldown/solution/fei-zhuang-tai-ji-de-dpjiang-jie-chao-ji-tong-su-y/

vector<vector<int>>dp(prices.size(),vector<int>(4));
    dp[0][0]=0;//不持有股票,本来就没有不是卖掉才没有的
    dp[0][1]=0;//不持有股票,卖出去才不持有股票的
    dp[0][2]=-1*prices[0];//持有股票,今天买入;
    dp[0][3]=-1*prices[0];//持有股票,非今天买入的;
    for(int i=1;i<prices.size();i++){
        dp[i][0]=max(dp[i-1][0],dp[i-1][1]);//前一天不持有股票的两种情况的最大值
        dp[i][1]=max(dp[i-1][2],dp[i-1][3])+prices[i];//今天卖出股票,来着前一天持有股票的最大值+pr
        dp[i][2]=dp[i-1][0]-prices[i];//今天买入股票,这前一天一定没有卖出股票
        dp[i][3]=max(dp[i-1][2],dp[i-1][3]);//今天没买股票,却持有股票,前一天继承来的,有两种情况
    }
    return max(dp[prices.size()-1][0],dp[prices.size()-1][1]);
}
213. 打家劫舍 II

在这里插入图片描述
环状排列意味着第一个房子和最后一个房子中只能选择一个偷窃,因此可以把此环状排列房间问题约化为两个单排排列房间子问题:

在不偷窃第一个房子的情况下(即 nums[1:]),最大金额是 p1 ;
在不偷窃最后一个房子的情况下(即 nums[:n−1]),最大金额是 p2。

综合偷窃最大金额: 为以上两种情况的较大值,即 max(p1,p2)。

注意下nums长度为1的情况

class Solution:
    def rob(self, nums: List[int]) -> int:
        def DP(nums):
            pper,per,cur = 0,0,0

            for i in range(len(nums)):
                cur = max(per,pper+nums[i])
                pper = per
                per = cur 

            return cur
        
        return max(DP(nums[:-1]),DP(nums[1:])) if len(nums) != 1 else nums[0]


53. 最大子数组和(Easy)

在这里插入图片描述
转移方程:dp[i] = max(dp[i-1]+nums[i],nums[i]),意味着前面dp[i-1]子序列和大于0时,继续添加,否则就从当前nums[i]开始重新开始算子数组。可能会有很多子数组,取其中和最大的

eg.nums = [-2,1,-3,4,-1,2,1,-5,4],我们算得的dp=[-2, 1, -2, 4, 3, 5, 6, 1, 5],关键是只要前面不为负,就可以一直添加数字使得序列和变大

class Solution(object):
    def maxSubArray(self, nums):
        
        dp = [0 for _ in range(len(nums))]
        dp[0] = nums[0] if len(nums)>0 else 0
        for i in range(1,len(nums)):
            dp[i] = max(dp[i-1]+nums[i],nums[i])
        
        return max(dp)

我们可以把一维降维,减少存储空间

用max_记录这个过程中子数组最大和

class Solution(object):
    def maxSubArray(self, nums):
        
        per,cur = 0,0
        per = nums[0] if len(nums)>0 else 0
        max_ = per
        for i in range(1,len(nums)):
            cur = max(per+nums[i],nums[i])
            per = cur
            max_ = max(max_,cur)
        
        return max_
343. 整数拆分

在这里插入图片描述

  • 思路(自己):首先是自己算了下n=1,2…10应该对应的输出是多少,n=[1,2,3,4,5,6,7,8,9,10]对应输出[1,1,2,4,6,9,12,18,27,36]。发现规律:n>=4之后的最大乘积都是由2和3相乘得来的,所以dp[i] = max(dp[i-2]*2,dp[i-3]*3)
  • 但需要注意的是,dp在2,3处的值并非等于2,3而是等于1,2.所以这时候需要判断,我们要乘的是真正的数值2,3而不是dp[2],dp[3]
    def integerBreak(self, n):
        dp = [1 for _ in range(n+1)]
        if n>=3:
            dp[3] = 2
        if n>=4:
            for i in range(4,n+1):
                if i-3<=3:
                    dp[i] = max((i-2)*2,(i-3)*3)
                else:
                    dp[i] = max(dp[i-2]*2,dp[i-3]*3)
        return dp[n]
583. 两个字符串的删除操作

在这里插入图片描述

class Solution:
    def minDistance(self, word1: str, word2: str) -> int:
        m, n = len(word1), len(word2)
        dp = [[0] * (n + 1) for _ in range(m + 1)]
        for i in range(1, m + 1):
            dp[i][0] = i
        for j in range(1, n + 1):
            dp[0][j] = j

        for i in range(1, m + 1):
            for j in range(1, n + 1):
                if word1[i - 1] == word2[j - 1]:
                    dp[i][j] = dp[i - 1][j - 1]
                else:
                    dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + 1
        
        return dp[m][n]

646. 最长数对链

在这里插入图片描述

  • 自己:先按照第一个数字从小到大排序,定义dp为序列结尾一定是pairs[i],形成的最长数对链长度。最后返回dp中最大的,因为最长链不一定是结尾为最后的。
class Solution(object):
    def findLongestChain(self, pairs):
        dp = [1 for _ in range(len(pairs))]

        pairs = sorted(pairs, key=lambda x: x[0])
        for i in range(1,len(pairs)):
            for j in range(i,-1,-1):
                if pairs[j][1]<pairs[i][0]:
                    dp[i] = max(dp[j]+1,dp[i])
        return max(dp)
376. 摆动序列

在这里插入图片描述
需要建立两个动态规划数组,up和down,up[i]表示nums[0:i] 中最后两个数字递增的最长摆动序列长度,down[i] 表示 nums[0:i] 中最后两个数字递减的最长摆动序列长度,只有一个数字时默认为 1。

下面直接给出降维版本的动态规划

public int wiggleMaxLength(int[] nums) {
    int down = 1, up = 1;
    for (int i = 1; i < nums.length; i++) {
        if (nums[i] > nums[i - 1])
            up = down + 1;
        else if (nums[i] < nums[i - 1])
            down = up + 1;
    }
    return nums.length == 0 ? 0 : Math.max(down, up);
}

494. Target Sum

给定一组数组nums和target, 在元素前添加正负号计算,使得最终和为target,返回添加正负号有几种

在这里插入图片描述

可以将这个题目用0-1背包问题求解,但我们不能让dp[i][j]表示前i个正负后和为j,然后返回dp[len-1][target]。首先是因为前i个可能为负,有点麻烦我不知道j=-1怎么搞。

那么,机智的方法是什么呢?已知题目给的nums中元素都是非零,我们假设nums[i]中取负数之后求和的绝对值是neg,本身nums求和后值为sum,那么我们剩下取正后求和的值就为sum-neg。所以,我们要找的是取正后求和的值-取负数之后求和的绝对值=target,即sum-neg-neg = target,所以neg = (target-sum)/2。

因此我们让dp[i][j]表示前i个正负后和为j,然后返回dp[len-1][neg]即可

在这里插入图片描述

class Solution {
    public int findTargetSumWays(int[] nums, int target) {
        int sum = 0;
        for (int num : nums) {
            sum += num;
        }
        int diff = sum - target;
        if (diff < 0 || diff % 2 != 0) {
            return 0;
        }
        int n = nums.length, neg = diff / 2;
        int[][] dp = new int[n + 1][neg + 1];
        dp[0][0] = 1;
        for (int i = 1; i <= n; i++) {
            int num = nums[i - 1];
            for (int j = 0; j <= neg; j++) {
                dp[i][j] = dp[i - 1][j];
                if (j >= num) {
                    dp[i][j] += dp[i - 1][j - num];
                }
            }
        }
        return dp[n][neg];
    }
}

分治法

在这里插入图片描述

241. Different Ways to Add Parentheses

给你一个由数字和运算符组成的字符串 expression ,按不同优先级组合数字和运算符,计算并返回所有可能组合的结果。你可以 按任意顺序 返回答案。

在这里插入图片描述
对于一个形如 x op y(op 为运算符,x 和 y 为数) 的算式而言,它的结果组合取决于 x 和 y 的结果组合数,而 x 和 y 又可以写成形如 x op y 的算式。

因此,该问题的子问题就是 x op y 中的 x 和 y:以运算符分隔的左右两侧算式解。

然后我们来进行 分治算法三步走:

分解:按运算符分成左右两部分,分别求解
解决:实现一个递归函数,输入算式,返回算式解
合并:根据运算符合并左右两部分的解,得出最终解
class Solution:
    def diffWaysToCompute(self, input: str) -> List[int]:
        # 如果只有数字,直接返回
        if input.isdigit():
            return [int(input)]

        res = []
        for i, char in enumerate(input):
            if char in ['+', '-', '*']:
                # 1.分解:遇到运算符,计算左右两侧的结果集
                # 2.解决:diffWaysToCompute 递归函数求出子问题的解
                left = self.diffWaysToCompute(input[:i])
                right = self.diffWaysToCompute(input[i+1:])
                # 3.合并:根据运算符合并子问题的解
                for l in left:
                    for r in right:
                        if char == '+':
                            res.append(l + r)
                        elif char == '-':
                            res.append(l - r)
                        else:
                            res.append(l * r)

        return res
932. Beautiful Array

给定n,返回1到n构成的数组,满足任意i<j,在i,j间都找不到k,使得 2 * nums[k] == nums[i] + nums[j],我们称这样的数组为漂亮数组,返回任意一个漂亮数组即可

在这里插入图片描述

  • 自顶向下
    构造过程如下:我们以n=10为例,最终得到了数组f(10)=[1,9,5,3,7,2,10,6,4,8]

我们可以发现,合的过程是由左右两部分构成:左边全是奇,右边全为偶,这样合并起来,会使得任意在左边选i,在右边选j,和均为奇数, 便找不到i,j之间的k使得2 * nums[k] == nums[i] + nums[j]。

接着,我们还需要保证i,j都选左边或者的时候,也不能找到这样的k。这就用到了一个性质:

如果{ X, Y, Z }是一个漂亮数组,则{ k * X + b, k * Y + b, k * Z + b } 也一定是漂亮数组

例如[1,5,3,2,4]是漂亮数组,那么2*x-1后得到[1,9,5,3,7]也是漂亮数组,这就使得我们保证i,j都选左边或者的时候,也不能找到这样的k。

如此,我们就找到了构成n的漂亮数组的过程。
在这里插入图片描述

class Solution(object):
    def beautifulArray(self, n):
        res = []
        if n==1:
            res.append(1)
            return res
        odd = (n+1)//2
        even = n//2

        # 分
        left = self.beautifulArray(odd)
        right = self.beautifulArray(even)

        # 合并
        for i in range(len(left)):
            res.append(2*left[i]-1)
        
        for i in range(len(right)):
            res.append(2*right[i])
        return res       
  • 优化:加入memo,记忆化搜索,提高速度

也就是说,比如把memo[5]=[1,9,5,3,7]给记录下来,省的一遍遍再算

class Solution:
    def beautifulArray(self, N):
        memo = {1: [1]}
        def f(N):
            if N not in memo:
                odds = f((N+1)/2)
                evens = f(N/2)
                memo[N] = [2*x-1 for x in odds] + [2*x for x in evens]
            return memo[N]
        return f(N)
  • 自底向上

在这里插入图片描述

巧解数学问题

辗转相除法求公倍数与公因数

在这里插入图片描述

在这里插入图片描述

204.计算质数(埃拉托斯特尼筛法)

在这里插入图片描述

  • 方法一:枚举

最简单的思路是,遍历到x处,如何判断x是否为质数?那就遍历2到x-1查看是不是x的因子,若是则x是合数,不是质数

更加优化的方案是,遍历2…根号x即可

class Solution {
    public int countPrimes(int n) {
        int ans = 0;
        for (int i = 2; i < n; ++i) {
            ans += isPrime(i) ? 1 : 0;
        }
        return ans;
    }

    public boolean isPrime(int x) {
        for (int i = 2; i * i <= x; ++i) {
            if (x % i == 0) {
                return false;
            }
        }
        return true;
    }
}

时间复杂度为 O ( n n ) O(n\sqrt n) O(nn ),空间复杂度为 O ( 1 ) O(1) O(1)

  • 埃氏筛

埃氏筛想法:如果 x 是质数,那么大于 x 的 x 的倍数 2x,3x,… 一定不是质数

具体做法:引入数组 isPrime[i]表示,如果i是质数isPrime[i]=1,否则为0,初始化isPrime均为1。i从2…n-1遍历,显然i的倍数(2倍以上)不是质数,因此对应的isPrime改为0。最后计算质数的个数,代码如下

class Solution {
    public int countPrimes(int n) {
        int[] isPrime = new int[n];
        Arrays.fill(isPrime, 1);
        int ans = 0;
        for (int i = 2; i < n; ++i) {
            if (isPrime[i] == 1) {
                ans += 1;
                if ((long) i * i < n) {
                    for (int j = i * i; j < n; j += i) {
                        isPrime[j] = 0;
                    }
                }
            }
        }
        return ans;
    }
}

表示为7进制(Easy)

在这里插入图片描述在这里插入图片描述
例如:num=100,a=100/7=14,b=100%7=2,把2写在最后一位
接着num=a=14,则a=14/7=2,b=14%7=0,把0写在最后一位
接着num=a=2,则a=2/7=0,b=2%7=2,把2写在最后一位
最后num=0结束

172. Factorial Trailing Zeroes

题目描述
给定一个非负整数,判断它的阶乘结果的结尾有几个 0。
输入输出样例
输入一个非负整数,输出一个非负整数,表示输入的阶乘结果的结尾有几个 0。

Input: 12
Output: 2
在这个样例中,12 != 479001600 的结尾有两个 0。

思路:每个尾部的 0 由 2 × 5 = 10 而来,因此我们可以把阶乘的每一个元素拆成质数相乘,统计有多少个 2 和 5。明显的,质因子 2 的数量远多于质因子 5 的数量,因此我们可以只统计阶乘结果里有多少个质因子 5。

具体:而 n!中质因子 5的个数等于 [1,n]的每个数的质因子 5的个数之和,我们可以通过遍历 [1,n]的所有 5的倍数求出。

class Solution(object):
    def trailingZeroes(self, n):
        ans = 0
        for i in range(5,n+1,5):
            # 每个数字统计有几个5因子
            while i%5 == 0:
                i = i//5
                ans += 1

        return ans
  • 优化

    实际上就是计算1-n之中有多少个5的因数。以130为例:

      第一次除以5时得到26,表明存在26个包含 [一] 个因数5的数;
      第二次除以5得到5,表明存在5个包含 [二] 个因数5的数(这些数字的一个因数5已经在第一次运算的时候统计了);
      第三次除以5得到1,表明存在1个包含 [三] 个因数5的数(这些数字的两个因数5已经在前两次运算的时候统计了);
      得到从1-n中所有5的因数的个数
    
class Solution {
    public int trailingZeroes(int n) {
        int ans=0;
        while(n!=0){
            n/=5;
            ans+=n;
        }
        return ans;
    }
}
415. 求和计算

在这里插入图片描述

class Solution(object):
    def addStrings(self, num1, num2):
        i,j = len(num1)-1, len(num2)-1

        l = 0
        ans = ''

        while i>=0 or j>=0:
            a = int(num1[i]) if i>=0 else 0
            b = int(num2[j]) if j>=0 else 0

            sum1 = a+b+l
            ans = ans+str(sum1%10)
            l = sum1/10
            i-=1
            j-=1

        if l!=0:
            ans = ans+str(l)
        
        return ans[::-1]
326. Power of Three 判断是否是3的幂次

在这里插入图片描述
x从1开始,1,3,9,27…一直到x<=n为止,求出最接近n的3的幂次x

class Solution(object):
    def isPowerOfThree(self, n):

        x = 1
        while x<=n:
            if x==n:
                return True
            x = 3*x

        return False

  • 一直除3

我们不断地将 n 除以 3,直到 n=1。如果此过程中 n 无法被 3整除,就说明 n不是 3 的幂。

本题中的 n 可以为负数或 0,可以直接提前判断该情况并返回 False,也可以进行试除,因为负数或 0也无法通过多次除以 3得到 1。

class Solution:
    def isPowerOfThree(self, n: int) -> bool:
        while n and n % 3 == 0:
            n //= 3
        return n == 1
  • 对数
    利用对数。设 log_3={n} =m,如果 n 是 3 的整数次方,那么 m 一定是整数

C函数名: fmod
功 能: 计算x对y的模, 即x/y的余数,若y是0,则返回NaN。

bool isPowerOfThree(int n) {
return fmod(log10(n) / log10(3), 1) == 0;
}
384. Shuffle an Array

在这里插入图片描述
在这里插入图片描述

class Solution {
    int[] nums;
    int n;
    Random random = new Random();
    public Solution(int[] _nums) {
        nums = _nums;
        n = nums.length;
    }
    public int[] reset() {
        return nums;
    }
    public int[] shuffle() {
        int[] ans = nums.clone();
        for (int i = 0; i < n; i++) {
            swap(ans, i, i + random.nextInt(n - i));
        }
        return ans;
    }
    void swap(int[] arr, int i, int j) {
        int c = arr[i];
        arr[i] = arr[j];
        arr[j] = c;
    }
}
获取个位数十位数百位数

num
个位 = num % 10
十位 = num // 10 % 10
百位 = num // 100 % 10
千位 = num // 1000

168. Excel Sheet Column Title(以1开头的26进制转换)

在这里插入图片描述

思路:1-26之间是单个数字,27-262是以A开头,262+1到263是B开头,…,2625+1到2626是以Z开头的两位数,2627就是以A开头的三位数

给定数字num,num%26表示最后一位数字,num//26%26表示倒数第二位,倒数第三位= num // (26*26) % 26

class Solution(object):
    def convertToTitle(self, columnNumber):

        num = columnNumber
        
        dic = {}
        for i in range(26):
            dic[i] = chr(65+i)
        st = ''
        while num>0:
            num = num-1
            st = st + dic[num%26]
            num = num//26
            
        return st[::-1]
67. Add Binary(二进制求和)

在这里插入图片描述
从后往前算,1+1=0且进1,所以最后一位是0
接着,倒数第二位a为1,b为0,加上之前的进1,所以1+0+1=0 且进1
最后,倒数第三位a和b均为0,加上之前的进1,所以0+0+1=1
因此,返回100

class Solution(object):
    def addBinary(self, a, b):
        i, j = len(a)-1, len(b)-1

        l = 0
        st = ''
        while i>=0 or j>=0:
            a1 = int(a[i]) if i>=0 else 0
            b1 = int(b[j]) if j>=0 else 0

            sum_ = a1+b1+l
            st = st + str(sum_%2)
            l = sum_/2

            i -= 1
            j -= 1
        if l>0:
            st = st + str(l)
        return st[::-1]
238. Product of Array Except Self(除自身以外数组的乘积)

你可以不使用除法做这道题吗?我们很早之前讲过的题目 135 或许能给你一些思路

  • 回顾135:分发糖果问题

在这里插入图片描述当时的解决方法:两次遍历,从左往右如果右边大于左边则右边加1,在从右往左,如果左边大于右边,再判断之前遍历后的糖果数是否也匹配,不匹配就加1

class Solution(object):
    def candy(self, ratings):
        n  = len(ratings)
        apple = [1 for _ in range(n)]
        # 从左往右遍历,右边是否大于左边
        for i in range(n-1):
            if ratings[i+1]>ratings[i]:
                apple[i+1] = apple[i]+1
        
        # 从右往左遍历,左边是否大于右边
        for i in range(n-1,0,-1):
            if ratings[i-1]>ratings[i]:
                apple[i-1] = max(apple[i-1], apple[i]+1)
            
        return sum(apple)
  • 我们现在再来看这题238,除自身以外数组的乘积
    注意:要求时间复杂度为O(n)

在这里插入图片描述
首先,定义乘积数组left,right,array,初始化为1。

从右往左,i从len-1到0,计算出位置i右边的乘积,放入right中

再从左往右,i从到0len-1,计算出位置i左边的乘积,放入left中

除自身以外数组的乘积array[i]=left[i]*right[i]

class Solution(object):
    def productExceptSelf(self, nums):
        n = len(nums)
        left, right =  [1 for _ in range(n)], [1 for _ in range(n)]

        for i in range(1,n):
            left[i] = left[i-1]*nums[i-1]
        for i in range(n-2,-1,-1):
            right[i] = right[i+1]*nums[i+1]
        
        array = map(lambda (a,b):a*b,zip(left,right))
        return array
  • 进阶:要求时间复杂度为O(n),空间复杂度为O(1)

以nums=[1,2,3,4]为例,则left,right数组分别代表i左边和右边的乘积,如下

left= [1,1,2,6],right = [24,6,3,1]

现在要求空间复杂度为O(1),注意输出数组不算进空间复杂度中,因此我们只需要常数的空间存放变量。

我们可以把right变为常量,right=1,然后从右往左遍历,输出数组为的left构造如下

left[3]=61
left[2]=left[2]14
left[1]=left[1]14
3

class Solution(object):
    def productExceptSelf(self, nums):
        n = len(nums)
        left = [1 for _ in range(n)]

        for i in range(1,n):
            left[i] = left[i-1]*nums[i-1]
        right = 1
        for i in range(n-2,-1,-1):
            right = right * nums[i+1]
            left[i] = left[i]*right
        
        return left
462. Minimum Moves to Equal Array Elements II(最少增减次数使得数组为相同元素)

在这里插入图片描述注意:每次只能加1或者减1

在这里插入图片描述
练习一下,我们用快速排序对数组进行排序

class Solution(object):
    def minMoves2(self, nums):
        
        def QucikSort(begin,end,nums):
            if begin>=end:
                return 
            l, r = begin, end
            key = nums[l]

            while l<r:
                # 两个while顺序不能换
                while l<r and nums[r]>=key:
                    r -= 1
                nums[l] = nums[r]
                while l<r and nums[l]<key:
                    l += 1
                nums[r] = nums[l]
            
            nums[l] = key
            QucikSort(begin,l-1,nums)
            QucikSort(l+1,end,nums)
        
        QucikSort(0,len(nums)-1,nums)
        
        ans = 0
        for i in range(len(nums)):
            ans += abs(nums[len(nums)/2]-nums[i])
        return ans

位运算

常见运算符的描述

在这里插入图片描述
在这里插入图片描述

461. Hamming Distance(汉明距离)
  • Easy
    在这里插入图片描述统计两数的二进制不同数的个数,例如1二进制是1,4的二进制是100,我们把1看成是001,所以和4的不同有2个。
class Solution(object):
    def hammingDistance(self, x, y):
        ans = 0
        while x or y:
            ans += 1 if x%2!=y%2 else 0
            x = x/2 
            y = y/2

        return ans 
            
  • 位运算求解

用异或计算(^),只有一个为0一个为1,才会被记作1。最后我们统计亦或得到的1

class Solution(object):
    def hammingDistance(self, x, y):
        
        ans = 0
        z = x^y
        while z:
            z = z&(z-1)
            ans += 1

        return ans 
            
位操作计某数的二进制中有多少个1
  • Easy

  • 关键:构造一个计算,使得每次操作完成后该数值少了1个1

  • 这个操作就是n&n-1

我们把n分为两种,分别是最低位是1和最低位是0

情况1: 如果最低位如果是1,n-1的最低位则变为0,与原数值与的时候,只有最低位变成0,因此少了一个1.

举个栗子:
计算5中1的个数,首先5的二进制为: 00000101
n & n-1 : 5 & 4 对应二进制为: 00000101 & 00000100 = 00000100

情况2: 如果最低位是0,n-1 需要向前借位,最后一个1变成0,后面的0变成1,则n & n-1则将借位的1消除掉。

n & (n - 1),可以将 n 的二进制表示中最低位的那个 1抹去变为0

例如2的二进制是10,则n-1的二进制就是01,相当于后面多1,前面少1,而我们需要把2中10中1消掉,所以可以直接用&,这样后面也不会有1前面也不会有1

举个栗子:
计算4中1的个数,首先4的二进制为: 00000100
n & n-1 : 5 & 4 对应二进制为: 00000100 & 00000011 = 00000000

具体代码如下

count = 0  
while(a){  
  a = a & (a - 1);  
  count++;  
}  
190. Reverse Bits(颠倒二进位)
  • Easy

在这里插入图片描述举例子:都是32位

2 的32 位应该是

0000 0000 0000 0000 0000 0000 0000 0010 

​ 反转后

0100 0000 0000 0000 0000 0000 0000 0000 

首先,获取2的个位数加到ans中,变成ans个位数,n&1是0,然后n即2右移,在接下来,ans左移,之前最后一位就到倒数二位了。

再获取2的倒数二位,变成ans个位数,然后n右移,在接下来,ans左移,之前倒数二位就到倒数三位了。

class Solution:
    # @param n, an integer
    # @return an integer
    def reverseBits(self, n):
        ans = 0
        for i in range(32):
            ans<<=1
            ans += n&1
            n>>=1
        return ans
136. Single Number(只出现一次的数字)
  • Easy
    给定数组,其中别的数都出现两次,只有一个数字出现一次,请你找到这个数并且返回
    在这里插入图片描述
    我们可以利用 x ∧ x = 0 和 x ∧ 0 = x 的特点,将数组内所有的数字进行按位异或。出现两次
    的所有数字按位异或的结果是 0,0 与出现一次的数字异或可以得到这个数字本身。
class Solution(object):
    def singleNumber(self, nums):
        ans = 0
        for num in nums:
            ans ^= num
        return ans

性质:A ^ (B ^ C) = (A ^ B) ^ C

解释:例如nums=[4,1,2,1,2],初始ans = 0
num=4, ans=ans^num= 0 ^ 4=4
num=1, ans =4^1
num=2,ans=4^1 ^2
num =1,ans=4^1 ^2 ^1= 4 ^2 ^(0)=4 ^2
num=2, ans = 4

231. Power of Two(判断是否是 2 的幂次)

一个数 n 是 2 的幂,当且仅当 n是正整数,并且 n的二进制表示中仅包含 1个 1。

关键:n & (n - 1)

n & (n - 1),可以将 n 的二进制表示中最低位的那个 1抹去变为0,再判断剩余的数值是否为 0 即可。

class Solution:
    def isPowerOfTwo(self, n: int) -> bool:
        return n > 0 and (n & (n - 1)) == 0
342. Power of Four(判断是否是 4 的幂次)
  • Easy
    在这里插入图片描述
    我们可以观察下,4的幂次二进制有什么特点?
    4->100(2个0)
    16->10000(4个0)
    64->1000000(6个0)

  • 方法一:先判断是否是2的幂次(n & (n - 1)) == 0 ),再判断除3是否余1

var isPowerOfFour = function(n) {
    return n > 0 && (n & (n - 1)) == 0 && n % 3 == 1
};

  • 方法二:如果这个数也是 4 的次方,那二进制表示中 1 的位置必须为奇数位。我们可以把 n 和二进制的 10101…101(即十进制下的 1431655765)做按位与,如果结果不为 0,那么说明这个数是 4 的次方。
bool isPowerOfFour(int n) {
	return n > 0 && !(n & (n - 1)) && (n & 1431655765);
}

318. Maximum Product of Word Lengths(最大单词长度乘积)

在这里插入图片描述
返回没有重复字母的最长两个单词的乘积,注意没有重复字母

338. Counting Bits(计算0到n的二进制分别有几个1)
  • Easy
    利用动态规划求解

在这里插入图片描述

vector<int> countBits(int num) {
        vector<int> result(num+1);
        result[0] = 0;
        for(int i = 1; i <= num; i++)
        {
            if(i % 2 == 1)
            {
                result[i] = result[i-1] + 1;
            }
            else
            {
                result[i] = result[i/2];
            }
        }
        
        return result;
    }
268. Missing Number
  • Easy

在这里插入图片描述
找到其中0到n缺少的数字

  • 暴力:直接遍历0到n,看哪个不在nums中
class Solution(object):
    def missingNumber(self, nums):
        for i in range(len(nums)+1):
            if i not in nums:
                return i
  • 高斯求和:求出1到n的和-sum(nums),就是我们要找的不在其中的数
class Solution(object):
    def missingNumber(self, nums):
        n = len(nums)
        return n*(n+1)/2-sum(nums)
  • 位运算^

x^x=0; 0 ^ x=x
所以一开始先ans =0, ans和nums中所有数做^运算

接着,对0到n中的数,在用ans对所有数做^运算,就能找到0到n中有,但是nums中没有的数了

class Solution {
    public int missingNumber(int[] nums) {
        int ans = 0;
        int n = nums.length;
        for (int i = 0; i < n; i++) {
            ans ^= nums[i];
        }
        for (int i = 0; i <= n; i++) {
            ans ^= i;
        }
        return ans;
    }
}
693. Binary Number with Alternating Bits(交替位二进制)
  • Easy

利用位运算判断一个数的二进制是否会出现连续的 0 和 1。

  • 最后一位和右移结合

首先,获取n的最后一位(n&1)
右移,判读右移后最后一位和之前的是否交替

class Solution(object):
    def hasAlternatingBits(self, n):

        # 获取n的最后一位
        a = n&1 
        while n:
            # 右移,判读右移后最后一位和之前的是否交替
            n >>= 1
            if n&1==a:
                return False
            a = n&1
        return True
  • 位运算的优化

另外一种更为巧妙的方式是利用交替位二进制数性质。
在这里插入图片描述
注意:不可以直接拿n&(n>>1)判断,例如4的二进制是100,n>>1就是010,n&(n>>1)就是000,但是4不是交替二进制。需要注意0和0取&还是0,我们需要前面的时候0和0碰在一起取0,后面0和0在一起取1。

476. Number Complement(数字的补数)
  • Easy
    在这里插入图片描述
    二进制翻转的变种题

  • 思路

注意:这里不能直接~ num,因为5的二进制共有32位,如果~的话会把前面的0也变成1,所以我们需要找出最高位 1的位置,再把后面的调换

一个简单的做法是:先对 num进行「从高到低」的检查,找到最高位 1的位置 s,然后再对 num 进行遍历,将低位到 s位的位置执行逐位取反操作

class Solution {
    public int findComplement(int num) {
        int s = -1;
        for (int i = 31; i >= 0; i--) {
            if (((num >> i) & 1) != 0) {
                s = i;
                break;
            }
        }
        int ans = 0;
        for (int i = 0; i < s; i++) {
            if (((num >> i) & 1) == 0) ans |= (1 << i);
        }
        return ans;
    }
}

例如5->101,那么应该往后移2格获得最高次的1,所以s=2

接着,ans初始化为0,分别num往后移0,1,…s格,比如说num移i格最后位数0,说明nums的i位置上是0,那么我们需要把他变为1

我们生成i位有数字1,其他为都为0的数,即1 << i。在此之前,ans我们最多是到了i-1位有数字,所以,ans |= (1 << i)表示我们把i位的数字变为1

260. Single Number III

- Medium

恰好有两个数出现一次,之前是只有一个数出现一次,其他数出现两次

  • 思路

首先,我们求出nums所有元素的^和xorsum ,因为其他数都出现两次,所以xorsum=a^b,其中a,b为仅出现一次的数

接着,我们用 x& -x取出 x 的二进制中出现1的最低位,设其为第 l l l 位,则由于^的特性,必然是a,b中一个在 l l l 位是1,另一个是0。

这样一来,我们就可以把 nums 中的所有元素分成两类,其中一类包含所有二进制表示的第 l l l 位为 0的数,另一类包含所有二进制表示的第 l l l 位为 1的数,再对每类进行^运算,就可以得到a,b。

问题是如何把 nums 中的所有元素分成两类?不妨设lsb = xorsum & (-xorsum),lsb则是最低位的1保留,其余全为0。如果 num & lsb==0,说明num在 l l l位没有1,否则有1,这样就可以区分了。

class Solution:
    def singleNumber(self, nums: List[int]) -> List[int]:
        xorsum = 0
        for num in nums:
            xorsum ^= num
        
        lsb = xorsum & (-xorsum)
        type1 = type2 = 0
        for num in nums:
            if num & lsb:
                type1 ^= num
            else:
                type2 ^= num
        
        return [type1, type2]


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值