算法与数据结构笔记

[TOC]

一、专题系列:

1. 二叉树:

1-1 递归遍历:

  • 前序遍历

    public void preOrder(TreeNode root, List<Integer> res) {
        if (root == null) {
          return;
      }
      res.add(root.val);
      preOrder(root.left, res);
      preOrder(root.right, res);
    }
  • 中序遍历

    public void inOrder(TreeNode root, List<Integer> res) {
        if (root == null) {
         return;
     }
     inOrder(root.left, res);
     res.add(root.val);
     inOrder(root.right, res);
    }
  • 后序遍历

    public void postOrder(TreeNode root, List<Integer> res) {
        if (root == null) {
         return;
     }
     postOrder(root.left, res);
     postOrder(root.right, res);
     res.add(root.val);
    }

1-2 迭代遍历:

本质上是在模拟递归,因为在递归的过程中使用了系统栈,所以在迭代的解法中常用Stack来模拟系统栈。

  • 前序遍历:

    从根节点开始,一直沿左子树寻找下去,一边将节点入栈,一边将节点的值加入到列表中,直到为空;为空后弹出栈顶元素,此时该节点以及它的左子树全部遍历完成,将节点的右子树指针获取到,继续执行上面的操作。

    public List<Integer> preOrderNonRecursion(TreeNode root) {
        List<Integer> result = new ArrayList<>();
        TreeNode node = root;
        Stack<TreeNode> st = new Stack<>();
        while (!st.empty() || node != null) {
            while(node!=null){
                st.push(node);
                result.add(node.val);
                node = node.left;
            }
            node = st.pop();
            node = node.right;
        }
        return result;
    }
  • 中序遍历:

    中序遍历与前序遍历相比,只是添加元素的位置不一样。

    public List<Integer> inOrderNonRecursion(TreeNode root) {
        List<Integer> result = new ArrayList<>();
        TreeNode node = root;
        Stack<TreeNode> st = new Stack<>();
        while (!st.empty() || node != null) {
            while(node!=null){
                st.push(node);
                node = node.left;
            }
            node = st.pop();
            result.add(node.val);
            node = node.right;
        }
        return result;
    }
  • 后序遍历:

    public List<Integer> postOrderNonRecursion(TreeNode root) {
        Stack<TreeNode> st = new Stack<>();
        List<Integer> res = new ArrayList<Integer>();
        TreeNode node = root;
        TreeNode prev = null;
        while (!st.empty() || node != null) {
            while (node != null) {
                st.push(node);
                node = node.left;
            }
            node = st.pop();
            if (node.right == null || node.right == prev) {  //左子树遍历完,只有在右子树为空的情况下才添加该节点元素
                res.add(node.val);
                prev = node;
                node = null;
            } else {  //对于右子树非空的情况,需要将栈顶弹出的元素入栈,先去处理右子树的元素
                st.push(node);
                node = node.right;
            }
        }
        return res;
    }
    
    /**
     *  方法二
     */
    public List<Integer> postOrder2(ListNode h){
         Stack<TreeNode> st = new Stack<>();
        List<Integer> res = new ArrayList<Integer>();
         st.push(h);
         TreeNode c = null;
         while(!st.isEmpty()){
             c = st.peek();
             if(c.left != null && h != c.left && h !=c.right){
                 st.push(c.left);
            }else if(c.righ != null && h != c.right){
                 st.push(c.right);
            }else{
                 res.add(st.pop().val);
                 h = c;
            }
        }
         return res;
    }
  • 层序遍历:

    层序遍历的实现采用一个队列,初始队列只有一个根节点,代表第一层,队列的长度就是每一层的元素个数,根结点从队尾弹出以后,分别将左右非空子节点从队尾入队,这样一次循环,注意,每一次处理完当前层的节点后,队列的长度就是下一层节点的数量。

    public static List<List<Integer>> levelOrder(TreeNode root) {
        if (root == null) {
            return new ArrayList<>();
        }
        Queue<TreeNode> nodeQueue = new LinkedList<>();
        TreeNode node = root;
        nodeQueue.offer(node);
        int count = 0;
        List<List<Integer>> result = new ArrayList<>();
        while (!nodeQueue.isEmpty()) {
            List<Integer> list = new ArrayList<>();
            count = nodeQueue.size();
            while (count > 0) {
                node = nodeQueue.poll();
                list.add(node.val);
                if (node.left != null) {
                    nodeQueue.offer(node.left);
                }
                if (node.right != null) {
                    nodeQueue.offer(node.right);
                }
                count--;
            }
            result.add(list);
        }
        return result;
    }

1-3 二叉树构建:

  • 二叉树性质:

    如果将中序遍历反序,则得到反向的中序遍历,即每次遍历右孩子,再遍历根节点,最后遍历左孩子。 如果将后序遍历反序,则得到反向的前序遍历,即每次遍历根节点,再遍历右孩子,最后遍历左孩子。

  • 已知一棵二叉树的中序遍历和后序遍历,构建出二叉树

​ 【题目可以保证每个节点的数值不一致】

​ 根据遍历的性质,后序遍历的最后一个节点就是每棵子树的根节点,而且后序遍历列表中数据分布是[ [左子树的中序遍历结果], [右子树的中序遍历结果], 根节点 ],所以根据这个性质,我们可以利用根节点在中序遍历的数组中的下标位置,然后将中序遍历的数组分成左右两棵子树,每个部分继续用同样的方法递归构造即可。

int postIndex;
int[] inOrder;
int[] postOrder;
Map<Integer, Integer> idxMap = new HashMap<>();

public TreeNode buildTree(int[] inorder, int[] postorder) {
    this.inOrder = inorder;
    this.postOrder = postorder;
    postIndex = postOrder.length - 1;
    int idx = 0;
    for (Integer val : inOrder) {   //将中序遍历的结果存储在map中,方便根据值查找下标
        idxMap.put(val, idx++);
    }
    return createTree(0, inOrder.length - 1);
}

public TreeNode createTree(int inLeft, int inRight) {
    if (inLeft > inRight) {
        return null;
    }
    int rootVal = postOrder[postIndex];
    TreeNode node = new TreeNode(rootVal);
    int index = idxMap.get(rootVal);
    postIndex--;
    node.right = createTree(index + 1, inRight);    //先右后左
    node.left = createTree(inLeft, index - 1);
    return node;
}
  • 根据一棵二叉树的前序遍历和中序遍历构建出二叉树
int preIndex;
int[] inOrder;
int[] preOrder;
Map<Integer, Integer> idxMap = new HashMap<>();

public TreeNode buildTree(int[] preorder, int[] inorder) {
    this.inOrder = inorder;
    this.preOrder = preorder;
    preIndex = 0;           //初始值不一样,前序遍历是从前往后
    int idx = 0;
    for (Integer val : inOrder) {
        idxMap.put(val, idx++);
    }
    return createTree(0, inOrder.length - 1);
}

public TreeNode createTree(int inLeft, int inRight) {
    if (inLeft > inRight) {
        return null;
    }
    int rootVal = preOrder[preIndex];
    TreeNode node = new TreeNode(rootVal);
    int index = idxMap.get(rootVal);
    preIndex++;
    node.left = createTree(inLeft, index - 1);      //先左后右
    node.right = createTree(index + 1, inRight);
    return node;
}

1.4 计算二叉树节点:

public int countNodes(TreeNode root) {
    if (root == null) {
        return 0;
    }
    return 1 + countNodes(root.left) + countNodes(root.right);
}

更低复杂度的实现方法:

因为一棵完全二叉树的两棵子树,至少有一棵是满二叉树。

public int countNodes2(TreeNode root) {
    TreeNode l = root, r = root;
    // 记录左、右子树的高度
    int hl = 0, hr = 0;
    while (l != null) {
        l = l.left;
        hl++;
    }
    while (r != null) {
        r = r.right;
        hr++;
    }
    // 如果左右子树的高度相同,则是一棵满二叉树
    if (hl == hr) {
        return (int)Math.pow(2, hl) - 1;
    }
    // 如果左右高度不同,则按照普通二叉树的逻辑计算
    return 1 + countNodes(root.left) + countNodes(root.right);
}

1.5 序列化与反序列化:

  • 递归遍历实现:
// Encodes a tree to a single string.
public String serialize(TreeNode root) {
    return preSerialize(root, "");
}

public String preSerialize(TreeNode node, String str) {
    if (node == null) {
        str += "null,";
        return str;
    }
    str += String.valueOf(node.val + ",");
    str = preSerialize(node.left, str);
    str = preSerialize(node.right, str);
    return str;
}

// Decodes your encoded data to tree.
public TreeNode deserialize(String data) {
    String[] strArr = data.split(",");
    List<String> dataList = new LinkedList<>(Arrays.asList(strArr));
    return preDeserialize(dataList);
}

public TreeNode preDeserialize(List<String> dataList) {
    if (dataList.size() == 0) {
        return null;
    }
    if (dataList.get(0).equals("null")) {
        dataList.remove(0);
        return null;
    }
    TreeNode root = new TreeNode(Integer.valueOf(dataList.get(0)));
    dataList.remove(0);
    root.left = preDeserialize(dataList);
    root.right = preDeserialize(dataList);
    return root;
}
  • 层序遍历实现:
public String serialize(TreeNode root) {
    if (root == null) {
        return "";
    }
    StringBuilder sb = new StringBuilder("");
    Deque<TreeNode> queue = new LinkedList<>();
    queue.add(root);
    while (!queue.isEmpty()) {
        TreeNode node = queue.pollFirst();
        if (node != null) {
            sb.append(node.val).append(",");
            queue.offerLast(node.left);
            queue.offerLast(node.right);
        } else {
            sb.append("null,");
        }
    }
    sb.deleteCharAt(sb.length() - 1);
    return sb.toString();
}

public TreeNode deserialize(String data) {
    if (data == null || data.length() == 0) {
        return null;
    }
    String[] vals = data.split(",");
    TreeNode root = new TreeNode(Integer.parseInt(vals[0]));
    Deque<TreeNode> queue = new LinkedList<>();
    queue.offerLast(root);
    int idx = 1;
    while (!queue.isEmpty()) {
        TreeNode node = queue.pollFirst();
        if (!"null".equals(vals[idx])) {
            node.left = new TreeNode(Integer.parseInt(vals[idx]));
            queue.offerLast(node.left);
        }
        idx++;
        if (!"null".equals(vals[idx])) {
            node.right = new TreeNode(Integer.parseInt(vals[idx]));
            queue.offerLast(node.right);
        }
        idx++;
    }
    return root;
}

2. 动态规划:

动态规划特点:

  • 重叠子问题
  • 状态转移方程
  • 最优子结构

解题思路:

+ 明确状态
+ 明确选择
+ 明确dp数组的定义
+ 明确base case

--- 明确 base case -> 明确「状态」-> 明确「选择」 -> 定义 dp 数组/函数的含义

  1. 通过状态转移方程,暴力穷举所有可行解
  2. 通过备忘录消除重叠子问题,实现自顶向下的解法
  3. 进一步可以写出自底向上的解法
  4. 再进一步,可能可以优化空间复杂度

2-1 斐波那契数列:

/**
 * 递归解法
 *
 * @param n
 * @return
 */
public int fib(int n) {
    if (n == 0 || n == 1) {
        return n;
    }
    return fib(n - 1) + fib(n - 2);
}


/**
 * 带备忘录的解法
 * 自顶向下
 */
public int fib1(int n) {
    int[] memory = new int[n + 1];
    return helper(n, memory);
}

private int helper(int n, int[] memory) {
    if (n == 0 || n == 1) {
        return n;
    }
    if (memory[n] != 0) {
        return memory[n];
    }
    memory[n] = helper(n - 1, memory) + helper(n - 2, memory);
    return memory[n];
}


/**
 * dp数组的迭代解法
 * 自底向上
 */
public int fib2(int n) {
    if (n == 0) {
        return 0;
    }
    int[] dp = new int[n + 1];
    dp[0] = 0;
    dp[1] = 1;
    for (int i = 2; i <= n; i++) {
        dp[i] = dp[i - 1] + dp[i - 2];
    }
    return dp[n];
}

/**
 * 优化空间
 */
public int fib3(int n) {
    if (n == 0) {
        return 0;
    }
    int preVal = 0;
    int curVal = 1;
    int sum = 0;
    for (int i = 2; i <= n; i++) {
        sum = curVal+preVal;
        preVal = curVal;
        curVal = sum;
    }
    return curVal;
}

2-2 0-1背包问题:

public int maxValue(int[] weights, int[] values, int bag) {
    int n = weights.length;
    int[][] dp = new int[n + 1][bag + 1];
    for (int[] t : dp) {
        Arrays.fill(t, -1);
    }
    return process1(weights, values, 0, bag);
    //return process2(weights, values, 0, bag, dp);
}

/**
 * 递归版本
 *
 * @param weights
 * @param values
 * @param index
 * @param rest
 * @return
 */
public int process1(int[] weights, int[] values, int index, int rest) {
    if (index == weights.length) {
        return 0;
    }
    // p1 代表 不取当前index位置的物品   index之后的所有物品构成的最大价值
    int p1 = process1(weights, values, index + 1, rest);
    int p2 = -1;
    if (rest >= weights[index]) {
        // p2 代表取了当前index位置的物品  以及之后所有物品能构成的最大价值
        p2 = process1(weights, values, index + 1, rest - weights[index]);
        p2 += values[index];

    }
    return Math.max(p1, p2);
}

/**
 * 递归加记忆化搜索,实现中间结果缓存,减少递归调用次数
 *
 * @param weights
 * @param values
 * @param rest    背包剩余容量
 * @param index   物品下标 0 ~ n-1
 *                从当前物品开始以及之后的所有物品,多能构成的最大价值
 * @return
 */
public int process2(int[] weights, int[] values, int index, int rest, int[][] dp) {
    if (index == weights.length) {
        return 0;
    }
    // p1 代表 不取当前index位置的物品   index之后的所有物品构成的最大价值
    int p1;
    if (dp[index + 1][rest] != -1) {
        p1 = dp[index + 1][rest];
    } else {
        p1 = process2(weights, values, index + 1, rest, dp);
    }

    int p2 = -1;
    if (rest >= weights[index]) {
        // p2 代表取了当前index位置的物品  以及之后所有物品能构成的最大价值
        if (dp[index + 1][rest - weights[index]] != -1) {
            p2 = dp[index + 1][rest - weights[index]];
        } else {
            p2 = process2(weights, values, index + 1, rest - weights[index], dp);
            p2 += values[index];
        }
    }
    dp[index][rest] = Math.max(p1, p2);
    return dp[index][rest];
}


/**
 * 动态规划版本
 *
 * @param weights
 * @param values
 * @param bag
 * @return
 */
public int dpWay(int[] weights, int[] values, int bag) {
    int n = weights.length;
    int[][] dp = new int[n + 1][bag + 1];

    //边界值 赋值
    for (int index = n - 1; index >= 0; index--) {
        for (int rest = 0; rest <= bag; rest++) {
            dp[index][rest] = dp[index + 1][rest];
            if (rest - weights[index] >= 0) {
                dp[index][rest] = Math.max(dp[index + 1][rest - weights[index]] + values[index], dp[index][rest]);
            }
        }
    }
    return dp[0][bag];
}

2-3 凑零钱问题:

/**
 * 暴力递归解法
 * 状态: 目标金额
 * 选择: 所有硬币的面值
 * 函数的定义: 凑出总金额最少需要的硬币数
 * base cae amount == 0时,需要0枚硬币,amount < 0时,返回-1
 */
public int coinChange(int[] coins, int amount) {
    if (amount == 0) {
        return 0;
    }
    if (amount < 0) {
        return -1;
    }
    int res = Integer.MAX_VALUE;
    for (int coin : coins) {
        int subProblem = coinChange(coins, amount - coin);
        if (subProblem == -1) {
            continue;
        }
        res = Math.min(res, subProblem + 1);
    }
    return res == Integer.MAX_VALUE ? -1 : res;
}

/**
 * 带备忘录的解法
 * 自顶向下
 */
int[] memory;
public int coinChange1(int[] coins, int amount) {
    if (amount == 0) {
        return 0;
    }
    if (amount < 0) {
        return -1;
    }
    memory = new int[amount + 1];
    Arrays.fill(memory, -2);
    return helper(coins, amount);
}

private int helper(int[] coins, int amount) {
    if (amount == 0) {
        return 0;
    }
    if (amount < 0) {
        return -1;
    }
    if (memory[amount] != -2) {
        return memory[amount];
    }
    int res = Integer.MAX_VALUE;
    for (int coin : coins) {
        int subProblem = helper(coins, amount - coin);
        if (subProblem == -1) {
            continue;
        }
        res = Math.min(res, subProblem + 1);
    }
    memory[amount] = (res == Integer.MAX_VALUE ? -1 : res);
    return memory[amount];
}

/**
 * dp数组迭代
 * 自底向上
 */
public static int coinChange2(int[] coins, int amount) {
    if (amount == 0) {
        return 0;
    }
    if (amount < 0) {
        return -1;
    }
    int[] dp = new int[amount + 1];
    Arrays.fill(dp, amount + 1);
    dp[0] = 0;
    for (int i = 0; i < dp.length; i++) {
        for (int coin : coins) {
            if (i - coin < 0) {
                continue;
            }
            dp[i] = Math.min(dp[i], 1 + dp[i - coin]);
        }
    }
    return dp[amount] == amount + 1 ? -1 : dp[amount];
}

2-4 DP定义思路:

在使用动态规划解题时,我们需要通过dp数组来找状态转移关系。

  1. 一维dp数组:

    在子数组array[0..i]中,以array[i]结尾的目标子序列(最长递增子序列)的长度是dp[i]。

最后还需要遍历一次dp数组
  1. 二维dp数组:
+   涉及两个字符串时:在子数组arr1[0..i]和子数组arr2[0..j]中,我们要求的子序列(**最长公共子序列**)长度为dp[i] [j].

+   涉及一个字符串时:在子数组array[i..j]中,我们要求的子序列(**最长回文子序列**)的长度为dp[i] [j] 

2.5 N皇后问题:

递归版本:

从左到右遍历模式,n的n次方问题,优化只能在常数项 优化采用位运算 这里多了打印结果的步骤,所以要考虑状态还原的问题 判断有效位置,只需考虑是否在同一列,是否在对角线 对角线的判断通过坐标差值巧妙比较

public List<List<String>> res = new ArrayList<>();

public List<List<String>> solveNQueens(int n) {
    int[] record = new int[n];
    int[][] path = new int[n][n];
    func(n, record, 0, path);
    return res;
}

public void func(int n, int[] record, int row, int[][] path) {
    if (row == n) {
        List<String> t = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            StringBuilder sb = new StringBuilder("");
            for (int j = 0; j < n; j++) {
                if (path[i][j]==0){
                    sb.append(".");
                }else {
                    sb.append("Q");
                }
            }
            t.add(sb.toString());
        }
        res.add(t);
        return;
    }
    for (int col = 0; col < n; col++) {
        if (isValidPos(record, row, col)) {
            record[row] = col;
            path[row][col] = 1;
            func(n, record, row + 1, path);
            path[row][col] = 0;
        }
    }
}

public boolean isValidPos(int[] record, int row, int col) {
    for (int i = 0; i < row; i++) {
        if (col == record[i] || Math.abs(row - i) == Math.abs(col - record[i])) {
            return false;
        }
    }
    return true;
}

3. 排序算法:

本节排序算法动图展示来源于: https://www.toutiao.com/a6593273307280179715/?iid=6593273307280179715&wid=1618818218329

非常好的动图展示效果。

下面的总结图有问题。

image-20210424211011985

3.1 冒泡排序:

​ 冒泡排序之所以叫冒泡,就是它的过程就像水中气泡上浮一样,每一次遍历待排序的数组时通过交换操作将当前最大的数字移动到指定的位置,这样通过一次次的遍历后,最后整个数组就有序了。

冒泡排序是稳定排序,时间复杂度O(n^2)

15351158361064c65468df5
public static void bubbleSort(int[] array) {
    if (array == null || array.length < 2) {
        return;
    }
    for (int i = array.length - 1; i > 0; i--) {
        for (int j = 0; j < i; j++) {
            if (array[j] > array[j + 1]) {
                swap(array, j, j + 1);
            }
        }
    }
}

public static void swap(int[] array, int i, int j) {
    array[i] = array[i] ^ array[j];
    array[j] = array[i] ^ array[j];
    array[i] = array[i] ^ array[j];
}

3.2 选择排序:

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

注:

选择排序和冒泡排序的交换操作发生在不同时刻。

153511583598984ab75e4cd

public static void selectSort(int[] array) {
    if (array == null || array.length < 2) {
        return;
    }
    for (int i = 0; i < array.length - 1; i++) {
        int minIndex = i;
        for (int j = i + 1; j < array.length; j++) {
            minIndex = array[j] < array[minIndex] ? j : minIndex;
        }
        swap(array, i, minIndex);
    }
}

public static void swap(int[] array, int i, int j) {
    int temp = array[i];
    array[i] = array[j];
    array[j] = temp;
}

3.3 插入排序:

​ 在要排序的一组数中,假定前n-1个数已经排好序,现在将第n个数插到前面的有序数列中,使得这n个数也是排好顺序的。如此反复循环,直到全部排好顺序。

1535115835716c7df243d77

public static void insertSort(int[] array) {
    if (array == null || array.length < 2) {
        return;
    }
    for (int i = 1; i < array.length; i++) {
        for (int j = i - 1; j >= 0 && array[j] > array[j + 1]; j--) {
            swap(array, j, j + 1);
        }
    }
}
public static void swap(int[] array, int i, int j) {
    int temp = array[i];
    array[i] = array[j];
    array[j] = temp;
}

【总结】: 对于冒泡排序和选择排序来说,输入的数据不会影响算法的复杂度。但是插入排序会受影响,最好的情况下,时间复杂度为O(N)。

3.4 归并排序:

​ 归并排序采用分而治之的思想,具体思路为:

  • 分: 不断把待排序数组从中间位置划分,将整个数组的排序问题转换为子数组的排序问题。

    ​ 终止条件: 当 l≥r 时,代表子数组长度为 1 ,此时终止划分。

  • 治: 划分到子数组长度为 1 时,开始向上合并,不断将较短排序数组合并为较长排序数组,直至合并至原数组时完成排序。

1535115836355d738942667

//   版本一
static int[] nums, tmp;

public static void mergeSortEnter(int[] arr) {
    nums = arr;
    tmp = new int[nums.length];
    mergeSort(0, nums.length - 1);
}

public static void mergeSort(int l, int r) {
    // 终止条件
    if (l >= r) {
        return;
    }
    // 递归划分
    int m = (l + r) / 2;
    mergeSort(l, m);
    mergeSort(m + 1, r);
    // 合并阶段
    int i = l, j = m + 1;
    for (int k = l; k <= r; k++) {
        tmp[k] = nums[k];
    }
    for (int k = l; k <= r; k++) {
        if (i == m + 1) {
            nums[k] = tmp[j++];
        } else if (j == r + 1 || tmp[i] <= tmp[j]) {
            nums[k] = tmp[i++];
        } else {
            nums[k] = tmp[j++];
        }
    }
}


/*
 * 版本二
 */
public static void mergeSort2(int[] arr) {
    if (arr == null || arr.length < 2) {
        return;
    }
    int length = arr.length;
    process(arr, 0, length - 1);
}

public static void process(int[] arr, int L, int R) {
    if (L >= R) {
        return;
    }
    int mid = L + ((R - L) >> 1);
    process(arr, L, mid);
    process(arr, mid + 1, R);
    merge(arr, L, mid, R);
}
//将两个有序的片段合并在一起
public static void merge(int[] arr, int L, int mid, int R) {
    int[] helper = new int[R - L + 1];
    int index = 0;
    int p1 = L;
    int p2 = mid + 1;
    while (p1 <= mid && p2 <= R) {
        helper[index++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];
    }
    while (p1 <= mid) {
        helper[index++] = arr[p1++];
    }
    while (p2 <= R) {
        helper[index++] = arr[p2++];
    }
    for (int i = 0; i < helper.length; i++) {
        arr[L + i] = helper[i];
    }
}

【应用】

​ 求逆序对,或者针对一个数组,每一个数要统计比它大的或者小的

3.5 快速排序:

【思想】

​ 通过一趟排序将某一个元素放在正确的排序位置,左边的元素都是小于等于当前元素,右边的元素都是大于当前元素,然后对左右两边的元素再进行递归操作,每一次都能将一个元素放在正确的位置

【进阶】

​ 每一趟排序能够将等于某个值的元素全部放在正确的位置,这样可以在递归的时候减少需要排序的元素数量,不过复杂度和上面的方法是一样的

之所以能够达到O(n*log(n))的复杂度,关键在于每一趟要处理的那个元素是随机选择 而不是采用固定的策略

快速排序
public static void quickSort(int[] arr) {
    if (arr == null || arr.length < 2) {
        return;
    }
    process(arr, 0, arr.length - 1);
}

public static void process(int[] arr, int L, int R) {
    if (L >= R) {
        return;
    }
    int mid = partition(arr, L, R);
    process(arr, L, mid - 1);
    process(arr, mid + 1, R);
}

public static int partition(int[] arr, int L, int R) {
    if (L >= R) {
        return -1;
    }
    //随机选择元素  复杂度为O(nlog(n))
    swap(arr, L + (int) (Math.random() * (R - L + 1)), R);
    int less = L;
    int more = R;
    int cur = L;
    while (cur < more) {
        if (arr[cur] < arr[R]) {
            swap(arr, cur++, less++);
        } else if (arr[cur] == arr[R]) {
            cur++;
        } else {
            swap(arr, cur, --more);
        }
    }
    swap(arr, cur, R);
    return cur;
}

public static void swap(int[] arr, int i, int j) {
    int temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}

3.6 堆排序:

基于堆结构实现的堆排序,首先将数据中的元素一次加入堆中,然后 一次从堆中弹出元素。

值得收藏的十大经典排序算法
 public static void headSort(int[] arr){
        MaxPriorityQueue heap = new MaxPriorityQueue(arr.length);
        for(int i=0;i<arr.length;i++){
            heap.insert(arr[i]);
        }
        int index = 0;
        while(!heap.isEmpty()){
            arr[index++] = heap.delMax();
        }
    }

大顶堆为例: 参考 堆结构

3.7 计数排序:

【基本思想】:

​ 计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数

【算法描述】:

找出待排序的数组中最大和最小的元素;

统计数组中每个值为i的元素出现的次数,存入数组C的第i项;

对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);

反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1。

值得收藏的十大经典排序算法

3.8 基数排序:

4. 二叉搜索树(BST):

【性质】

1. 二叉搜索树的中序遍历是一个升序列表。那它的逆过程就是一个降序列表。
// 这段代码可以升序打印节点的值
void traverse(TreeNode root) {
    if (root == null) {
        return;
    }
    traverse(root.left);   //先遍历左子树
    // 中序遍历代码位置
    print(root.val);
    traverse(root.right);
}

//这段代码可以降序打印节点的值
void traverse(TreeNode root) {
    if (root == null) {
        return;
    }
    // 先递归遍历右子树
    traverse(root.right);  // 先遍历右子树
    // 中序遍历代码位置
    print(root.val);
    // 后递归遍历左子树
    traverse(root.left);
}

4.1 查找结点:

​ 对于BST左小右大的性质,我们在查找节点的时候可以用上。就可以不同递归的搜索两边,可以采用二分查找的思想,每次排除一遍,这样可以提升查找效率。

boolean isInBST(TreeNode root, int target) {
    if (root == null){
        return false;
    }
    if (root.val == target){
        return true;
    }
    //目标值比当前值大,查找右子树
    if (root.val < target) {
        return isInBST(root.right, target);
    }
    //目标值比当前值小,查找左子树
    if (root.val > target){
        return isInBST(root.left, target);
    }
}

4.2 插入节点:

​ 插入一个节点就是先找到该节点要插入的位置,然后插入节点。由于插入节点需要将新节点链接到原来的树上,所以方法需要返回TreeNode类型。

TreeNode insertIntoBST(TreeNode root, int val) {
    // 找到空位置插入新节点
    if (root == null){
        return new TreeNode(val);
    }
    if (root.val < val) {
        root.right = insertIntoBST(root.right, val);
    }
    if (root.val > val) {
        root.left = insertIntoBST(root.left, val);
    }
    return root;
}

4.3 删除节点:

​ 对于删除的操作,也是需要先查到节点,然后做删除操作,但是存在一个问题就是删除后不能破坏BST的性质,所以就需要针对如下几种情况做进一步分析:

    + 该节点为叶子节点,左右子树均为空,则可以直接删除
    + 该节点只有一个非空子节点,则让子节点代替该节点的位置
    + 该节点有两个非空子节点,就是说左右子树都不为空,那么为了不破坏BST的性质,我们就需要<font color=red>**找左子树最大的节点或者右子树中最小的节点来代替当前节点。**</font>
TreeNode deleteNode(TreeNode root, int key) {
    if (root == null) {
        return null;
    }
    if (root.val == key) {
        // 这两个 if 把情况 1 和 2 都正确处理了
        if (root.left == null) {
            return root.right;
        }
        if (root.right == null) {
            return root.left;
        }
        // 处理情况 3
        TreeNode minNode = getMin(root.right);
        root.val = minNode.val;
        root.right = deleteNode(root.right, minNode.val);
    } else if (root.val > key) {
        root.left = deleteNode(root.left, key);
    } else if (root.val < key) {
        root.right = deleteNode(root.right, key);
    }
    return root;
}

TreeNode getMin(TreeNode node) {
    while (node.left != null){
        node = node.left;
    }
    return node;
}

4.4 判断合法性:

根据BST的性质,对于每个节点来说,它的值要比左子树中所有节点的值都要大,同时还要比右子树中所有节点的值都要小。所以我们在递归的过程中需要给函数提供当前要判断的节点的值所在的范围。

public boolean isValidBST(TreeNode root) {
    return validate(root, Long.MIN_VALUE, Long.MAX_VALUE);
}

public boolean validate(TreeNode node, long min, long max) {
    if (node == null) {
        return true;
    }
    if (node.val <= min || node.val >= max) {
        return false;
    }
    return validate(node.left, min, node.val) && validate(node.right, node.val, max);
}

5. 字典树[前缀树]:

​ 一种树形数据结构,用于高效地存储和检索字符串数据集中的键。这一数据结构有相当多的应用情景,例如自动补完和拼写检查。

前缀树不仅可以统计字符串出现的次数,更重要的在于可以快速计算出有多少个字符串中包含指定的字符前缀。

基本数据结构的定义:

public class Node {
    /**
     * 拿单词构成的字典树举例来说,因为单词由字母构成,一共26个字母,每个节点需要开辟一个26个长度大小的数组,
     * 用来存储子节点的信息;另外一个字段决定当前是否遍历到字典树结束。
     * pass : 代表所有字符串中通过当前节点的数量
     * end :  代表所有字符串中以当前节点结尾的字符串的数量
     */
    public int pass;
    public int end;
    public Node[] next;

    public Node() {
        this.pass = 0;
        this.end = 0;
        this.next = new Node[26];
    }
}

前缀树代码示例:

class Trie{
    
  private Node root = new Node();

  /**
   * 插入一个单词
   */
  public void insert(String word) {
      if (word == null) {
          return;
      }
      char[] str = word.toCharArray();
      Node node = root;
      int index = 0;
      node.pass++;
      for (char c : str) {
          index = c - 'a';
          if (node.next[index] == null) {
              node.next[index] = new Node();
          }
          node = node.next[index];
          node.pass++;
      }
      node.end++;
  }

  /**
   * 查询指定单词出现的次数
   */
  public int search(String word) {
      if (word == null) {
          return 0;
      }
      Node node = root;
      char[] str = word.toCharArray();
      int index = 0;
      for (char c : str) {
          index = c - 'a';
          if (node.next[index] == null) {
              return 0;
          }
          node = node.next[index];
      }
      return node.end;
  }


  public int searchPrefix(String prefix) {
      if (prefix == null) {
          return 0;
      }
      Node node = root;
      char[] str = prefix.toCharArray();
      int index = 0;
      for (char c : str) {
          index = c - 'a';
          if (node.next[index] == null) {
              return 0;
          }
          node = node.next[index];
      }
      return node.pass;
  }

  public void delete(String word) {
      if (search(word) != 0) {
          Node node = root;
          node.pass--;
          char[] str = word.toCharArray();
          int index = 0;
          for (char c : str) {
              index = c - 'a';
              if (--node.next[index].pass == 0) {
                  node.next[index] = null;
                  return;
              }
              node = node.next[index];
          }
          node.end--;
      }
  }
}

6. 堆结构

​ 堆结构也是一个完全二叉树

​ 二叉堆(Binary Heap)其主要操作就两个,sink(下沉)和swim(上浮),用以维护二叉堆的性质。其主要应用有两个,首先是一种排序方法「堆排序」,第二是一种很有用的数据结构「优先级队列」。

优先级队列这种数据结构有一个很有用的功能,你插入或者删除元素的时候,元素会自动排序,这底层的原理就是二叉堆的操作。

对于最大堆,会破坏堆性质的有两种情况:

  1. 如果某个节点 A 比它的子节点(中的一个)小,那么 A 就不配做父节点,应该下去,下面那个更大的节点上来做父节点,这就是对 A 进行下沉
  2. 如果某个节点 A 比它的父节点大,那么 A 不应该做子节点,应该把父节点换下来,自己去做父节点,这就是对 A 的上浮
/**
 * 大顶堆
 */
public class MaxPriorityQueue {

    private int[] queue;
    private int N = 0;

    private MaxPriorityQueue(int cap) {
        //索引0不用,所以多一个位置,方便计算父子节点位置
        queue = new int[cap + 1];
    }
    //返回当前队列的最大元素
    private int max() {
        return queue[1];
    }
    //父节点索引位置
    private int parent(int node) {
        return node / 2;
    }
    //左子节点索引位置
    private int left(int node) {
        return node * 2;
    }
    //右子节点索引位置
    private int right(int node) {
        return node * 2 + 1;
    }

    /**
     * 插入一个元素的操作,先将元素置为末尾,然后执行上浮操作
     */
    public void insert(int val) {
        N++;
        queue[N] = val;
        swim(N);
    }

    /**
     * 删除堆顶元素,将数组末尾元素放在堆顶,然后执行下沉操作
     */
    public int delMax() {
        int max = queue[1];
        exch(1, N);
        //设置无效
        queue[N] = Integer.MAX_VALUE;
        N--;
        sink(1);
        return max;
    }

    /**
     * 上浮第k个元素
     * 不断比较子节点和父节点的大小,如果父节点小于子节点就相互交换
     */
    private void swim(int k) {
        while (k > 1 && less(parent(k), k)) {
            exch(parent(k), k);
            k = parent(k);
        }
    }

    /**
     * 下沉第k个元素
     * 循环条件索引位置没有超过数组末尾
     * 1. 先比较左右子节点位置的元素,找出较大的元素
     * 2. 如果较大的元素没有位置k的元素大,则下沉操作结束
     * 3. 如果较大的元素比位置k的元素大,则交换两个位置的元素,k指向交换后的位置,继续进行下沉操作
     */
    private void sink(int k) {
        while (left(k) <= N) {
            int older = left(k);
            if (right(k) <= N && less(older, right(k))) {
                older = right(k);
            }
            if (less(older, k)) {
                break;
            }
            exch(older, k);
            k = older;
        }
    }

    private void exch(int i, int j) {
        int tmp = queue[i];
        queue[i] = queue[j];
        queue[j] = tmp;
    }

    private boolean less(int i, int j) {
        return queue[i] < queue[j];
    }
}

7.并查集:

public static class Node<V> {
    V value;

    public Node(V v) {
        value = v;
    }
}

public static class UnionSet<V> {
    public Map<V, Node<V>> nodes;
    public Map<Node<V>, Node<V>> parents;
    public Map<Node<V>, Integer> sizeMap;

    public UnionSet(List<V> values) {
        this.nodes = new HashMap<>();
        this.parents = new HashMap<>();
        this.sizeMap = new HashMap<>();
        for (V v : values) {
            Node<V> node = new Node<>(v);
            nodes.put(v, node);
            parents.put(node, node);
            sizeMap.put(node, 1);
        }
    }
        
    public Node<V> findFather(Node<V> cur) {
        Stack<Node<V>> path = new Stack<>();
        while (cur != parents.get(cur)) {
            path.push(cur);
            cur = parents.get(cur);
        }
        //扁平化处理 优化时间复杂度,findFather方法调用越频繁,时间复杂度越接近O(1)
        while (!path.isEmpty()) {
            parents.put(path.pop(), cur);
        }
        return cur;
    }


    //查询两个节点是否是同一个集合
    public boolean isSameSet(V a, V b) {
        if (!nodes.containsKey(a) || !nodes.containsKey(b)) {
            return false;
        }
        return findFather(nodes.get(a)) == findFather(nodes.get(b));
    }

    //合并两个节点所在的集合
    public void union(V a, V b) {
        if (!nodes.containsKey(a) || !nodes.containsKey(b)) {
            return;
        }
        Node<V> aHead = findFather(nodes.get(a));
        Node<V> bHead = findFather(nodes.get(b));
        if (aHead != bHead) {
            int aSetSize = sizeMap.get(aHead);
            int bSetSize = sizeMap.get(bHead);
            Node<V> big = aSetSize >= bSetSize ? aHead : bHead;
            Node<V> small = big == aHead ? bHead : aHead;
            parents.put(small, big);
            sizeMap.put(big, aSetSize + bSetSize);
            sizeMap.remove(small);
        }
    }
}

8. 图:

图是由边和顶点组成

// 图的表示
public class Graph {
    public HashMap<Integer, Node> nodes;
    public HashSet<Edge> edges;

    public Graph() {
        this.nodes = new HashMap<>();
        this.edges = new HashSet<>();
    }
}
//节点有下标、入度、出度、指向的邻居节点、相关联的边
public class Node {
    public int value;
    public int in;
    public int out;
    public List<Node> nexts;
    public List<Edge> edges;

    public Node(int value) {
        this.value = value;
        this.in = 0;
        this.out = 0;
        this.nexts = new ArrayList<>();
        this.edges = new ArrayList<>();
    }
}

//有向边,边有权重,同时记录边的起点和终点
public class Edge {
    public int weight;
    public Node from;
    public Node to;

    public Edge(int weight, Node from, Node to) {
        this.weight = weight;
        this.from = from;
        this.to = to;
    }
}

8.1 图的遍历:

广度优先遍历:

public void bfs(Node node) {
    if (node == null) {
        return;
    }
    Queue<Node> queue = new LinkedList<>();
    HashSet<Node> set = new HashSet<>();
    queue.add(node);
    set.add(node);
    while (!queue.isEmpty()) {
        Node cur = queue.poll();
        System.out.println(cur.value);
        for (Node n : cur.nexts) {
            if (!set.contains(n)) {
                queue.add(n);
                set.add(n);
            }
        }
    }
}

深度优先遍历:

public void dfs(Node node) {
    if (node == null) {
        return;
    }
    Stack<Node> st = new Stack<>();
    Set<Node> set = new HashSet<>();
    st.push(node);
    set.add(node);
    System.out.println(node.value);
    while (!st.isEmpty()) {
        Node cur = st.pop();
        for (Node n : node.nexts) {
            if (!set.contains(n)) {
                st.push(cur);
                st.push(n);
                set.add(n);
                System.out.println(n.value);
                break;
            }
        }
    }
}

拓扑排序:

public List<Node> topology(Graph graph) {
    List<Node> result = new ArrayList<>();
    Map<Node, Integer> inMap = new HashMap<>();
    Queue<Node> zeroInQueue = new LinkedList<>();
    for(Node node : graph.nodes.values()){
        inMap.put(node,node.in);
        if(node.in == 0){
            zeroInQueue.offer(node);
        }
    }
    while(!zeroInQueue.isEmpty()){
        Node cur = zeroInQueue.poll();
        result.add(cur);
        for(Node next : cur.nexts){
            inMap.put(next,inMap.get(next)-1);
            if(inMap.get(next) == 0){
                zeroInQueue.offer(next);
            }
        }
    }
    return result;
}

8.2 最小生成树:

K算法:

//每次选择权重最小的边,直到所有的节点都在同一个连通图中,用并查集
public static class UnionFind {
    private Map<Node, Node> fatherMap;
    private Map<Node, Integer> sizeMap;

    public UnionFind() {
        fatherMap = new HashMap<>();
        sizeMap = new HashMap<>();
    }

    public void makeSets(Collection<Node> nodes) {
        fatherMap.clear();
        sizeMap.clear();
        for (Node node : nodes) {
            fatherMap.put(node, node);
            sizeMap.put(node, 1);
        }
    }

    public Node findFather(Node node) {
        Stack<Node> path = new Stack<>();
        while (node != fatherMap.get(node)) {
            path.push(node);
            node = fatherMap.get(node);
        }
        while (!path.isEmpty()) {
            fatherMap.put(path.pop(), node);
        }
        return node;
    }

    public boolean isSameSet(Node a, Node b) {
        return findFather(a) == findFather(b);
    }

    public void union(Node a, Node b) {
        if (a == null || b == null) {
            return;
        }
        Node aHead = fatherMap.get(a);
        Node bHead = fatherMap.get(b);
        if (aHead != bHead) {
            int aSetSize = sizeMap.get(aHead);
            int bSetSize = sizeMap.get(bHead);
            Node big = aSetSize >= bSetSize ? aHead : bHead;
            Node small = big == aHead ? bHead : aHead;
            fatherMap.put(small, big);
            sizeMap.put(big, aSetSize + bSetSize);
            sizeMap.remove(small);
        }
    }
}

public Set<Edge> kMST(Graph graph) {
    UnionFind unionFind = new UnionFind();
    unionFind.makeSets(graph.nodes.values());
    PriorityQueue<Edge> edgeQueue = new PriorityQueue<>(new EdgeComparator());
    for (Edge edge : graph.edges) {
        edgeQueue.add(edge);
    }
    Set<Edge> result = new HashSet<>();
    while (!edgeQueue.isEmpty()) {
        Edge edge = edgeQueue.poll();
        if (!unionFind.isSameSet(edge.from, edge.to)) {
            result.add(edge);
            unionFind.union(edge.from, edge.to);
        }
    }
    return result;
}


public static class EdgeComparator implements Comparator<Edge> {
    @Override
    public int compare(Edge o1, Edge o2) {
        return o1.weight - o2.weight;
    }
}

P算法:

public static class EdgeComparator implements Comparator<Edge> {
    @Override
    public int compare(Edge o1, Edge o2) {
        return o1.weight - o2.weight;
    }
}

/**
 * 从一个点开始逐步扩散,每次选择关联边中权重最小的
 * @param graph
 * @return
 */
public Set<Edge> primMST(Graph graph) {
    Set<Edge> result = new HashSet<>();
    PriorityQueue<Edge> edgeQueue = new PriorityQueue<>(new EdgeComparator());

    //记录走过的点和边
    Set<Node> nodeSet = new HashSet<>();
    Set<Edge> edgeSet = new HashSet<>();

    for (Node node : graph.nodes.values()) {  //for循环是为了防止森林
        if (!nodeSet.contains(node)) {
            nodeSet.add(node);
            for (Edge edge : node.edges) {
                if (!edgeSet.contains(edge)) {
                    edgeSet.add(edge);
                    edgeQueue.add(edge);
                }
            }
            while (!edgeQueue.isEmpty()) {
                Edge edge = edgeQueue.poll();
                Node toNode = edge.to;
                if (!nodeSet.contains(toNode)) {
                    nodeSet.add(toNode);
                    result.add(edge);
                    for (Edge nextEdge : toNode.edges) {
                        if (!edgeSet.contains(nextEdge)) {
                            edgeSet.add(nextEdge);
                            edgeQueue.add(nextEdge);
                        }
                    }
                }
            }
        }
    }
    return result;
}

二、经典问题:

1. 超级水王问题:

问题描述:

寻找一个数组中出现次数超过数组长度一半的数?

解题思路:

    1. 每次删除数组中两个不同的数,如果数组中存在水王数,则最后一定会剩下,如果没有剩下,则数据组中不存在水王数,但是剩下的不一定是水王数。
    2. 判断剩下的数字是否是水王数,再遍历一次数组,统计该数字出现的次数。

如何优雅的一次删除两个数? 定义两个变量,候选、血量。

【规定】:

  • 血量==0,无候选
  • 血量>0,有候选

【操作】: 遍历一次数组

  • 如果无侯选,则立当前数为候选,血量=1

  • 如果有候选,

    • 当前数等于候选, 血量+1
      • 当前数不等于候选,血量-1

    最终,如果血量大于0,则得到删除操作完成后的剩余数字,如果血量等于0,则没有水王数。

public static int waterKing(int[] array) {
    if (array == null || array.length == 0) {
        return -1;
    }
    int candidate = 0;
    int restHp = 0;
    for (int current : array) {
        if (restHp == 0) {
            candidate = current;
            restHp = 1;
        } else if (current != candidate) {
            restHp--;
        } else {
            restHp++;
        }
    }

    if (restHp == 0) {
        return -1;
    }
    int count = 0;
    for (int num : array) {
        if (num == candidate) {
            count++;
        }
    }
    return count > (array.length >> 1) ? candidate : -1;
}

2. KMP算法:

3. 二分查找:

3.1 基本的二分查找:

【细节】

  • 计算 mid 时需要防止溢出,代码中 left + (right - left) / 2 就和 (left + right) / 2 的结果相同,但是有效防止了 leftright 太大直接相加导致溢出
public int binarySearch(int[] nums, int target) {
    int left = 0;
    int right = nums.length - 1; // 注意
    /*
     * 注意循环条件的设置:  <= 或者 <
     * 因为初始化 right 的赋值是 nums.length - 1,即最后一个元素的索引,而不是 nums.length。
     * 这二者可能出现在不同功能的二分查找中,区别是:前者相当于两端都闭区间 [left, right],后者相当于左闭右开区间 [left,      * right),因为索引大小为 nums.length 是越界的
     */
    while (left <= right) {
        int mid = left + ((right - left) >> 1);
        if (nums[mid] == target) {
            return mid;
        } else if (nums[mid] < target) {
            left = mid + 1; // 注意
        } else if (nums[mid] > target) {
            right = mid - 1; // 注意
        }
    }
    return -1;
}

3.2 寻找左侧边界:

思路: 如果当前位置的值大于等于目标值,则保存当前位置下标,在左侧继续寻找,否则在右侧寻找,直到没有数字可以搜索。

public int leftIndex(int[] nums, int target) {
    int left = 0;
    int right = nums.length - 1;
    int index = -1;
    while (left <= right) {
        int mid = left + ((right - left) >> 1);
        if (nums[mid] >= target) {
            index = mid;
            right = mid - 1;
        } else {
            left = mid + 1;
        }
    }
    return index;
}

3.3 寻找右侧边界:

public int rightIndex(int[] nums, int target) {
    int left = 0;
    int right = nums.length - 1;
    int index = -1;
    while (left <= right) {
        int mid = left + ((right - left) >> 1);
        if (nums[mid] <= target) {
            index = mid;
            left = mid + 1;
        } else {
            right = mid - 1;
        }
    }
    return index;
}

3.4 将上述三种应用场景用同一种写法实现:

/**
 *  查找目标值下标
 *  查找目标值左侧边界
 *  查找目标值右侧边界
 * 统一方法: 区间是闭区间 while循环就是<=
 */

public int search(int[] nums, int target) {
    int left = 0;
    int right = nums.length - 1;
    while (left <= right) {
        int mid = left + ((right - left) >> 1);
        if (nums[mid] == target) {
            return mid;
        } else if (nums[mid] > target) {
            right = mid - 1;
        } else {
            left = mid + 1;
        }
    }
    return -1;
}


public int searchLeft(int[] nums, int target) {
    int left = 0;
    int right = nums.length - 1;
    while (left <= right) {
        int mid = left + ((right - left) >> 1);
        if (nums[mid] == target) {
            //更新区间
            right = mid - 1;
        } else if (nums[mid] > target) {
            right = mid - 1;
        } else {
            left = mid + 1;
        }
    }
    //左边界  检查越界
    if (left >= nums.length || nums[left] != target) {
        return -1;
    }
    return left;
}


public int searchRight(int[] nums, int target) {
    int left = 0;
    int right = nums.length - 1;
    while (left <= right) {
        int mid = left + ((right - left) >> 1);
        if (nums[mid] == target) {
            //更新区间
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid - 1;
        } else {
            left = mid + 1;
        }
    }
    // 右边界 检查越界
    if (right < 0 || nums[right] != target) {
        return -1;
    }
    return right;
}

3.4.1 寻找左右边界

【34】 给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。

如果数组中不存在目标值 target,返回 [-1, -1]。

public int[] searchRange(int[] nums, int target) {
    int[] res = new int[]{-1, -1};
    if(nums == null || nums.length<1){
        return res;
    }
    res[0] = leftBounds(nums, target);
    res[1] = rightBounds(nums, target);
    return res;
}

public int leftBounds(int[] nums, int target) {
    int left = 0;
    int right = nums.length - 1;
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] >= target) {
            right = mid - 1;
        } else {
            left = mid + 1;
        }
    }
    if(left >=nums.length || nums[left]!=target){
        return -1;
    }
    return left;
}

public int rightBounds(int[] nums, int target) {
    int left = 0;
    int right = nums.length - 1;
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] <= target) {
            left = mid + 1;
        } else {
            right = mid - 1;
        }
    }
    if(right <0 || nums[right]!=target){
        return -1;
    }
    return right;
}

3.5 局部最小值:

定义:

  • 下标0位置的数比1位置的数小,0位置就是局部最小
  • 下标n位置的数比n-1位置的数小,n位置的数就是局部最小
  • 下标i位置的数比i-1和i+1位置的数都小,i位置的数就是局部最小

问题:

​ 给定一个无序数组任意两个相邻的数都不相等,返回数组中的一个局部最小值即可。

思路:

​ 根据定义判断,如果两端没有局部最小值,那么数组中间一定至少存在一个局部最小值。(开始递减,结尾递增,中间一定有转折点)

public int getLessIndex(int[] nums) {
    if (nums == null || nums.length == 0) {
        return -1;
    }
    if (nums.length == 1 || nums[0] < nums[1]) {
        return 0;
    }
    if (nums[nums.length - 1] < nums[nums.length - 2]) {
        return nums.length - 1;
    }
    int left = 1;
    int right = nums.length - 2;
    int mid = 0;
    while (left < right) {
        mid = (left + right) / 2;
        if (nums[mid] > nums[mid + 1]) {
            left = mid + 1;
        } else if (nums[mid] > nums[mid - 1]) {
            right = mid - 1;
        } else {
            return mid;
        }
    }
    return left;
}

4 . 滑动窗口:

问题描述:

给定一个数组 nums 和滑动窗口的大小 k,请找出所有滑动窗口里的最大值。

解题思路:

维护一个双端队列,这个队列保存依次遍历的下标值。入队列的规则是比队尾小的数可以入队,比队尾大的数,将队列末尾所有小的数都清除直到遇到比这个数大的数位置。每到一个窗口,都从队头读取元素,如果元素已经超出窗口范围,就在看下一个元素。

public int[] maxSlidingWindow(int[] nums, int k) {
   if (nums == null || k < 1 || nums.length < k) {
        return new int[0];
    }
    LinkedList<Integer> qmax = new LinkedList<Integer>();
    int[] res = new int[nums.length - k + 1];
    int index = 0;
    for (int i = 0; i < nums.length; i++) {
        while (!qmax.isEmpty() && nums[qmax.peekLast()] <= nums[i]) {
            qmax.pollLast();
        }
        qmax.addLast(i);
        if (qmax.peekFirst() == i - k) {
            qmax.pollFirst();
        }
        if (i >= k - 1) {
            res[index++] = nums[qmax.peekFirst()];
        }
    }
    return res;
}

5. 汉明距离:

问题描述:

两个整数之间的汉明距离指的是这两个数字对应二进制位不同的位置的数目。给出两个整数 xy,计算它们之间的汉明距离。

解题思路:

我们要计算的汉明距离也就是将两个数进行异或之后,结果对应的二进制数中的1的个数。

  1. java内置位计数器,统计二进制中1的个数

    public int hammingDistance1(int x, int y) {
       return Integer.bitCount(x ^ y);
    }
  2. 手动实现统计1的个数

    public int hammingDistance(int x, int y) {
       int s = x ^ y;
       int count = 0;
       while (s != 0) {
          count += s & 1;
          s >>= 1;
       }
       return count;
    }
  3. Brian kernighan算法

    该算法是对方法二的优化,在方法二中我们每次右移一位,判断是否为1,如果一个二进制串中存在连续的几个0,那我们就想能不能通过优化,跳过这些0。采用的方式就是将x与x-1异或,刚好可以去掉最右侧的1,如此循环,直到x=0

    image-20210527084155095

    public int hammingDistance(int x, int y) {
        int s = x ^ y;
        int count = 0;
        while (s != 0) {
            s &= s - 1;
            count++;
        }
        return count;
    }

6. 数据流中位数

设计一个支持以下两种操作的数据结构:

​ void addNum(int num) - 从数据流中添加一个整数到数据结构中。 ​ double findMedian() - 返回目前所有元素的中位数。

class MedianFinder{
    Queue<Integer> A, B;
    public MedianFinder() {
        A = new PriorityQueue<>(); // 小顶堆,保存较大的一半
        B = new PriorityQueue<>((x, y) -> (y - x)); // 大顶堆,保存较小的一半
    }
    public void addNum(int num) {
        if(A.size() != B.size()) {
            A.add(num);
            B.add(A.poll());
        } else {
            B.add(num);
            A.add(B.poll());
        }
    }
    public double findMedian() {
        return A.size() != B.size() ? A.peek() : (A.peek() + B.peek()) / 2.0;
    }
}

7. LRU缓存算法实现:

【146】:

数据结构: 哈希表+双向链表 Java中的LinkedHashMap

//自定义数据结构
class LRUCache {
    private Map<Integer,Node> cache;
    private int cap;
    Node head;
    Node tail;

    public LRUCache(int capacity) {
        this.cache = new HashMap<>();
        this.cap = capacity;
        this.head = new Node();
        this.tail = new Node();
        head.next = tail;
        tail.prev = head;
    }
    
    public int get(int key) {
        if(cache.containsKey(key)){
            moveToHead(cache.get(key));
            return cache.get(key).val;
        }
        return -1;
    }
    
    public void put(int key, int value) {
        if(cache.containsKey(key)){
            Node node = cache.get(key);
            node.val = value;
            moveToHead(node);
        }else{
            Node node = new Node(key,value);
            if(cache.size() >= cap){
                Node removeNode = tail.prev;
                removeNode(removeNode);
                cache.remove(removeNode.key);
            }
            cache.put(key,node);
            addNode(node);
        }
    }

    public void removeNode(Node node){
        System.out.println(node.val);
        node.prev.next = node.next;
        node.next.prev = node.prev;
    }

    public void addNode(Node node){
        node.next = head.next;
        node.prev = head;
        head.next.prev = node;
        head.next = node;
    }

    public void moveToHead(Node node){
        removeNode(node);
        addNode(node);
    }
}
class Node{
    public int key;
    public int val;
    public Node next;
    public Node prev;

    public Node(){}

    public Node(int key,int val){
        this.key = key;
        this.val = val;
    }
}

//基于Java内置的数据结构实现
class LRUCache {
    private LinkedHashMap<Integer, Integer> linkMap;
    int cap;

    public LRUCache(int capacity) {
        this.linkMap = new LinkedHashMap<>(capacity);
        this.cap = capacity;
    }

    public int get(int key) {
        if (linkMap.containsKey(key)) {
            makeRecent(key);
            return linkMap.get(key);
        }
        return -1;
    }

    public void put(int key, int value) {
        if (this.linkMap.containsKey(key)) {
            this.linkMap.put(key, value);
            makeRecent(key);
            return;
        }
        if (this.linkMap.size() >= this.cap) {
            this.linkMap.remove(this.linkMap.keySet().iterator().next());
        }
        this.linkMap.put(key, value);
    }

    public void makeRecent(int key) {
        int val = this.linkMap.get(key);
        this.linkMap.remove(key);
        this.linkMap.put(key, val);
    }
}

8. 前缀和问题:

【力扣 剑指offer专项版 010 011】

求和为k的连续子数组个数:

​ 假设当前位置为i,hash[i] 代表前i个数字中,和为sum出现了多少次。

public int subarraySum(int[] nums, int k) {
    Map<Integer,Integer> map = new HashMap<>();
    int count = 0;
    int preSum = 0;
    map.put(0,1);
    for(int num : nums){
        preSum += num;
        if(map.containsKey(preSum-k)){
            count += map.get(preSum - k);
        }
        map.put(preSum,map.getOrDefault(preSum,0)+1);
    }
    return count;
}

9. 接雨水问题:

力扣【42】:

public int trap(int[] height) {
    int n = height.length;
    int[] leftMax = new int[n];
    int[] rightMax = new int[n];
    if(n<=2){
        return 0;
    }
    leftMax[0] = height[0];
    rightMax[n-1] = height[n-1];
    for(int i=1;i<n;i++){
        leftMax[i] = Math.max(leftMax[i-1],height[i]);
    }
    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;
}

10. 最大矩形面积:

10.1 直方图中最大矩形面积:

【84】

单调栈递增

public int largestRectangleArea(int[] heights) {
    int maxArea = 0;
    int n = heights.length;
    Deque<Integer> q = new LinkedList<>();
    q.offerLast(-1);
    for(int i=0;i<n;i++){
        while(q.peekLast() != -1 && heights[i] < heights[q.peekLast()]){
            int h = q.pollLast();
            int j = q.peekLast();
            maxArea = Math.max(maxArea,heights[h]*(i-j-1));
        }
        q.offerLast(i);
    }
    while(q.peekLast()!=-1){
        int h = q.pollLast();
        int j  = q.peekLast();
        maxArea = Math.max(maxArea,heights[h]*(n-j-1));
    }
    return maxArea;
}

10.2 最大矩形

【85】

单调栈

将输入拆分成一系列的柱状图。为了计算矩形的最大面积,我们只需要计算每个柱状图中的最大面积,并找到全局最大值

public int maximalRectangle(char[][] matrix) {
    int maxArea = 0;
    int m = matrix.length;
    int n = matrix[0].length;
    int[] heights = new int[n];
    for(int i=0;i<m;i++){
        for(int j=0;j<n;j++){
            char c = matrix[i][j];
            if(c == '0'){
                heights[j] = 0;
            }else{
                heights[j] += c -'0';
            }
        }
        maxArea = Math.max(maxArea,calMaxArea(heights,n));
    }
    return maxArea;
}

public int calMaxArea(int[] heights,int n){
    int area = 0;
    Deque<Integer> q = new LinkedList<>();
    q.offerLast(-1);
    for(int i=0;i<n;i++){
        while(q.peekLast()!=-1 && heights[i]<heights[q.peekLast()]){
            int h = q.pollLast();
            int j = q.peekLast();
            area = Math.max(area,heights[h]*(i-j-1));
        }
        q.offerLast(i);
    }
    while(q.peekLast()!=-1){
        int h = q.pollLast();
        int j = q.peekLast();
        area = Math.max(area,heights[h]*(n-j-1));
    }
    return area;
}

10.3 最大正方形:

【221】

\(dp[i][j]\) 代表以(i,j)点为右下角,且只包含1的正方形的边长最大值

public int maximalSquare(char[][] matrix) {
    int maxSide = 0;
    if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
        return maxSide;
    }
    int rows = matrix.length, columns = matrix[0].length;
    int[][] dp = new int[rows][columns];
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < columns; j++) {
            if (matrix[i][j] == '1') {
                if (i == 0 || j == 0) {
                    dp[i][j] = 1;
                } else {
                    dp[i][j] = Math.min(Math.min(dp[i - 1][j], dp[i][j - 1]), dp[i - 1][j - 1]) + 1;
                }
                maxSide = Math.max(maxSide, dp[i][j]);
            }
        }
    }
    int maxSquare = maxSide * maxSide;
    return maxSquare;
}

11. 博弈问题:

【877】 石子游戏:

//递归版本,清晰明了
public boolean stoneGame1(int[] piles) {
    int n = piles.length;
    return f(piles,0,n-1) > s(piles,0,n-1);
}
//f方法求解的是,在[left,right]区间,当前玩家为先手的情况下能得到的最大值
public int f(int[] piles,int left,int right){
    if(left == right){
        return piles[left];
    }
    return Math.max(piles[left]+s(piles,left+1,right),piles[right]+s(piles,left,right-1));
}
//s方法求解的是,在[left,right]区间,当前玩家为后手的情况下能得到的最大值
public int s(int[] piles,int left,int right){
    if(left == right){
        return 0;
    }
    return Math.min(f(piles,left+1,right),f(piles,left,right-1));
}
//递归思路+记忆化搜索
public boolean stoneGame(int[] piles) {
    int n = piles.length;
    int[][] f_record = new int[n][n];
    int[][] s_record = new int[n][n];
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n; j++) {
            f_record[i][j] = -1;
            s_record[i][j] = -1;
        }
    }
    return f(piles, 0, piles.length - 1, f_record, s_record) > s(piles, 0, piles.length - 1, f_record, s_record);
}

public int f(int[] piles, int L, int R, int[][] f_record, int[][] s_record) {
    if (f_record[L][R] != -1) {
        return f_record[L][R];
    }
    if (L == R) {
        f_record[L][R] = piles[L];
        return f_record[L][R];
    }
    f_record[L][R] = Math.max(piles[L] + s(piles, L + 1, R, f_record, s_record), piles[R] + s(piles, L, R - 1, f_record, s_record));
    return f_record[L][R];
}

public int s(int[] piles, int L, int R, int[][] f_record, int[][] s_record) {
    if (s_record[L][R] != -1) {
        return s_record[L][R];
    }
    if (L == R) {
        s_record[L][R] = 0;
        return 0;
    }
    s_record[L][R] = Math.min(f(piles, L + 1, R, f_record, s_record), f(piles, L, R - 1, f_record, s_record));
    return s_record[L][R];
}

DP思路:

定义\(dp[i][j]\)代表在区间\([i,j]\)上,双方都做好选择的情况下,先手与后手的最大得分差值

public boolean stoneGame(int[] piles) {
    int n = piles.length;
    int[][] dp = new int[n+2][n+2];
    for(int len=1;len<n;len++){
        for(int l = 0;l+len<n;l++){
            int r = l+len;
            dp[l][r] = Math.max(piles[l]-dp[l+1][r],piles[r]-dp[l][r-1]);
        }
       
    }
    return dp[0][n-1]>0;
}

12. 逆序对:

offer51】求数组中的逆序对。

在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。

【思路】: 时间复杂度O(nlogn) 考虑二分查找和归并排序的解法

int[] nums, tmp;

public int reversePairs(int[] nums) {
    this.nums = nums;
    tmp = new int[nums.length];
    return mergeSort(0, nums.length - 1);
}

private int mergeSort(int l, int r) {
    // 终止条件
    if (l >= r) {
        return 0;
    }
    // 递归划分
    int m = (l + r) / 2;
    int res = mergeSort(l, m) + mergeSort(m + 1, r);
    // 合并阶段
    int i = l, j = m + 1;
    for (int k = l; k <= r; k++) {
        tmp[k] = nums[k];
    }
    for (int k = l; k <= r; k++) {
        if (i == m + 1) {
            nums[k] = tmp[j++];
        } else if (j == r + 1 || tmp[i] <= tmp[j]) {
            nums[k] = tmp[i++];
        } else {
            nums[k] = tmp[j++];
            res += m - i + 1; // 统计逆序对
        }
    }
    return res;
}

13. 全排列:

【回溯算法经典应用】

13.1 不含重复数字的全排列:

  • 给定一个不含重复数字的数组 nums ,返回其所有可能的全排列 。你可以按任意顺序返回答案。
ArrayList<ArrayList<Integer>> res = new ArrayList<>();

public ArrayList<ArrayList<Integer>> permute(int[] num) {
    backTrack(num,new LinkedList<>());
    return res;
}

public void backTrack(int[] nums, LinkedList<Integer> list){
    if(nums.length == list.size()){
        res.add(new ArrayList<>(list));
        return;
    }
    for(int num : nums){
        if(list.contains(num)){   // 判断是否处理过
            continue;
        }
        list.add(num);
        backTrack(nums,list);
        list.removeLast();
    }
}

13.2 包含重复数字的全排列:

  • 给定一个可包含重复数字的序列nums ,按任意顺序返回所有不重复的全排列。
List<List<Integer>> res = new ArrayList<>();
int[] visit;
public List<List<Integer>> permuteUnique(int[] nums) {
    Arrays.sort(nums);   //排序
    visit = new int[nums.length];
    backTrack(nums,new LinkedList<Integer>());
    return res;
}

public void backTrack(int[] nums,LinkedList<Integer> list){
    if(list.size() == nums.length){
        res.add(new ArrayList<>(list));
        return;
    }
    for(int i=0;i<nums.length;i++){
        if(visit[i]==1 || (i>0 && nums[i] == nums[i-1] && visit[i-1] ==1)){
            continue;
        }
        list.add(nums[i]);
        visit[i] = 1;
        backTrack(nums,list);
        visit[i] = 0;
        list.removeLast();
    }
}

三、基本数据结构:

双指针在链表和数组中的应用:

  1. 快慢指针主要解决链表中的问题:

    是否有环、找中点

  2. 左右指针主要解决数组中的问题:

    二分查找

1. 链表:

1.1 删除倒数第k个节点:[19]

+ 两次循环实现,第一次获得链表长度,第二次删除指定节点,效率低。
+ 采用滑动窗口实现,窗口大小为k。

1.2 判断环形链表:[141]

  • 通过哈希表存储节点信息来判断,空间复杂度较高
  • 快慢指针,如果在遍历过程中,两个指针相遇,则存在环。

找到环形链表的入口:

1.3 判断两个链表是否相交:[160]

  • 哈希表存储一个链表的所有节点信息,然后遍历另一个链表,第一个在set中的就是相交的起点,空间复杂度高

  • 双指针法

    ​ 创建两个指针 pA 和 pB,分别初始化为链表 A 和 B 的头结点。然后让它们向后逐结点遍历。当 pA到达链表的尾部时,将它重定位到链表 B 的头结点 (你没看错,就是链表 B); 类似的,当 pB 到达链表的尾部时,将它重定位到链表 A 的头结点。若在某一时刻 pA和 pB 相遇,则 为相交结点。

    假设pA和pB走完了全程:

    由于指针pA走了个A+B,指针pB走了个B+A,因此两者走过的长度是相等的

    • A和B结尾部分是相等的,因此A+B和B+A结尾部分也是相等的
    • 因此当pA与pB相遇时,该节点就是相交节点

    假设A和B没有交点:

    A和B均是以null结尾,因此A+B和B+A也是以null结尾

    • 因此当pA和pB恰好走完全程后,他们同时到达null,此时他们是相等的
    • 依然满足循环终止条件

1.4 反转链表[206]:

  1. 解题思路:

    反转链表的核心在于每一个节点的next指针需要指向当前节点的前一个节点,在一次遍历链表的过程中,分别用两个临时变量保存当前遍历节点的上一个节点【pre】和下一个节点【next】,然后修改当前节点的next指针。考虑初始化问题,pre指针指针必须初始化为null,因为反转完成后,第一个节点的next一定指向null。而next变量的赋值一定要在修改当前节点的next指针之前先保存下一个节点信息。

// 反转整个列表

// ---迭代实现---
public ListNode reverseList(ListNode head) {
    ListNode pre = null;
    ListNode next = null;
    while (head != null) {
        next = head.next;
        head.next = pre;
        pre = head;
        head = next;
    }
    return pre;
}
// ---递归实现---
public ListNode reverse(ListNode head){
   if(head.next == null){
       return head;
   }
   ListNode last = reverseList(head.next);
   head.next.next = head;
   head.next = null;
   return last;
}

// 反转链表中前n个节点
ListNode successor = null;
public ListNode reverseN(ListNode head,int n){
        if(n==1){
            successor = head.next;
            return head;
    }
    ListNode last = reverseN(head.next,n-1);
    head.next.next = head;
    head.next = successor;
    return last;
}

// 反转链表的一部分,给定索引区间[m,n]
ListNode successor = null;
public ListNode reverseBetween(ListNode head,int m,int n){
    if(m == 1){
            return reverseN(head,n); 
    }
    head.next = reverseBetween(head.next,m-1,n-1);
    return head;
}

1.5 判断回文链表[234]:

  1. 通过快慢指针找到链表的中点
  2. 反转右边链表
  3. 左右数据比对
public boolean isPalindrome(ListNode head) {
    ListNode fast = head;
    ListNode slow = head;
    //快慢指针找中点
    while(fast!=null && fast.next!=null){
        slow = slow.next;
        fast = fast.next.next;
    }
    //如果是奇数个节点,slow指针需要右移一位
    if(fast!=null){
        slow = slow.next;
    }
    //反转右半边链表
    ListNode right = reverse(slow);
    ListNode left = head;
    //左右比对
    while(right!=null){
        if(right.val !=left.val){
            return false;
        }
        right = right.next;
        left = left.next;
    }
    return true;
}

public ListNode reverse(ListNode node){
    ListNode pre = null;
    ListNode nxt;
    while(node!=null){
        nxt = node.next;
        node.next = pre;
        pre = node;
        node = nxt;
    }
    return pre;
}

1.6 K个一组反转链表[25]:

public ListNode reverseKGroup(ListNode head, int k) {
    if (head == null) {
        return null;
    }
    ListNode end = head;
    for (int i = 0; i < k; i++) {
        if (end == null) {
            return head;
        }
        end = end.next;
    }
    ListNode firstNode = reverse(head, end);
    head.next = reverseKGroup(end, k);
    return firstNode;
}

public ListNode reverse(ListNode begin, ListNode end) {
    ListNode pre = null;
    ListNode nxt;
    while (begin != end) {
        nxt = begin.next;
        begin.next = pre;
        pre = begin;
        begin = nxt;
    }
    return pre;
}

2. 数组:

2.1 丢失的数字[268]:

给定一个包含 [0, n]n 个数的数组 nums ,找出 [0, n] 这个范围内没有出现在数组中的那个数。

基本解法:

  • 对数组排序,然后遍历:

  • 利用hash集合:

    将数组中的每个元素加入哈希集合,然后依次检查从 0到 n的每个整数是否在哈希集合中

巧妙解法:

  • 位运算:

    利用异或运算的性质:

    • \(x \oplus x = 0\)
    • \(x \oplus 0 = x\)

    将数组的n个数和0-n的n+1个数一起异或的结果就是丢失的数。

  • 数学方法:

    0--n的n+1个数和数组中n个数分别求和的差值就是丢失的数。

2.2 只出现一次的数字:

2.2.1 一个元素出现一次,其他元素出现两次[136]:

借用异或的运算性质巧妙解题。

a ^ a = 0
int res = 0;
for(int num : nums){
    res ^= num;
}
return res;

2.2.2 一个元素出现一次,其他元素出现三次:

依次确定每一个二进制位。

​ 对于数组中非答案的元素,每一个元素都出现了 3次,对应着第 i个二进制位的 3个 0或 3 个 1,无论是哪一种情况,它们的和都是 3 的倍数(即和为 0 或 3)

int ans = 0;
for (int i = 0; i < 32; ++i) {
    int total = 0;
    for (int num: nums) {
        total += ((num >> i) & 1);
    }
    if (total % 3 != 0) {
        ans |= (1 << i);
    }
}
return ans;

2.2.3 两个元素出现一次,其他元素出现两次:

运算性质:

a ^ a = 0
使用位运算 x & -x 取出 x的二进制表示中最低位那个 1
  1. 首先遍历数组,进行异或运算,消除出现两次的元素,可以得到 xor = a ^ b
  2. 找到xor的二进制表示中的最低位的1,假设为第l位,那么,a和b中的某一个数的二进制表示的第l位一定是0,而另一个数的二进制表示的第l位一定是1。这样我们可以通过第l位将数组中的元素分为两类,一类是l位为0的,一类是l位为1的。

因此,如果我们将每一类的元素全部异或起来,那么其中一类会得到a,另一类会得到 b。这样我们就找出了这两个只出现一次的元素

public int[] singleNumber(int[] nums) {
    int xor = 0;
    for(int num : nums){
        xor ^= num;
    }
    // xor = a ^ b
    int diff = xor== Integer.MIN_VALUE ? xor : xor & (-xor);
    int a=0;
    int b=0;
    for(int num : nums){
        if((num & diff) !=0){
            a ^= num;
        }else{
            b ^= num;
        }
    }
    return new int[]{a,b};
}

3. 字符串:

3.1 反转每对括号间的字符串[1190]:

按照从括号内到外的顺序,逐层反转每对匹配括号中的字符串,并返回最终的结果。结果中不包含括号。

输入:s = "(ed(et(oc))el)"
输出:"leetcode"
  • 采用栈来处理

    在从左到右遍历字符串的过程中,需要记录每一层的字符串,而每遇到一个左括号就代表开启了新的一层,这时需要将上一层获取到的字符串入栈,然后记录新一层的字符,如果遇到了右括号,则说明当前这一层的字符读取完毕,需要对该字符串进行反转,反转后层数退到上一层,这时栈中栈顶的元素代表的是上一层的字符串,所以需要将当前层反转后的字符串拼接到上一层字符串的末尾,然后继续遍历,直到遇到下一个右括号,然后再将当前层的整个字符串反转。直到遍历结束。

3.2 最长回文子串[5]:

/**
 * 实现思路:
 * <p>
 * 外层遍历一次字符串,对于每一个字符进行两次中心扩展比较
 * <p>
 * 时间复杂度为O(n^2)
 *
 * @param s 输入字符
 * @return 输出结果
 */
public  String longestPalindrome(String s) {
    String longestStr = "";
    String tempStr1 = "";
    String tempStr2 = "";
    for (int i = 0; i < s.length(); i++) {
        tempStr1 = palindrome(s, i, i);
        tempStr2 = palindrome(s, i, i + 1);
        if (tempStr1.length() >= tempStr2.length()) {
            longestStr = longestStr.length() >= tempStr1.length() ? longestStr : tempStr1;
        } else {
            longestStr = longestStr.length() >= tempStr2.length() ? longestStr : tempStr2;
        }
    }
    return longestStr;

}

/**
 * 采用中心扩展法查找当前索引位置的最长回文字符串
 * 回文字符串有两种形式:
 * 如果字符串是奇数,则中心位置为一个字符串
 * 如果字符串为偶数,则中心位置位相邻的两个字符串
 *
 * @param s       查找字符串
 * @param median1 中心位置1
 * @param median2 中心位置2
 * @return
 */
public String palindrome(String s, int median1, int median2) {
    while (median1 >= 0 && median2 < s.length()) {
        if (s.charAt(median1) == s.charAt(median2)) {
            median1--;
            median2++;
        } else {
            break;
        }
    }
    return s.substring(++median1, median2);
}

3.3 最长回文子序列[516]:

考虑两个问题,状态转移方程和base case

\(dp[i][j]\)表示字符串 s 的下标范围\([i,j]\)内的最长回文子序列的长度。

状态转移:

\(if\) \(dp[i] == dp[j]\) \(dp[i][j] = dp[i+1][j-1] + 2\)

\(else\) \(dp[i][j] = max(dp[i+1][j],dp[i][j-1])\)

如何找出状态转移方程? https://mp.weixin.qq.com/s/zNai1pzXHeB2tQE6AdOXTA

public int longestPalindromeSubseq(String s) {
    int n = s.length();
    int[][] dp = new int[n][n];
    for (int i = 0; i < n; i++) {
        dp[i][i] = 1;
    }

    for (int i = n - 1; i >= 0; i--) {
        for (int j = i + 1; j < n; j++) {
            if (s.charAt(i) == s.charAt(j)) {
                dp[i][j] = dp[i + 1][j - 1] + 2;
            } else {
                dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1]);
            }
        }
    }
    return dp[0][n - 1];
}

3.4 最长公共子序列[1143]:

\(dp[i][j]\) 表示\(text1[0:i]\)\(text2[0][j]\)的最大公共子序列的长度

考虑边界情况; \(dp[0][j] = 0\) \(dp[i][0] = 0\)

i,j取值从1~n

状态转移:

\(if\) \(text1[i-1] == text2[j-1]\) \(dp[i][j] = dp[i-1][j-1] + 1\)

\(else\) \(dp[i][j] = max(dp[i-1][j],dp[i][j-1])\)

public int longestCommonSubsequence(String text1, String text2) {
    int m = text1.length();
    int n = text2.length();

    int[][] dp = new int[m + 1][n + 1];
    for (int i = 1; i <= m; i++) {
        for (int j = 1; j <= n; j++) {
            if (text1.charAt(i - 1) == text2.charAt(j - 1)) {
                dp[i][j] = 1 + dp[i - 1][j - 1];
            } else {
                dp[i][j] = Math.max(dp[i][j - 1], dp[i - 1][j]);
            }
        }
    }
    return dp[m][n];
}

3.5 最长递增子序列[300]:

  • \(dp[i]\)的值代表以\(nums[i]\)结尾的最长子序列长度
public int lengthOfLIS(int[] nums) {
    int n = nums.length;
    int[] dp = new int[n];
    Arrays.fill(dp,1);
    int maxLen = 1;
    for(int i=0;i<n;i++){
       for(int j=0;j<i;j++){
           if(nums[i]>nums[j]){
               dp[i] = Math.max(dp[i],dp[j]+1);
           }
       }
        maxLen = Math.max(maxLen,dp[i]);
    }
    return maxLen;
}
  • 二分查找:
public int lengthOfLIS(int[] nums) {
    int n = nums.length;
    int[] d = new int[n];
    int res = 0;
    for (int num : nums) {
        int left = 0;
        int right = res;
        while (left < right) {
            int m = left + ((right - left) >> 1);
            if (d[m] < num) {
                left = m + 1;
            } else {
                right = m;
            }
        }
        d[right] = num;
        if (res == right) {
            res++;
        }
    }
    return res;
}

3.6 最短编辑距离[72]:

\(dp[i][j]\)表示字符串A的前i个字母和字符串B的前j个字母之间的编辑距离

状态转移:

  • 如果A和B的最后一个字母相同

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

    • 如果A和B的最后一个字母不同

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

image-20220308091020108
public int minDistance(String word1, String word2) {
    int m = word1.length();
    int n = word2.length();
    int[][] dp = new int[m+1][n+1];
    for(int i=1;i<=m;i++){
        dp[i][0] = i;
    }
    for(int j=1;j<=n;j++){
        dp[0][j] = j;
    }
    for(int i=1;i<=m;i++){
        for(int j=1;j<=n;j++){
            if(word1.charAt(i-1) == word2.charAt(j-1)){
                dp[i][j] = Math.min(dp[i][j-1]+1,Math.min(dp[i-1][j]+1,dp[i-1][j-1]));
            }else{
                dp[i][j] = Math.min(dp[i][j-1],Math.min(dp[i-1][j],dp[i-1][j-1]))+1;
            }
        }
    }
    return dp[m][n];
}

四、数学运算技巧:

1. 常用位操作:

1.1 利用或操作 | 和空格将英文字符转换为小写

('a' | ' ') = 'a'
('A' | ' ') = 'a'

1.2 利用与操作 & 和下划线将英文字符转换为大写

('b' & '_') = 'B'
('B' & '_') = 'B'

1.3 利用异或操作 ^ 和空格进行英文字符大小写互换

('d' ^ ' ') = 'D'
('D' ^ ' ') = 'd'

1.4 判断两个数是否异号

int x = -1, y = 2;
bool f = ((x ^ y) < 0); // true

int x = 3, y = 2;
bool f = ((x ^ y) < 0); // false

1.5 不用临时变量交换两个数

int a = 1, b = 2;
a ^= b;
b ^= a;
a ^= b;
// 现在 a = 2, b = 1

1.6 n & (n - 1)巧妙用法

image-20210715092932003

计算汉明权重:因为 n & (n - 1) 可以消除最后一个 1,所以可以用一个循环不停地消除 1 同时计数,直到 n 变成 0 为止。

判断一个数是否是2的指数: 一个数如果是 2 的指数,那么它的二进制表示一定只含有一个 1。

2. Java基本数据类型数组和包装类型数组转换以及排序:

int[] arr = new int[5];
//升序
Arrays.sort(arr);
//降序
Integer[] arr2 = Arrays.stream(arr).boxed().toArray(Integer[]::new);
Arrays.sort(arr2,(a,b)->(b-a));
int[] arr3 = Arrays.stream(arr2).mapToInt(Integer::intValue).toArray();
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!

请我喝杯咖啡吧~

支付宝
微信