算法个人学习笔记

86 阅读11分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

根据数据范围估算时间复杂度

算法的时间复杂度是大致确定的,但是数据范围却千变万化。所以根据数据范围选择最优算法是一种简单而准确的方法。

以下介绍几种根据数据范围大致对应的时间复杂度。

  1. N20N\leq 20 O(2n)O(2^n)
  2. 20<N10020<N\leq 100 O(n3)O(n^3)
  3. 100<N1000100<N\leq 1000 O(n2)O(n^2)
  4. 10000<N10510000<N\leq 10^5 O(nlogn)O(nlogn)
  5. 105<N10810^5<N\leq 10^8 O(n)O(n)
  6. N>108N>10^8 O(logn)O(logn)

十大经典排序算法

1、插入排序

public static void insertSort(int[] nums) {
  // [0, i]是有序的
  for (int i = 0; i < nums.length - 1; ++i) {
    int j = i + 1;
    int tmp = nums[i + 1];
    // 帮i+1找到一个合适的位置,并插入
    for (; j - 1 >= 0 && tmp < nums[j - 1]; --j) {
      nums[j] = nums[j - 1];
    }
    nums[j] = tmp;
  }
}

2、选择排序

public static void selectSort(int[] nums) {
  for (int i = 0; i < nums.length; ++i) {
    int min = nums[i];
    int minIdx = i;
    // 在[i+1, nums.length-1]中找到小于nums[i]且最小的元素进行交换
    for (int j = i + 1; j < nums.length; ++j) {
      if (nums[j] < min) {
        min = nums[j];
        minIdx = j;
      }
    }
    nums[minIdx] = nums[i];
    nums[i] = min;
  }
}

3、冒泡排序

public static void bubbleSort(int[] nums) {
  for (int i = 0; i < nums.length; ++i) {
    // 不停地暴力交换
    for (int j = 0; j < nums.length - 1; ++j) {
      if (nums[j] > nums[j + 1]) {
        swap(nums, j, j + 1);
      }
    }
  }
}

4、希尔排序

public static void shellSort(int[] nums) {
  for (int gap = nums.length / 2; gap > 0; gap /= 2) {
    // 同插入排序,但是每次处理的数据间隙是逐步减小的
    for (int i = 0; i + gap < nums.length; i += gap) {
      int tmp = nums[i + gap];
      int j = i + gap;
      for (; j - gap >= 0 && tmp < nums[j - gap]; j -= gap) {
        nums[j] = nums[j - gap];
      }
      nums[j] = tmp;
    }
  }
}

5、归并排序 merge(nums, 0, nums.length - 1)

public static void merge(int[] nums, int left, int right) {
  int mid = left + (right - left) /2;
  if (left < right) {
    merge(nums, left, mid);
    merge(nums, mid + 1, right);
    mergeSort(nums, left, mid, right);
  }
}

private static void mergeSort(int[] nums, int left, int mid, int right) {
  int[] tmp = new int[right - left + 1];
  int index = 0;
  int indexA = left, indexB = right;

  while (indexA <= mid && indexB <= right) {
    if (nums[indexA] <= nums[indexB]) {
      tmp[index++] = nums[indexA++];
    } else {
      tmp[index++] = nums[indexB++];
      ans += mid - indexA + 1;
    }
  }
  while (indexA <= mid) {
    tmp[index++] = nums[indexA++];
  }
  while (indexB <= right) {
    tmp[index++] = nums[indexB++];
  }
  System.arraycopy(tmp, 0, nums, from, tmp.length);
}

6、快速排序

public static void quickSort(int[] nums) {
  quickSort0(nums, 0, nums.length-1);
}

// [from, to]
private static void quickSort0(int[] nums, int from, int to) {
  if (from >= to) {
    return ;
  }
  int cmp = nums[from];
  int l = from, r = to;
  while (l < r) {
    while (l <= r && nums[l] <= cmp) {
      ++l;
    }
    while (l <= r && nums[r] > cmp) {
      --r;
    }
    if (l < r) {
      swap(nums, l, r);
    }
  }
  swap(nums, from, r);
  quickSort0(nums, from, r-1);
  quickSort0(nums, r+1, to);
}

public void add(int[] nums, int i, int val){
  nums[i] = val;
  int curIndex = i;
  while (curIndex > 0) {
    int parentIndex = (curIndex - 1) / 2;
    if (nums[parentIndex] < nums[curIndex])
      swap(nums, parentIndex, curIndex);
    else break;
    curIndex = parentIndex;
  }
}

public int remove(int[] nums, int size){
  int result = nums[0];
  nums[0] = nums[size - 1];
  int curIndex = 0;
  while (true) {
    int leftIndex = curIndex * 2 + 1;
    int rightIndex = curIndex * 2 + 2;
    if (leftIndex >= size) break;
    int maxIndex = leftIndex;
    if (rightIndex < size && nums[maxIndex] < nums[rightIndex])
      maxIndex = rightIndex;
    if (nums[curIndex] < nums[maxIndex])
      swap(nums, curIndex, maxIndex);
    else break;
    curIndex = maxIndex;
  }
  return result;
}

private static void swap(int[] nums, int idxA, int idxB) {
  int tmp = nums[idxA];
  nums[idxA] = nums[idxB];
  nums[idxB] = tmp;
}

递归和动态规划 DP

斐波那契数列

1, 1, 2, 3, 5, 8, ... 求第n项;

1)暴力递归 -- O(2N)O(2^N)

public int f1(int n) {
  if (n < 1) return 0;
  if (n == 1 || n == 2) return 1;
  return f1(n - 1) + f1(n - 2);
}

2)顺序计算 -- O(N)O(N)

public int f2(int n) {
  if (n < 1) return 0;
  if (n == 1 || n == 2) return 1;
  int res = 1, pre = 1, tmp = 0;
  for (int i = 3; i <= n; i++) {
    tmp = res;
    res += pre;
    pre = tmp;
  }
  return res;
}

3)加速矩阵乘法的动态规划 -- O(logN)O(logN)

(F(n),F(n1))=(F(n1),F(n2))×1110=(1,1)×1110n2(F(n),F(n-1))=(F(n-1),F(n-2))\times \begin{vmatrix} 1 & 1 \\ 1 & 0 \\ \end{vmatrix}=(1,1)\times {\begin{vmatrix} 1 & 1 \\ 1 & 0 \\ \end{vmatrix}}^{n-2}

// 求矩阵m的p次方
public int[][] matrixPower(int[][] m, int p) {
  int[][] res = new int[m.length][m[0].length];
  // 先设res为单位矩阵
  for (int i = 0; i < res.length; i++) res[i][i] = 1;
  int [][] tmp = m;
  // 根据p的二进制数形式进行累乘
  for (; p != 0; p >>= 1) {
    if ((p & 1) != 0) {
      res = multiMatrix(res, tmp);
    }
    tmp = multiMatrix(tmp, tmp);
  }
  return res;
}
// 两矩阵相乘
public int[][] multiMatrix(int[][] m1, int[][] m2) {
  int[][] res = new int[m1.length][m2[0].length];
  for (int i = 0; i < m1.length; i++) {
    for (int j = 0; j < m2[0].length; j++) {
      for (int k = 0; k < m2.length; k++) {
        res[i][j] += m1[i][k] * m2[k][j];
      }
    }
  }
  return res;
}

public int f3(int n) {
  if (n < 1) return 0;
  if (n == 1 || n == 2) return 1;
  int [][] base = {{1, 1}, {1, 0}};
  int [][] res = matrixPower(base, n - 2);
  return res[0][0] + res[1][0];
}

矩阵的最小路径和

给定一个矩阵m,从左上角开始只能向右或者向下走,最后达到右下角,返回路径上所有数字加起来的最小和;

输入: 1 3 5 9 8 1 3 4 5 0 6 1 8 8 4 0 输出:12 解释:1-3-1-0-6-1-0

1)dp 矩阵 -- 时间 O(M×N)O(M\times N) - 空间 O(M×N)O(M\times N)

首先确定第一行和第一列的路径和,其次根据上边和左边的较小值得到整个路径和的矩阵;

public int minPathSum1(int[][] m) {
  if (m == null || m.length == 0 || m[0] == null || m[0].length == 0) {
    return 0;
  }
  int row = m.length, col = m[0].length;
  // 初始化dp矩阵
  int[][] dp = new int[row][col];
  dp[0][0] = m[0][0];
  for (int i = 1; i < row; i++) {
    dp[i][0] = dp[i - 1][0] + m[i][0];
  }
  for (int j = 1; j < col; j++) {
    dp[0][j] = dp[0][j - 1] + m[0][j];
  }
  for (int i = 1; i < row; i++) {
    for (int j = 1; j < col; j++) {
      dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + m[i][j];
    }
  }
  // 返回结果
  return dp[row - 1][col - 1];
}

2)空间压缩法(仅适于求最优解,不可回溯) -- 时间 O(M×N)O(M\times N) - 空间 O(min{M,N})O(min\{M,N\})

public int minPathSum2(int[][] m) {
  if (m == null || m.length == 0 || m[0] == null || m[0].length == 0) {
    return 0;
  }
  // 定义行数与列数中的较大和较小的身份
  int more = Math.max(m.length, m[0].length);
  int less = Math.min(m.length, m[0].length);
  // 行数是否大于列数
  boolean rowMore = more == m.length;
  int[] arr = new int[less]; // 辅助数组的长度仅为较小值
  arr[0] = m[0][0];
  for (int i = 1; i < less; i++) {
    arr[i] = arr[i - 1] + (rowMore ? m[0][i] : m[i][0]);
  }
  for (int i = 1; i < more; i++) {
    arr[0] += rowMore ? m[i][0] : m[0][i];
    for (int j = 1; j < less; j++) {
      arr[j] = Math.min(arr[j - 1], arr[j]) + (rowMore ? m[i][j] : m[j][i]);
    }
  }
  return arr[less - 1];
}

换钱的最少货币数

arr中所有值都是正数且不重复,每个值代表一种面值的货币(可以使用任意张),求组成 aim(要找的钱数)的最少货币数;

eg. arr=[5,2,3], aim=20 4张5元,return 4 arr=[5,2,3], aim=0 不使用任何货币,return 0 arr=[3,5], aim=2 不能找开,return -1

空间压缩法 -- 时间 O(N×aim)O(N\times aim) - 空间 O(aim)O(aim)

public int minCoins2(int[] arr, int aim) {
  if (arr == null || arr.length == 0 || aim < 0) return -1;
  int max = Integer.MAX_VALUE;
  int[] dp = new int[aim + 1];
  for (int j = 1; j <= aim; j++) {
    dp[j] = max;
    if (j - arr[0] >= 0 && dp[j - arr[0]] != max) {
      dp[j] = dp[j - arr[0]] + 1;
    }
  }
  int tmp;
  for (int i = 1; i < arr.length; i++) {
    for (int j = 1; j <= aim; j++) {
      tmp = max;
      if (j - arr[i] >= 0 && dp[j - arr[i]] != max) {
        tmp = dp[j - arr[i]] + 1;
      }
      dp[j] = Math.min(tmp, dp[j]);
    }
  }
  return dp[aim] == max ? -1 : dp[aim];
}

换钱的方法数

arr中所有值都是正数且不重复,每个值代表一种面值的货币(可以使用任意张),求组成aim(要找的钱数)的方法数;

eg. arr=[5,10,25,1], aim=0 return 1 arr=[5,10,25,1], aim=15 return 6 arr=[3,5], aim=2 return 0

1)暴力递归 -- 时间 O(aimN)O(aim^N)

public int coins1(int[] arr, int aim) {
  if (arr == null || arr.length == 0 || aim < 0) return 0;
  return process1(arr, 0, aim);
}

// 如果用arr[index..N-1]面值的钱组成aim,返回总的方法数
public int process1(int[] arr, int index, int aim) {
  int res = 0;
  if (index == arr.length) {
    res = aim == 0 ? 1 : 0; // 递归出口
  } else {
    for (int i = 0; arr[index] * i <= aim; i++) {
      res += process1(arr, index + 1, aim - arr[index] * i);
    }
  }
  return res;
}

2)记忆化搜索方法(n维map存储已经计算出的值,n为递归影响因素的个数) -- 时间 O(N×aim2)O(N\times aim^2)

记忆化搜索是对必须要计算的递归过程才去计算并记录的。

3)动态规划的优化 -- 时间 O(N×aim)O(N\times aim)

public int coins4(int[] arr, int aim) {
  if (arr == null || arr.length == 0 || aim < 0) return 0;
  int[][] dp = new int[arr.length][aim + 1];
  for (int i = 0; i < arr.length; i++) {
    dp[i][0] = 1;
  }
  for (int j = 1; arr[0] * j <= aim; j++) {
    dp[0][arr[0] * j] = 1;
  }
  for (int i = 1; i < arr.length; i++) {
    for (int j = 1; j <= aim; j++) {
      dp[i][j] = dp[i - 1][j]; // 0张此面值的钱的情况
      dp[i][j] += j - arr[i] >= 0 ? dp[i][j - arr[i]] : 0; // 判断是不是越界了
    }
  }
  return dp[arr.length - 1][aim];
}

4)动态规划的优化 + 空间压缩 -- 时间 O(N×aim)O(N\times aim) - 空间 O(aim)O(aim)

public int coins5(int[] arr, int aim) {
  if (arr == null || arr.length == 0 || aim < 0) return 0;
  int[] dp = new int[aim + 1];
  for (int j = 0; arr[0] * j <= aim; j++) {
    dp[arr[0] * j] = 1;
  }
  for (int i = 1; i < arr.length; i++) {
    for (int j = 1; j <= aim; j++) {
      dp[j] += j - arr[i] >= 0 ? dp[j - arr[i]] : 0;
    }
  }
  return dp[aim];
}

正则表达式匹配

给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 '.' 和 '*' 的正则表达式匹配。

'.' 匹配任意单个字符 '*' 匹配零个或多个前面的那一个元素 所谓匹配,是要涵盖 整个 字符串 s 的,而不是部分字符串。

示例 1: 输入:s = "aa", p = "a" 输出:false 解释:"a" 无法匹配 "aa" 整个字符串。

示例 2: 输入:s = "aa", p = "a*" 输出:true 解释:因为 '*' 代表可以匹配零个或多个前面的那一个元素, 在这里前面的元素就是 'a'。因此,字符串 "aa" 可被视为 'a' 重复了一次。

示例 3: 输入:s = "ab", p = ".*" 输出:true 解释:".*" 表示可匹配零个或多个('*')任意字符('.')。

public boolean isMatch(String s, String p) {
  int m = s.length(), n = p.length();
  boolean[][] dp = new boolean[m + 1][n + 1];
  dp[0][0] = true;

  for (int i = 0; i <= m; i++) {
    for (int j = 1; j <= n; j++) {
      if (p.charAt(j - 1) != '*') {
        if (i >= 1 && (s.charAt(i - 1) == p.charAt(j - 1) || p.charAt(j - 1) == '.')) {
          dp[i][j] = dp[i - 1][j - 1];
        }
      } else {
        if (j >= 2 && i >= 1 && (s.charAt(i - 1) == p.charAt(j - 2) || p.charAt(j - 2) == '.')) {
          dp[i][j] = dp[i - 1][j];
        }
        if (j >= 2) {
          dp[i][j] |= dp[i][j - 2];
        }
      }
    }
  }
  return dp[m][n];
}

经典题目

接雨水

给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。

img

输入:height = [0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1] 输出:6 解释:上面是由数组 [0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。

public int trap(int[] height) {
  int n = height.length;
  if (n == 0) {
    return 0;
  }
  
  // 统计左投影下的雨水量
  int[] leftMax = new int[n];
  leftMax[0] = height[0];
  for (int i = 1; i < n; ++i) {
    leftMax[i] = Math.max(leftMax[i - 1], height[i]);
  }
  
  // 统计右投影下的雨水量
  int[] rightMax = new int[n];
  rightMax[n - 1] = height[n - 1];
  for (int i = n - 2; i >= 0; --i) {
    rightMax[i] = Math.max(rightMax[i + 1], height[i]);
  }

  int ans = 0;
  for (int i = 0; i < n; ++i) {
    ans += Math.min(leftMax[i], rightMax[i]) - height[i];
  }
  return ans;
}

计算右侧小于当前元素的个数

给你一个整数数组 nums ,按要求返回一个新数组 counts 。数组 counts 有该性质: counts[i] 的值是 nums[i] 右侧小于 nums[i] 的元素的数量。

示例 1: 输入:nums = [5,2,6,1] 输出:[2,1,1,0] 解释: 5 的右侧有 2 个更小的元素 (2 和 1) 2 的右侧仅有 1 个更小的元素 (1) 6 的右侧有 1 个更小的元素 (1) 1 的右侧有 0 个更小的元素

示例 2: 输入:nums = [-1] 输出:[0]

示例 3: 输入:nums = [-1,-1] 输出:[0,0]

class Solution {
  static int[] ansNum;
  static int[] indexes;

  public List<Integer> countSmaller(int[] nums) {
    indexes = new int[nums.length];
    ansNum = new int[nums.length];
    for (int i = 0; i < nums.length; i++) {
      indexes[i] = i;
    }
    merge(nums, 0, nums.length - 1);
    List<Integer> ans = new ArrayList<>();
    for (int num : ansNum) {
      ans.add(num);
    }
    return ans;
  }

  void merge(int[] nums, int left, int right) {
    int mid = left + (right - left) / 2;
    if (left < right) {
      merge(nums, left, mid);
      merge(nums, mid + 1, right);
      mergeSort(nums, left, mid, right);
    }
  }

  void mergeSort(int[] nums, int left, int mid, int right) {
    int[] tmp = new int[right - left + 1];
    int[] tmp_indexes = new int[right - left + 1];
    int index = 0;
    int indexA = left, indexB = mid + 1;

    while (indexA <= mid && indexB <= right) {
      if (nums[indexA] <= nums[indexB]) {
        tmp_indexes[index] = indexes[indexA];
        // 增加符合条件的个数
        ansNum[indexes[indexA]] += indexB - mid - 1;
        tmp[index++] = nums[indexA++];
      } else {
        tmp_indexes[index] = indexes[indexB];
        tmp[index++] = nums[indexB++];
      }
    }
    while (indexA <= mid) {
      tmp_indexes[index] = indexes[indexA];
      // 增加符合条件的个数
      ansNum[indexes[indexA]] += indexB - mid - 1;
      tmp[index++] = nums[indexA++];
    }
    while (indexB <= right) {
      tmp_indexes[index] = indexes[indexB];
      tmp[index++] = nums[indexB++];
    }
    System.arraycopy(tmp, 0, nums, left, tmp.length);
    System.arraycopy(tmp_indexes, 0, indexes, left, tmp_indexes.length);
  }
}

数组题目

和为K的子数组

给你一个整数数组 nums 和一个整数 k ,请你统计并返回该数组中和为 k 的子数组的个数。

示例 1: 输入:nums = [1,1,1], k = 2 输出:2

示例 2: 输入:nums = [1,2,3], k = 3 输出:2

// 构建前缀和数组,空间换时间
public int subarraySum(int[] nums, int k) {
  int n = nums.length;
  int[] preSum = new int[n + 1];
  preSum[0] = 0;
  for (int i = 0; i < n; i++) {
    preSum[i + 1] = nums[i] + preSum[i];
  }

  int ans = 0;
  for (int left = 0; left < n; left++) {
    for (int right = left; right < n; right++) {
      if (preSum[right + 1] - preSum[left] == k) {
        ans++;
      }
    }
  }
  return ans;
}

回溯法

全排列

给定一个不含重复数字的数组 nums ,返回其所有可能的全排列。你可以按任意顺序返回答案。

示例 1: 输入:nums = [1,2,3] 输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

示例 2: 输入:nums = [0,1] 输出:[[0,1],[1,0]]

示例 3: 输入:nums = [1] 输出:[[1]]

public List<List<Integer>> permute(int[] nums) {
  List<List<Integer>> res = new ArrayList<List<Integer>>();

  List<Integer> output = new ArrayList<Integer>();
  for (int num : nums) {
    output.add(num);
  }

  backtrack(nums.length, output, res, 0);
  return res;
}

public void backtrack(int n, List<Integer> output, List<List<Integer>> res, int first) {
  // 所有数都填完了
  if (first == n) {
    res.add(new ArrayList<Integer>(output));
  }
  for (int i = first; i < n; i++) {
    // 动态维护数组
    Collections.swap(output, first, i);
    // 继续递归填下一个数
    backtrack(n, output, res, first + 1);
    // 撤销操作
    Collections.swap(output, first, i);
  }
}