Skip to content

caipengbo/LeetCode

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

LeetCode总结

所有的题目总结均在每一个package的README中

目录

  • 搜索(回溯、BFS、DFS):
      1. 回溯:数独、N皇后、37、51、79、93、[212、301]
      1. BFS:矩阵、单词变换
      1. 排列、组合、分割、子集:四大类问题,常用回溯、DFS解决
      1. 图的搜索:DFS、BFS、并查集、Flood
      1. 并查集(TODO)
  • 二分查找:
    • g 函数,利用边界
    • K th 问题
    • 旋转数组
  • 双指针:
    • 左右指针:数组(或字符串)问题,二分查找也算是双指针,三数之和,Sunday算法
    • 快慢指针:链表中环的问题
    • 滑动窗口:更新窗口
  • 链表:
    • 链表的基本操作
    • 旋转(K组旋转,奇偶旋转)、拆分
    • 归并
    • 判断环(快慢指针)
  • 二叉树:
    • 遍历
    • 深度、层次
    • 树的结构相关
    • 树的路径相关,递归的过程之中不断更新全局变量
    • 剪枝
    • 二叉搜索树
  • 数学:
    • 概率:洗牌算法、蓄水池抽样、蒙特卡洛
    • 数论:素数,最小公倍数,最大公约数
    • 位运算:异或,与的巧妙用法
    • 特殊的数:有效数字(状态机),第n个丑数,平方数(DP解法),回文数
    • 数字的转化:溢出检测、模拟运算(时刻注意溢出)、罗马、字符转成数字;分数转小数
    • 其他:Pow()快速幂算法,众数投票法
  • 动态规划
  • 排序
  • 数据结构
    • 单调栈
    • 单调队列
  • 分治
  • 贪心

心法宝典

  1. 递归要素:开头-判断边界(退出条件);中间-进行相关的计算、缩小范围递归(经常用到全局变量哦);结尾-返回值(还要学会如何利用返回值)
  2. 反转链表:迭代写法返回prev,递归写法每次考虑两个节点(返回值是前递归的返回值)
  3. 采样:n个数随机采样m个,knuth采样:对于每个下标[0,n) 每次n--, 若rand()%n < m时才m--
  4. Shuffle:knuth Shuffle,i从后往前,每次从[0,i]选择一个位置与i交换
  5. DP: DP 就是填表法,首先明确初始值(并且要明确这个值是什么值),然后要明确填表的顺序,最后是如何填(状态转移方程)。
  6. 使用移位运算一定要注意运算符的优先级

双指针

双指针主要分为三类:左右(一般是数组或者字符串,经典的有三数之和和四数之和),快慢指针(一般是和链表环有关系),滑动窗口

1. 序列

  1. 无重复字符的最长子串 给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。注意区分子串与子序列区别
// [i, j), 无重复的时候j++, 有重复的时候i++
public int lengthOfLongestSubstring(String s) {
    Set<Character> set = new HashSet<>();  // int[] dic = new int[256];
    int i = 0, j = 0;
    int ret = 0;
    while (j < s.length() && i <= j) {
        // 是否存在重复字符
        if (!set.contains(s.charAt(j))) {
            set.add(s.charAt(j));
            j++;
            ret = Math.max(ret, j-i);  // 注意此处没+1,因为j先自增了
        } else {
            set.remove(s.charAt(i));
            i++;
        }
    }
    return ret;
}
  1. 盛最多水的容器

不能倾斜容器,且 n (数组的长度)的值至少为 2。

思路:我们要做的就是保证宽度最大的情况下,高度最大; 一开始宽度最大,然后逐步减少宽度;这个时候要不断的去更新高度,使得高度尽量的大,如何移动较大的一端,那么面积肯定是减小的;移动较小的那一个端,面积有可能增大

public int maxArea(int[] height) {
    int i = 0, j = height.length-1;
    int maxValue = 0;
    while (j - i >= 1) {
        System.out.println((j-i) * Math.min(height[i], height[j]));
        maxValue = Math.max(maxValue, (j-i) * Math.min(height[i], height[j]));
        if (height[i] < height[j]) {
            i++;
        } else {
            j--;
        }
    }
    return maxValue;
}
  1. 三数之和 给定一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a, b, c, 使得 a + b + c = 0 ?找出所有满足条件且不重复的三元组。
// 难点: 三数之和 -> 两数之和(哈希、排序+双指针); 排除重复
// 排序+双指针 : val<target时low++; val>target时 high--; val==target时low++ & high--
// 如何跳过重复
public List<List<Integer>> threeSum(int[] nums) {
    List<List<Integer>> ret = new LinkedList<>();
    Integer iPre = null;
    Arrays.sort(nums);
    for (int i = 0; i < nums.length-2; i++) {
        if (nums[i] > 0) break;
        int j = i+1, k = nums.length-1, sum = 0 - nums[i];
        Integer jPre = null, kPre = null;
        if (iPre != null && iPre == nums[i]) continue;  // 去重
        iPre = nums[i];
        while (j < k) {
            if (nums[j] + nums[k] < sum) {
                j++;
            }
            else if (nums[j] + nums[k] > sum) {
                k--;
            } else {
                if (jPre == null || (jPre != nums[j] && kPre != nums[k])) {
                    ret.add(Arrays.asList(nums[i], nums[j], nums[k]));
                }
                jPre = nums[j];
                kPre = nums[k];
                j++;
                k--;
            }
        }
    }
    return ret;
}
  1. 最接近的三数之和 给定一个包括 n 个整数的数组 nums 和 一个目标值 target。找出 nums 中的三个整数,使得它们的和与 target 最接近。返回这三个数的和。假定每组输入只存在唯一答案。
// 本题假定只有 一个答案,所以不用去重了
// 和第15题思路一模一样
public int threeSumClosest(int[] nums, int target) {
    // nums.length > 3
    Arrays.sort(nums);
    int ret = nums[0]+nums[1]+nums[2];
    for (int i = 0; i < nums.length-2; i++) {
        int j = i+1, k = nums.length-1, sum;
        while (j < k) {
            sum = nums[i] + nums[j] + nums[k];
            if (Math.abs(sum-target) < Math.abs(ret-target)) {
                ret = sum;
            }
            if (sum == target) {
                return target;
            } else if (sum > target) {
                k--;
            } else {
                j++;
            }
        }
    }
    return ret;
}
  1. 四数之和 给定一个包含 n 个整数的数组 nums 和一个目标值 target,判断 nums 中是否存在四个元素 a,b,c 和 d , 使得 a + b + c + d 的值与 target 相等? 找出所有满足条件且不重复的四元组。和第15题一个思路。
// 双指针 + 去重
public List<List<Integer>> fourSum(int[] nums, int target) {
    List<List<Integer>> ret = new LinkedList<>();
    Integer aPre = null;
    Arrays.sort(nums);
    for (int a = 0; a < nums.length-3; a++) {
        if (nums[a] > 0 && nums[a] > target) break;  // 注意一定要加nums[a] > 0
        if (aPre != null && aPre == nums[a]) continue;
        aPre = nums[a];
        Integer bPre = null;
        for (int b = a+1; b < nums.length-2; b++) {
            if (nums[b] > 0 && nums[a] + nums[b] > target) break;
            if (bPre != null && bPre == nums[b]) continue;  // 去重
            bPre = nums[b];
            int c = b+1, d = nums.length-1, sum = target - (nums[a] + nums[b]);
            Integer cPre = null, dPre = null;
            while (c < d) {
                if (nums[c] + nums[d] < sum) c++;
                else if (nums[c] + nums[d] > sum) d--;
                else {
                    if (cPre == null || (cPre != nums[c] && dPre != nums[d])) {
                        ret.add(Arrays.asList(nums[a], nums[b], nums[c], nums[d]));
                    }
                    cPre = nums[c];
                    dPre = nums[d];
                    c++;
                    d--;
                }
            }
        }
    }
    return ret;
}
  1. 删除排序数组的重复项 不要使用额外的数组空间,你必须在原地修改输入数组并在使用 O(1) 额外空间的条件下完成。不需要考虑数组中超出新长度后面的元素。
public int removeDuplicates(int[] nums) {
    int i = 0, len = nums.length;
    for (int j = 0; j < len; j++) {
        if (j+1 < len && nums[j] != nums[j+1]) {
            nums[i++] = nums[j];
        }
    }
    if (len > 0) nums[i++] = nums[len-1];
    return i;
}
  1. 移除元素 原地移除元素,分很多种情况。
// 2. 当要移动的很多时候
public int removeElement(int[] nums, int val) {
    if (nums == null || nums.length == 0) return 0;
    int i = 0, j = nums.length-1;
    while (i <= j) {
        while (i <= j && nums[i] != val) i++;
        while (i <= j && nums[j] == val) j--;
        
        if (j > i) nums[i++] = nums[j--];
    }
    // if (nums[i] == val) return i;
    return j+1;
}
// 2.2 只和最后一个元素交换
public int removeElement2(int[] nums, int val) {
    if (nums == null || nums.length == 0) return 0;
    int i = 0, n = nums.length;
    while (i < n) {
        if (nums[i] == val) {
            nums[i] = nums[n-1];
            n--;
        } else {
            i++;
        }
    }
    // if (nums[i] == val) return i;
    return n;
}
  1. 模式匹配 不要求掌握较难的KMP算法,掌握Sunday算法。
// Sunday算法:如果当前窗口不匹配,比较窗口下一个字符串;下一个字符串在shift数组中的位置,就是窗口要偏移的距离
// 先计算shift数组 every char : len(needle) - max(char)   otherwise: len+1
public int strStr(String haystack, String needle) {
    // 使用 HashMap 实现shift数组
    HashMap<Character, Integer> shiftMap = new HashMap<>();
    int len = needle.length();
    for (int i = 0; i < len; i++) {
        Character character = needle.charAt(i);
        if (!shiftMap.containsKey(character)) {
            for (int j = len-1; j >= 0; j--) {
                if (needle.charAt(j) == character) {
                    shiftMap.put(character, len-j);
                    break;
                }
            }
        }
    }
    int p =  0;
    while (p + len <= haystack.length()) {
        int i;
        for (i = 0; i < len; i++) {
            if (haystack.charAt(p+i) != needle.charAt(i)) break;
        }
        if (i == len) return p;
        else if (p+len == haystack.length()) return -1;
        else p += shiftMap.getOrDefault(haystack.charAt(p+len), len+1);
    }
    return -1;
}
  1. 颜色分类 给定一个包含红色、白色和蓝色,一共 n 个元素的数组,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。此题中,我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。使用一趟扫描。
private void swap(int[] nums, int i, int j) {
    if (nums[i] == nums[j]) return;
    int temp = nums[i];
    nums[i] = nums[j];
    nums[j] = temp;
}
public void sortColors(int[] nums) {
    int p0 = 0, p2 = nums.length-1;
    int cur = 0;
    while (cur <= p2) {
        if (nums[cur] == 0) swap(nums, cur++, p0++);
        else if (nums[cur] == 2) swap(nums, cur, p2--);
        else cur++;
    }
}
  1. 合并两个有序数组 给定两个有序整数数组 nums1 和 nums2,将 nums2 合并到 nums1 中,使得 num1 成为一个有序数组。初始化 nums1 和 nums2 的元素数量分别为 m 和 n。你可以假设 nums1 有足够的空间(空间大小大于或等于 m + n)来保存 nums2 中的元素。
public void merge(int[] nums1, int m, int[] nums2, int n) {
    int i = m-1, j = n-1, k = m+n-1;
    while (i >= 0 && j >= 0) {
        if (nums1[i] < nums2[j]) {
            nums1[k--] = nums2[j--];
        } else {
            nums1[k--] = nums1[i--];
        }
    }
    while (j >= 0) {
        nums1[k--] = nums2[j--];
    }
}
  1. 搜索二维矩阵 II 编写一个高效的算法来搜索 m x n 矩阵 matrix 中的一个目标值 target。 该矩阵具有以下特性:每行的元素从左到右升序排列。每列的元素从上到下升序排列。
public boolean searchMatrix(int[][] matrix, int target) {
    if (matrix.length == 0 || matrix[0].length == 0) return false;
    int m = matrix.length, n = matrix[0].length, row = 0, col = n-1;
    while (row < m && col >= 0) {
        if (matrix[row][col] < target) row++;
        else if (matrix[row][col] > target) col--;
        else return true;
    }
    return false;
}
  1. 移动零 给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。必须在原数组上操作,不能拷贝额外的数组。尽量减少操作次数。
private void swap(int[] nums, int i, int j) {
    int temp = nums[i];
    nums[i] = nums[j];
    nums[j] = temp;
}
public void moveZeroes(int[] nums) {
    int zeroPos = 0;
    for (int i = 0; i < nums.length; i++) {
        if (nums[i] != 0) {
            swap(nums, zeroPos++, i);
        }
    }
}

2. 快慢指针

  1. 环形链表 给一个链表判断链表是否有环,你能用 O(1)(即,常量)内存解决此问题吗?
// 使用哈希表,将每个指针地址存入,然后判断,空间复杂度 O(n)
// 快慢指针 类似于两人跑步(慢指针每次1步,快指针每次2步),那么 环形部分/1 = 循环迭代的次数
public boolean hasCycle(ListNode head) {
    ListNode slow = head, fast = head;
    while (fast != null && fast.next != null) {
        slow = slow.next;
        fast = fast.next;
        fast = fast.next;
        if (slow == fast) return true;
    }
    return false;
}
  1. 环形链表 II 给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。说明:不允许修改给定的链表。
// 是否有环,注意返回的不是入口节点哦, 实际上是获得的环形部分的数目!!!
public ListNode hasCycle(ListNode head) {
    ListNode slow = head, fast = head;
    while (fast != null && fast.next != null) {
        slow = slow.next;
        fast = fast.next;
        fast = fast.next;
        if (slow == fast) return slow;
    }
    return null;
}
// 有环,找环的入口地点,然后同步走,就会走到环的入口处
public ListNode detectCycle(ListNode head) {
    ListNode node1 = head;
    ListNode node2 = hasCycle(head);
    if (node2 == null) return null;
    while (node1 != node2) {
        node1 = node1.next;
        node2 = node2.next;
    }
    return node1;
}
  1. 相交链表 编写一个程序,找到两个单链表相交的起始节点:如果两个链表没有交点,返回 null;在返回结果后,两个链表仍须保持原有的结构;可假定整个链表结构中没有循环;程序尽量满足 O(n) 时间复杂度,且仅用 O(1) 内存,如果使用 hash 很简单。
    // 方法2:如何找到两个链表的差距? 快的指针走到头后,回到长的链表头部;慢的指针走到头,回到短的链表头部,这样就抵消了 差距
    public ListNode getIntersectionNode2(ListNode headA, ListNode headB) {
        if (headA == null || headB == null) return null;
        ListNode p = headA, q = headB;
        while (p != q) {
            p = (p == null) ? headB : p.next;
            q = (q == null) ? headA : q.next;
        }
        return p;
    }
  1. 回文链表 请判断一个链表是否为回文链表。用 O(n) 时间复杂度和 O(1) 空间复杂度解决此题。
// 寻找mid,翻转后半部分,最后比较
public boolean isPalindrome(ListNode head) {
    if (head == null || head.next == null) return true;
    ListNode slow = head, fast = head;
    while (fast != null && fast.next != null) {
        slow = slow.next;
        fast = fast.next.next;
    }
    // 偶数时,slow指向后半部分的开头,奇数时,slow指向正中间(让slow再多走一个)
    if (fast != null) slow = slow.next;
    slow = reverse(slow);

    fast = head;
    while (slow != null) {
        if (fast.val != slow.val) return false;
        slow = slow.next;
        fast = fast.next;
    }
    return true;
}
private ListNode reverse(ListNode head) {
    if (head == null || head.next == null) return head;
    ListNode pre = null, p = head, q;
    while (p != null) {
        q = p.next;
        p.next = pre;
        pre = p;
        p = q;
    }
    return pre;
}

3. 滑动窗口

  1. 长度最小的子数组(双指针滑动窗口) 给定一个含有 n 个正整数的数组和一个正整数 s ,找出该数组中满足其和 ≥ s 的长度最小的连续子数组。如果不存在符合条件的连续子数组,返回 0。
public int minSubArrayLen(int s, int[] nums) {
    int n = nums.length, res = n + 1;
    int i = 0, j;
    for (j = 0; j < n; j++) {
        s -= nums[j];
        while (s <= 0) {
            res = Math.min(res, j-i+1);
            s += nums[i++];
        }
    }
    return res % (n + 1); // res == n+1 说明不存在,返回0
}
  1. 爱生气的书店老板 书店老板有一家店打算试营业 customers.length 分钟。每分钟都有一些顾客(customers[i])会进入书店,所有这些顾客都会在那一分钟结束后离开。在某些时候,书店老板会生气。 如果书店老板在第 i 分钟生气,那么 grumpy[i] = 1,否则 grumpy[i] = 0。 当书店老板生气时,那一分钟的顾客就会不满意,不生气则他们是满意的。书店老板知道一个秘密技巧,能抑制自己的情绪,可以让自己连续 X 分钟不生气,但却只能使用一次。请你返回这一天营业下来,最多有多少客户能够感到满意的数量。 TODO https://leetcode-cn.com/problems/grumpy-bookstore-owner/

链表

链表主要是:一些基本操作(遍历),比较重要的 判断链表中是否有环(见上方快慢指针章节),比较难得就是链表的旋转(K个一组旋转,奇偶旋转)、拆分、归并(K路归并)

基本操作

  1. 两数相加 给出两个 非空 的链表用来表示两个非负的整数。其中,它们各自的位数是按照 逆序 的方式存储的,并且它们的每个节点只能存储 一位 数字。如果,我们将这两个数相加起来,则会返回一个新的链表来表示它们的和。
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
    ListNode head = new ListNode(-1);  // 初始创建一个头指针,就可以避免判断当前指针是否为空
    ListNode cur = head;
    int sum = 0;  // 余数、和使用一个变量来表示
    while (l1 != null || l2 != null || sum != 0) {
        if (l1 != null) {
            sum += l1.val;
            l1 = l1.next;
        }
        if (l2 != null) {
            sum += l2.val;
            l2 = l2.next;
        }
        cur.next = new ListNode(sum % 10);  // 使用取余和取整
        cur = cur.next;
        sum = sum / 10;
    }
    return head.next;
}
  1. 对链表进行插入操作
// 优化
public ListNode insertionSortListOpt(ListNode head) {
    ListNode dummy = new ListNode(Integer.MIN_VALUE);
    ListNode cur = head, p = null, next;
    while (cur != null) {
        // p = dummy; 每次都把 p 置在了头部位置
        if (p == null || p.val >= cur.val) p = dummy;  // 优化: 有时候不必将 p 移动至 头部位置
        next = cur.next;
        while (p.next != null && cur.val > p.next.val) {
            p = p.next;
        }
        // 注意以下两行代码
        cur.next = p.next;
        p.next = cur;
        cur = next;
    }
    return dummy.next;
}
  1. 两数相加2 链表不是逆序方式存储的,该如何去做。
// 不修改输入链表
// 难点:1. 预先不只是位数 2. 进位不清楚  3. 计算顺序是从后往前的
// 使用栈 时间和空间复杂度 O(m+n)
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
    Stack<Integer> stack1 = new Stack<>();
    Stack<Integer> stack2 = new Stack<>();
    while (l1 != null) {
        stack1.push(l1.val);
        l1 = l1.next;
    }
    while (l2 != null) {
        stack2.push(l2.val);
        l2 = l2.next;
    }

    ListNode head= null, cur;
    int sum = 0;
    while (!stack1.empty() || !stack2.empty() || sum != 0) {
        if (!stack1.empty()) sum += stack1.pop();
        if (!stack2.empty()) sum += stack2.pop();
        cur = new ListNode(sum % 10);
        cur.next = head;
        head = cur;
        sum = sum / 10;
    }
    return head;
}

删除

  1. 删除链表的倒数第N个节点 给定一个链表,删除链表的倒数第 n 个节点(n保证有效, 不会等于0 哦),并且返回链表的头结点。使用一趟扫描。
// 简化:使用dummy,不用pre指针,直接slow指向要删除的节点的前面位置,使用 n 递减计数
public ListNode removeNthFromEnd2(ListNode head, int n) {
    if (head == null) return null;
    ListNode dummy = new ListNode(-1);
    dummy.next = head;
    ListNode fast = dummy, slow = dummy;
    while (fast.next != null) {
        if (n <= 0) slow = slow.next;
        fast = fast.next;
        n--;
    }
    slow.next = slow.next.next;
    return dummy.next;
}
  1. 删除排序链表中的重复元素 给定一个排序链表,删除所有含有重复数字的节点,只保留原始链表中 没有重复出现 的数字。
public ListNode deleteDuplicates(ListNode head) {
    if(head == null) return null;
    ListNode dummy = new ListNode(-1);
    dummy.next = head;
    ListNode pre = dummy, p = head;
    while (p != null) {
        if (p.next == null || p.next.val != p.val) {
            if (pre.next == p) pre = p;
            else pre.next = p.next;
        }
        p = p.next;
    }
    return dummy.next;
}
  1. 删除排序链表中的重复元素 II 给定一个排序链表,删除所有重复的元素,使得每个元素只出现一次。
public ListNode deleteDuplicates(ListNode head) {
    if(head == null) return null;
    ListNode dummy = new ListNode(-1);
    dummy.next = head;
    ListNode p = head;
    while(p != null && p.next != null) {
        if(p.val == p.next.val) {
            p.next = p.next.next;
        } else {
            p = p.next;
        }
    }
    return dummy.next;
}
  1. 移除链表元素 删除链表中等于给定值 val 的所有节点。
public ListNode removeElements(ListNode head, int val) {
    if(head == null) return null;
    int temp = (val == -1) ? -2 : -1;
    ListNode dummy = new ListNode(temp);
    dummy.next = head;
    ListNode p = dummy;
    while(p.next != null) {
        if(p.next.val == val) {
            p.next = p.next.next;
        } else {
            p = p.next;
        }
    }
    return dummy.next;
}

旋转

  1. 两两交换链表中的节点 给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。给定 1->2->3->4, 你应该返回 2->1->4->3.
// 节点的移动(三个节点)
public ListNode swapPairs(ListNode head) {
    if (head == null || head.next == null) return head;
    ListNode dummy = new ListNode(-1);
    dummy.next = head;
    ListNode cur = dummy, swap1, swap2;
    while (cur.next != null && cur.next.next != null) {
        swap1 = cur.next;
        swap2 = cur.next.next;
        cur.next = swap2;
        swap1.next = swap2.next;
        swap2.next = swap1;
        cur = swap1;
    }
    return dummy.next;
}
// 递归写法(考虑两个节点)
public ListNode swapPairsRecursive(ListNode head) {
    if(head == null || head.next == null) return head;
    ListNode nextNode = head.next;
    head.next = swapPairsRecursive(nextNode.next);
    nextNode.next = head;
    return nextNode;
}
  1. K个一组反转链表 给你一个链表,每 k 个节点一组进行翻转,请你返回翻转后的链表。 k 是一个正整数,它的值小于或等于链表的长度。 如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。
public ListNode reverseKGroup(ListNode head, int k) {
    ListNode p = head, q = head;
    // 找到 第 k-1个结点p
    for (int i = 0; i < k; i++) {
        if (p == null) return head;
        q = p;
        p = p.next;
    }
    q.next = null;
    ListNode newHead = reverse(head);
    head.next = reverseKGroup(p, k);
    return newHead;
}
private ListNode reverse(ListNode head) {
    if (head == null || head.next == null) return head;
    ListNode ret = reverse(head.next);
    head.next.next = head;
    head.next = null;
    return ret;
}
  1. 旋转链表 给定一个链表,旋转链表,将链表每个节点向右移动 k 个位置,其中 k 是非负数。
// 快指针先走k-1步, 若快指针先为null说明链表长度小于k, 则快指针走 k%n
// 最终慢指针为新头结点,快指针.next = head
public ListNode rotateRight(ListNode head, int k) {
    if (head == null) return null;
    ListNode slow = head, fast = head, ret = null;
    int i = 0;
    for (i = 0; i < k; i++) {
        if (fast == null) break;
        fast = fast.next;
    }
    // k== 链表长度
    if (i == k && fast == null) return head;
    if (i < k) {
        k = k % i;
        fast = head;
        for (i = 0; i < k; i++) {
            fast = fast.next;
        }    
    }
    while (fast.next != null) {
        slow = slow.next;
        fast = fast.next;
    }
    fast.next = head;
    ret = slow.next;
    slow.next = null;
    return ret;
}
  1. 反转链表2 反转从位置 m 到 n 的链表。请使用一趟扫描完成反转。
// 设置DummyNode
public ListNode reverseBetween2(ListNode head, int m, int n) {
    ListNode dummy = new ListNode(-1);
    dummy.next = head;
    ListNode oldLast = dummy, before = null, cur, after;
    
    for (int i = 1; i < m; i++) {
        oldLast = oldLast.next;
    }
    ListNode reverseLast = oldLast.next;
    cur = reverseLast;
    for (int i = m; i <= n; i++) {
        after = cur.next;
        cur.next = before;
        before = cur;
        cur = after;
    }
    reverseLast.next = cur;
    oldLast.next = before;
    return dummy.next;
}
  1. 反转链表(使用迭代 和 递归) 反转一个单链表。
// 迭代(循环版本)
public ListNode reverseList(ListNode head) {
    ListNode front = null, cur = head, back;
    while (cur != null) {
        back = cur.next;
        cur.next = front;
        front = cur;
        cur = back;
    }
    return front;
}
// 递归版本
public ListNode reverseList2(ListNode head) {
    if (head == null || head.next == null) return head;
    ListNode p = reverseList(head.next);
    head.next.next = head;
    head.next = null;
    return p;
}

拆分

  1. 复制带随机指针的链表 给定一个链表,每个节点包含一个额外增加的随机指针,该指针可以指向链表中的任何节点或空节点。要求返回这个链表的深拷贝。你必须返回给定头的拷贝作为对克隆列表的引用。 难点:如何拷贝 随机节点? 思路1:使用 HashMap: 需要额外的空间 2. 旧节点和新节点 交错排列,然后复制 random,再拆开 next.
private class Node {
    public int val;
    public Node next;
    public Node random;

    public Node() {}

    public Node(int _val, Node _next, Node _random) {
        val = _val;
        next = _next;
        random = _random;
    }
}
// 拆开链表的地方有点不一样
public Node copyRandomList(Node head) {
    if (head == null) return null;
    Node p = head, newNode, q;
    // 交叉排列
    while (p != null) {
        newNode = new Node(p.val, null, null);
        newNode.next = p.next;
        p.next = newNode;
        p = newNode.next;
    }
    // 赋值新节点的 random
    p = head;
    while (p != null) {
        p.next.random = (p.random != null ? p.random.next : null);
        p = p.next.next;
    }
    // 拆开链表(拆解方法 2)
    p = head;
    q = head.next;
    newNode = p.next;
    while (p.next != null && p.next.next != null) {
        p.next = p.next.next;
        q.next = q.next.next;
        p = p.next;
        q = q.next;
    }
    p.next = null;  // 注意封尾操作,详细的拆分 参见 328题
    return newNode;
}
  1. 奇偶链表(链表的拆分) 给定一个单链表,把所有的奇数节点和偶数节点分别排在一起。请注意,这里的奇数节点和偶数节点指的是节点编号的奇偶性,而不是节点的值的奇偶性。 请尝试使用原地算法完成。你的算法的空间复杂度应为 O(1),时间复杂度应为 O(nodes),nodes 为节点总数。应当保持奇数节点和偶数节点的相对顺序。链表的第一个节点视为奇数节点,第二个节点视为偶数节点,以此类推。
public static ListNode oddEvenList(ListNode head) {
    if (head == null || head.next == null || head.next.next == null) return head;
    ListNode p = head, q = head.next;
    ListNode evenHead = head.next;
    while (p.next != null && p.next.next != null) {  // 判断条件是 p.next,也就是说,是 到 链表的最后一个元素,这个时候需要进行封尾操作
        p.next = p.next.next;
        q.next = q.next.next;
        p = p.next;
        q = p.next;
    }
    p.next = evenHead;  // 封尾操作(该题比较特殊,如果是单纯的将一个链表拆成一个,需要进行封尾)
    // 因为 p.next == null 跳出循环, p
    return head;
}

归并

  1. 合并两个有序链表(循环写法和递归写法) 将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
    ListNode head = new ListNode(-1);
    ListNode p = head;
    while(l1 != null && l2 != null) {
        if (l1.val <= l2.val) {
            p.next = l1;
            l1 = l1.next;
        } else {
            p.next = l2;
            l2 = l2.next;
        }
        p = p.next;
    }
    // 直接接到 剩余的链表即可
    if (l1 != null) p.next = l1;
    if (l2 != null) p.next = l2;

    return head.next;
}
public ListNode mergeTwoListsRecursive(ListNode l1, ListNode l2) {
    if (l1 == null) return l2;
    if (l2 == null) return l1;
    if (l1.val <= l2.val) {
        l1.next = mergeTwoListsRecursive(l1.next, l2);
        return l1;
    } else {
        l2.next = mergeTwoListsRecursive(l1, l2.next);
        return l2;
    }
}
  1. 合并K个排序链表(使用堆和不使用堆) 合并 k 个排序链表,返回合并后的排序链表。请分析和描述算法的复杂度。
// 使用堆
public ListNode mergeKLists(ListNode[] lists) {
    int k = lists.length;
    if (k == 0) return null;
    PriorityQueue<ListNode> priorityQueue = new PriorityQueue<ListNode>(k, new Comparator<ListNode>() {
        @Override
        public int compare(ListNode o1, ListNode o2) {
            return o1.val - o2.val;
        }
    });
    ListNode head = new ListNode(-1);
    ListNode p = head, temp;
    boolean hasNode = true;
    while (hasNode) {
        hasNode = false;
        for (int i = 0; i < k; i++) {
            if (lists[i] != null) {
                //  这里也要注意
                temp = lists[i];
                lists[i] = lists[i].next;
                priorityQueue.add(temp);
                hasNode = true;
            }
        }
    }
    while (!priorityQueue.isEmpty()) {
        p.next = priorityQueue.poll();
        p = p.next;
    }
    p.next = null;  // 必须加这个封尾操作,有可能会出现环
    return head.next;
}
// 不使用堆
public ListNode mergeKLists2(ListNode[] lists) {
    return mergeKLists2(lists, 0, lists.length - 1);
}
public ListNode mergeKLists2(ListNode[] lists, int start, int end) {
    if (start == end) return lists[start];
    else if (start < end) {
        int mid = start + (end - start) / 2;
        ListNode left = mergeKLists2(lists, start, mid);
        ListNode right = mergeKLists2(lists, mid+1, end);
        return mergeTwoListsRecursive(left, right);
    } else return null;
}
public ListNode mergeTwoListsRecursive(ListNode l1, ListNode l2) {
    if (l1 == null) return l2;
    if (l2 == null) return l1;
    if (l1.val <= l2.val) {
        l1.next = mergeTwoListsRecursive(l1.next, l2);
        return l1;
    } else {
        l2.next = mergeTwoListsRecursive(l1, l2.next);
        return l2;
    }
}
  1. 排序链表(归并排序) 在 O(n log n) 时间复杂度和常数级空间复杂度下,对链表进行排序。 注意:递归版本使用到了 系统栈,所以空间复杂度不是是lg(n);链表的归并需要移动指针找到链表的中点。
// 递归版本 (自顶向下)
public ListNode sortList(ListNode head) {
    if (head == null || head.next == null) return head;
    // 将链表分为两段
    ListNode p = head, q = head, pre = head;
    while (q != null && q.next != null) {
        pre = p;
        p = p.next;
        q = q.next.next;
    }

    pre.next = null;  // 截断链表
    ListNode left = sortList(head);
    ListNode right = sortList(p);
    return mergeTwoListsRecursive(left, right);
}
public ListNode mergeTwoListsRecursive(ListNode l1, ListNode l2) {
    if (l1 == null) return l2;
    if (l2 == null) return l1;

    if (l1.val <= l2.val) {
        l1.next = mergeTwoListsRecursive(l1.next, l2);
        return l1;
    } else {
        l2.next = mergeTwoListsRecursive(l1, l2.next);
        return l2;
    }
}
// 非递归版本(迭代)
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
    if (l1 == null) return l2;
    if (l2 == null) return l1;
    ListNode dummy = new ListNode(-1), cur = dummy;

    while (l1 != null && l2 != null) {
        if (l1.val < l2.val) {
            cur.next = l1;
            l1 = l1.next;
        } else {
            cur.next = l2;
            l2 = l2.next;
        }
        cur = cur.next;
    }
    if (l1 == null) {
        cur.next = l2;
    } else {
        cur.next = l1;
    } 
    return dummy.next;
}

树就是练习递归的地方,递归重点:边界值(提前退出),缩小范围(进入递归)(此处一般都要辅以操作),返回值(以及充分利用返回值)

遍历

三种遍历方法 94. 144. 145. 题前中后序遍历

递归写法(中序遍历为例):

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

迭代写法: 前序

List<Integer> ret = new ArrayList<>();
Stack<TreeNode> stack = new Stack<>();
TreeNode cur = root;
stack.add(cur);
while (!stack.isEmpty()) {
    cur = stack.pop();
    if (cur != null) {
        ret.add(cur.val);
        stack.add(cur.right);
        stack.add(cur.left);
    }
}
return ret;

中序

List<Integer> ret = new ArrayList<>();
Stack<TreeNode> stack = new Stack<>();
TreeNode cur = root;
while (cur != null || !stack.empty()) {
    while (cur != null) {
        stack.add(cur);
        cur = cur.left;
    }
    cur = stack.pop();
    ret.add(cur.val);
    cur = cur.right;
}
return ret;

后序 前序遍历的顺序是 根-左-右,后序遍历的顺序是 左-右-根 那么 前序稍微修改一下 变成 根-右-左,然后把结果倒序,就变成 左-右-根* 了

// 使用LinkedList的头插,让结果倒序
LinkedList<Integer> ret = new LinkedList<>();
Stack<TreeNode> stack = new Stack<>();
TreeNode cur = root;
stack.add(cur);
while (!stack.empty()) {
    cur = stack.pop();
    if (cur != null) {
        ret.addFirst(cur.val);  // 头插法,让结果倒序
        stack.add(cur.left);
        stack.add(cur.right);
    }
}
return ret;

根据遍历序列构造树

  1. 前中(唯一) 假设树中没有重复的元素,根据一棵树的前序遍历与中序遍历构造二叉树。
// 难点:如何在数组上分区域
// 方案:递归的在子数组上进行操作
public TreeNode buildTree(int[] preorder, int[] inorder) {
    return helper(0, 0, inorder.length-1, preorder, inorder);
}
public TreeNode helper(int preStart, int inStart, int inEnd, int[] preorder, int[] inorder) {
    if (preStart >= preorder.length || inStart > inEnd) return null;
    // 当前根节点
    TreeNode root = new TreeNode(preorder[preStart]);
    // 中序遍历中找到当前的 根节点,划分左右区域(为了加速可以使用 HashMap,O(1)时间找到inIndex)
    int inIndex = 0;
    for (int i = inStart; i <= inEnd; i++) {
        if (inorder[i] == root.val) {
            inIndex = i;
            break;
        }
    }
    // 划分区域
    root.left = helper(preStart+1, inStart, inIndex-1, preorder, inorder);
    root.right = helper(preStart+inIndex-inStart+1, inIndex+1, inEnd, preorder, inorder);
    return root;
}
  1. 中后(唯一)

    从中序与后序遍历序列构造二叉树

public TreeNode buildTree(int[] inorder, int[] postorder) {
    return helper(postorder.length-1, 0, inorder.length-1, postorder, inorder);
}
public TreeNode helper(int postStart, int inStart, int inEnd, int[] postorder, int[] inorder) {
    if (postStart < 0 || inStart > inEnd) return null;
    // 当前根节点
    TreeNode root = new TreeNode(postorder[postStart]);
    // 后序遍历中从后往前找到当前根节点,划分左右区域(为了加速可以使用 HashMap,O(1)时间找到inIndex)
    int inIndex = 0;
    for (int i = inStart; i <= inEnd; i++) {
        if (inorder[i] == root.val) {
            inIndex = i;
            break;
        }
    }
    // 划分区域(难点)
    root.left = helper(postStart-(inEnd-inIndex+1), inStart, inIndex-1, postorder, inorder);
    root.right = helper(postStart-1, inIndex+1, inEnd, postorder, inorder);
    return root;
}
  1. 前后(不唯一)

  2. 将二叉搜索树转化为排序的双向链表 剑指offer 36题, BST中序遍历是一个有序序列,使用全局变量保存前一个节点。

TreeNode pre = null;
public TreeNode convert(TreeNode root) {
    if (root == null) return null;
    this.pre = null;
    convertHelper(root);
    TreeNode p = root;
    while(p.left != null) p = p.left;
    return p;
}
// 中序遍历 记录前一个访问的节点
// 使用全局变量保存pre,在convertHelper中传pre的值不行
public void convertHelper(TreeNode cur) {
    if (cur == null) return;
    convertHelper(cur.left);

    cur.left = this.pre;
    if (pre != null) this.pre.right = cur;
    this.pre = cur;

    convertHelper(cur.right);
}

深度

  1. 二叉树的最大深度 给定一个二叉树,找出其最大深度。二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。说明: 叶子节点是指没有子节点的节点。
// 递归思路:二叉树的最大深度 = max(左子树深度,右子树深度)
public int maxDepth(TreeNode root) {
    if (root == null) return 0;
    int left = maxDepth(root.left);
    int right = maxDepth(root.right);
    return Math.max(left, right)+1;
}
  1. 判断平衡二叉树 左右子树的高度不相差1
// 在104的思路基础上修改:先计算树的高度,如果发现当前的节点高度差大于1了
// 那么就直接返回-1
private int recursive(TreeNode root) {
    if (root == null) return 0;
    int left = recursive(root.left);
    if (left == -1) return -1;
    int right = recursive(root.right);
    if (right == -1) return -1;
    if (Math.abs(left-right)>1) return -1;
    return Math.max(left, right) + 1;
}

public boolean isBalanced(TreeNode root) {
    return recursive(root) != -1;
}
  1. 二叉树的最小深度
// 难点:当二叉树退化成单侧时
public int minDepth(TreeNode root) {
    if (root == null) return 0;
    int left = minDepth(root.left);
    int right = minDepth(root.right);
    // 下面两句是和Max Depth不一样的地方
    if (root.left == null) return right+1;
    if (root.right == null) return left+1;
    return Math.min(left, right)+1;
}

层序

  1. 二叉树的层次遍历(广度优先遍历) 给定一个二叉树,返回其按层次遍历的节点值。 (即逐层地, 每一层都放到一个链表内,从左到右访问所有节点)。
// 使用一个队列,下一层元素数目等于当前队列中元素的数目
public List<List<Integer>> levelOrder(TreeNode root) {
    List<List<Integer>> ret = new ArrayList<>();
    if (root == null) return ret;
    Queue<TreeNode> queue = new LinkedList<>();
    queue.add(root);
    while (!queue.isEmpty()) {
        int nodeCount = queue.size();  // Key
        List<Integer> level = new ArrayList<>(nodeCount);
        for (int i = 0; i < nodeCount; i++) {
            TreeNode node = queue.poll();
            level.add(node.val);
            // 锯齿形层次遍历 使用flag判断是否反转
            // if (!flag) level.add(node.val);
            // else level.addFirst(node.val);
            if (node.left != null) queue.add(node.left);
            if (node.right != null) queue.add(node.right);
        }
        ret.add(level);  // 107题: ret.addFirst(level);
    }
    return ret;
}
// 递归写法(深度优先遍历),参数包括(node, level, resList),然后将第 K 层放到第K个 List
// 重点:什么时候初始化List
private void helper(TreeNode root, int level, List<List<Integer>> res) {
    if (root == null) return;
    // Key
    if (level >= res.size()) res.add(new LinkedList<>());
    res.get(level).add(root.val);
    // 103题锯齿形层次遍历:奇数时候头部插入结果List,偶数时候尾部插入
    // if(level % 2 == 0) res.get(level).add(root.val);
    // else ((LinkedList)res.get(level)).addFirst(root.val);
    helper(root.left, level+1, res);
    helper(root.right, level+1, res);
}
public List<List<Integer>> levelOrderRecursive(TreeNode root) {
    List<List<Integer>> res = new LinkedList<>();
    helper(root, 0, res);
    return res;
}
  1. 二叉树的层次遍历 II 给定一个二叉树,返回其节点值自底向上的层次遍历。 (即按从叶子节点所在层到根节点所在的层,逐层从左向右遍历)和102题一模一样,只不过本题需要将(层次)结果翻转一下,使用链表的头插,实现结果的翻转。

结构

  1. 相同的树 给定两个二叉树,编写一个函数来检验它们是否相同。如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。
public boolean isSameTree2(TreeNode p, TreeNode q) {
//        if (p == null && q == null) return true;  // 同时为空
//        if (p == null || q == null) return false;  // 不同时为空
    if (p == null || q == null) return p == q;  // 更精简的写法
    if (p.val != q.val) return false;  // 都不为空
    return isSameTree(p.left, q.left) && isSameTree(p.right, q.right);
}
  1. 对称二叉树(镜像对称) 给定一个二叉树,检查它是否是镜像对称的。例如,二叉树 [1,2,2,3,4,4,3]是对称的
private boolean recursive(TreeNode tree1, TreeNode tree2) {
    if (tree1 == null || tree2 == null) return tree1 == tree2;
    if (tree1.val != tree2.val) return false;
    return recursive(tree1.left, tree2.right) && recursive(tree1.right, tree2.left);
}
public boolean isSymmetric(TreeNode root) {
    return iterative(root, root);
}
  1. 反转二叉树
public TreeNode invertTree(TreeNode root) {
    if (root == null) return null;
    TreeNode left = invertTree(root.left);
    TreeNode right = invertTree(root.right);
    root.left = right;
    root.right = left;
    return root;
}
  1. 另一个树的子树 给定两个非空二叉树 s 和 t,检验 s 中是否包含和 t 具有相同结构和节点值的子树。s 的一个子树包括 s 的一个节点和这个节点的所有子孙。s 也可以看做它自身的一棵子树。
public boolean isSubtree(TreeNode s, TreeNode t) {
    if (t == null) {
        return true;
    }
    if (s == null) {
        return false;
    }
    if (s.val == t.val && isSame(s, t)) {
        return true;
    }
    return isSubtree(s.left, t) || isSubtree(s.right, t);
}
private boolean isSame(TreeNode tree1, TreeNode tree2) {
    if (tree1 == null || tree2 == null) {
        return tree1 == tree2;
    }
    return tree1.val == tree2.val && isSame(tree1.left, tree2.left) && isSame(tree1.right, tree2.right);
}

路径

  1. 路径和 判断树中是否存在一个路径,和为sum.
public boolean hasPathSum(TreeNode root, int sum) {
    if (root == null) return false;
    // if (root.left == null && root.right == null && root.val == sum) return true;
    if (root.left == null && root.right == null) return  root.val == sum;
    return hasPathSum(root.left, sum-root.val) || hasPathSum (root.right, sum-root.val); 
}
  1. 路径和2
// 难点:如何保存结果,回溯法!!!
private void findPath(TreeNode root, int sum, LinkedList<Integer> path, List<List<Integer>> res) {
    if (root == null) return;
    path.add(root.val);
    if (root.left == null && root.right == null) {
        // 注意必须new一个新的list
        if (root.val == sum) res.add(new LinkedList<>(path));
    }
    findPath(root.left, sum-root.val, path, res);
    findPath(root.right, sum-root.val, path, res);
    path.removeLast();
}
public List<List<Integer>> pathSum(TreeNode root, int sum) {
    LinkedList path = new LinkedList();
    List<List<Integer>> res = new ArrayList<>();
    findPath(root, sum, path, res);
    return res;
}
  1. 求根到叶子节点数字之和 给定一个二叉树,它的每个结点都存放一个 0-9 的数字,每条从根到叶子节点的路径都代表一个数字。例如,从根到叶子节点路径 1->2->3 代表数字 123。 计算从根到叶子节点生成的所有数字之和。
private int sum;
public int sumNumbers(TreeNode root) {
    sum = 0;
    helper(root, 0);
    return sum;
}
private void helper(TreeNode root, int pre) {
    if (root == null) return;
    pre = pre * 10 + root.val;
    if (root.left == null && root.right == null) {
        sum += pre;
        return;
    }
    helper(root.left, pre);
    helper(root.right, pre);
}
  1. 二叉搜索树的最近公共祖先 给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。百度百科中最近公共祖先的定义为:对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
// 该题求BST的最近公共祖先,难度显著降低,BST有明显的特点
// 遍历树,如果p,q都在左(右)子树,那么就从左(右)子树进行递归,否则就找到了LCA
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
    if (root == null) return null;
    if (p.val < root.val && q.val < root.val) return lowestCommonAncestor(root.left, p, q);
    if (p.val > root.val && q.val > root.val) return lowestCommonAncestor(root.right, p, q);
    return root;
}
  1. 二叉树的最近公共祖先 本题比235稍微难一些,235题可以通过数值的大小判断左右子树,该题不是BST不行。
// 递归:对每个节点对应的子树,若该子树不含有p或q,返回nullptr;
// 否则,如果p和q分别位于当前子树根节点两侧,则返回当前节点,
// 否则(p和q在同一侧,或者只有某一侧有p或q)返回来自左边或右边的LCA。
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
    if (root == null || p == root || q == root) return root;
    // 左边存在p或者q
    TreeNode left = lowestCommonAncestor(root.left, p, q);
    // 右边存在p或者q
    TreeNode right = lowestCommonAncestor(root.right, p, q);
    // p,q分别位于两侧
    if (left != null && right != null) return root;
    return (left == null) ? right : left;
}

以下三个题是 使用全局变量,递归的时候更新,递归的返回值和最终的结果关系不大 124. 二叉树中的最大路径和 给定一个非空二叉树,返回其最大路径和。本题中,路径被定义为一条从树中任意节点出发,达到任意节点的序列。该路径至少包含一个节点,且不一定经过根节点。

private int maxSum;
public int maxPathSum(TreeNode root) {
    if (root == null) return 0;
    maxSum = root.val;
    arrowMaxPath(root);
    return maxSum;
}
private int arrowMaxPath(TreeNode root) {
    if (root == null) return 0;
    int left = arrowMaxPath(root.left);
    int right = arrowMaxPath(root.right);
    // 如何 累加值
    left = (left > 0 ? left + root.val : root.val);
    right = (right > 0 ? right + root.val : root.val);

    maxSum = Math.max(maxSum, left+right-root.val);
    return Math.max(left, right);
}
  1. 二叉树的直径 给定一棵二叉树,你需要计算它的直径长度。一棵二叉树的直径长度是任意两个结点路径长度中的最大值。这条路径可能穿过根结点。注意:两结点之间的路径长度是以它们之间边的数目表示。
int max;
public int diameterOfBinaryTree(TreeNode root) {
    max = 0;
    helper(root);
    return max;
}
private int helper(TreeNode root) {
    if (root == null) {
        return -1;
    }
    if (root.left == null && root.right == null) {
        return 0;
    }
    int left = helper(root.left) + 1;
    int right = helper(root.right) + 1;
    max = Math.max(max, left+right);
    return Math.max(left, right);
}
  1. 最长同值路径(本题是任意两个节点的路径) 本题和543几乎是同一个题目,给定一个二叉树,找到最长的路径,这个路径中的每个节点具有相同值。注意:这条路径可以经过也可以不经过根节点。两个节点之间的路径长度由它们之间的边数表示。
// 这种路径是 1.左子树最长同值路径(单箭头路径)  2. 右子树最长同值路径(单箭头路径) 的 最大值
private int longest = 0;
public int longestUnivaluePath(TreeNode root) {
    longest = 0;
    arrowPath(root);
    return longest;
}
private int arrowPath(TreeNode root) {
    if (root == null) return 0;
    int left = arrowPath(root.left);
    int right = arrowPath(root.right);
    int arrowLeft = 0, arrowRight = 0;
    if (root.left != null && root.left.val == root.val) arrowLeft = left + 1;
    if (root.right != null && root.right.val == root.val) arrowRight = right + 1;
    // 更新最终结果是双向的
    longest = Math.max(longest, arrowLeft + arrowRight);
    // 返回的是单向的
    return Math.max(arrowLeft, arrowRight);
}

剪枝

剪枝的要点就是让当前节点的cur.left=pruning(cur.left) pruning函数返回当前树的根节点

  1. 修剪二叉搜索树 给定一个二叉搜索树,同时给定最小边界L 和最大边界 R。通过修剪二叉搜索树,使得所有节点的值在[L, R]中 (R>=L) 。你可能需要改变树的根节点,所以结果应当返回修剪好的二叉搜索树的新的根节点。
// 重点在于如何剪枝,如何调整节点
// 参考思路:< L , 只保留二叉树的右子树
// > R, 只保留二叉树的左子树
public TreeNode trimBST(TreeNode root, int L, int R) {
    if (root == null) return null;
    // 调整节点(难点)
    // < L, 只保留二叉树的右子树(结果肯定在右边)
    if (root.val < L) return trimBST(root.right, L, R);
    
    // > R, 只保留二叉树的左子树(结果肯定在左边)
    if (root.val > R) return trimBST(root.left, L, R);
    
    root.left = trimBST(root.left, L, R);
    root.right = trimBST(root.right, L, R);
    return root;
}
  1. 二叉树剪枝 给定二叉树根结点 root ,此外树的每个结点的值要么是 0,要么是 1。返回移除了所有不包含 1 的子树的原二叉树。(节点 X 的子树为 X 本身,以及所有 X 的后代。)
// 判断是否含有1
private boolean hasOne(TreeNode root) {
    if (root == null) return false;
    if (root.val == 1) return true;
    return hasOne(root.left) || hasOne(root.right);
}
public TreeNode pruneTree(TreeNode root) {
    if (!hasOne(root)) return null;
    root.left = pruneTree(root.left);
    root.right = pruneTree(root.right);
    return root;
}
// 后续遍历写法
public TreeNode pruneTree2(TreeNode root) {
    if (root == null) return null;
    root.left = pruneTree(root.left);
    root.right = pruneTree(root.right);
    if (root.val == 0 && root.left == null && root.right == null)
        return null;
    return root;
}

二叉搜索树

二叉搜索树,最常考的性质就是中序遍历的递增,一般的做法是使用递归(中序遍历),然后使用全局变量保存pre节点,然后在中间的时候(中序遍历的时候)更新全局变量。

  1. 验证二叉搜索树 给定一个二叉树,判断其是否是一个有效的二叉搜索树, 空树为BST。
TreeNode pre = null;
public boolean isValidBST(TreeNode root) {
    if (root == null ) {
        return true;
    }
    if (!isValidBST(root.left)) {
        return false;
    }
    if (pre != null && root.val <= pre.val) {
        return false;
    } 
    pre = root;
    return isValidBST(root.right);
}
  1. 恢复二叉搜索树 二叉搜索树中的两个节点被错误地交换。请在不改变其结构的情况下,恢复这棵树。
// 难点:找到这两个错误的节点
// 有序的序列,交换两个元素,会导致增长序列出现两个(或者一个)下降点
// 两个下降点: first是第一个下降点处较大的元素;second是第二个下降点处较小的元素
// 一个下降点: first下降点处较大元素;second是下降点处较小元素
private TreeNode first = null;
private TreeNode second = null;
private TreeNode pre;
public void recoverTree(TreeNode root) {
    first = null;
    second = null;
    pre = null;
    traverse(root);
    // 交换 first 和 second
    int temp = first.val;
    first.val = second.val;
    second.val = temp;
}
private void traverse(TreeNode root) {
    if (root == null) return;
    traverse(root.left);
    if (pre != null && pre.val > root.val) {
        // 此处是重点
        if (first == null) {
            first = pre;
            second = root;  // 注意此处(只有一个下降点时)
        } else second = root;
    }
    pre = root;
    traverse(root.right);
}
  1. 将有序数组转换为二叉搜索树 将一个按照升序排列的有序数组,转换为一棵高度平衡二叉搜索树。本题中,一个高度平衡二叉树是指一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1。
// 树尽量平衡 —— 要求数组尽量划分均等(二分)(使用下标进行划分,注意数目的奇偶)
// 0 1 2 3 4 5
public TreeNode sortedArrayToBST(int[] nums) {
    TreeNode root = helper(nums, 0, nums.length-1);
    return root;
}
private TreeNode helper(int[] nums, int i, int j) {
    if (i < 0 || j >= nums.length ||  i > j) return null;
    int mid = (i+j)/2;
    TreeNode node = new TreeNode(nums[mid]);
    node.left = helper(nums, i, mid-1);
    node.right = helper(nums, mid+1, j);
    return node;
}
  1. 将有序链表转换为二叉搜索树
public TreeNode sortedListToBST(ListNode head) {
    // while (tail.next != null) tail = tail.next;
    return helper(head, null);
}
private TreeNode helper(ListNode head, ListNode tail) {  // 左闭右开
    if (head == tail) return null;
    if (head.next == tail) return new TreeNode(head.val);
    ListNode slow = head, fast = head;
    
    while (fast != tail && fast.next != tail) {
        slow = slow.next;
        fast = fast.next.next;
    }
    TreeNode root = new TreeNode(slow.val);
    root.left = helper(head, slow); 
    root.right = helper(slow.next, tail);
    return root;
}
  1. 二叉搜索树中第K小的元素 中序遍历
int count = 0;
public int kthSmallest(TreeNode root, int k) {
    if (root == null) {
        return -1;
    }
    int left = kthSmallest(root.left, k);
    if (left != -1) {
        return left;
    }
    count++;
    if (count == k) {
        return root.val;
    }
    return kthSmallest(root.right, k);
}
  1. 删除二叉搜索树中的节点 给定一个二叉搜索树的根节点 root 和一个值 key,删除二叉搜索树中的 key 对应的节点,并保证二叉搜索树的性质不变,返回二叉搜索树(有可能被更新)的根节点的引用。
public TreeNode deleteNode(TreeNode root, int key) {
    if (root == null) {
        return null;
    }
    if (root.val != key) {
        if (root.val > key) {
            root.left = deleteNode(root.left, key);
        } else {
            root.right = deleteNode(root.right, key);
        }
    } else {
        if (root.left == null) {
            return root.right;
        }
        if (root.right == null) {
            return root.left;
        }
        // 难点
        TreeNode rightMaxNode = findRightMax(root);  // 找到右侧最大值
        root.val = rightMaxNode.val;  // 与当前值交换
        rightMaxNode.val = key;
        root.right = deleteNode(root.right, key);  // 在右侧递归
    }
    return root;
}
private TreeNode findRightMax(TreeNode root) {
    TreeNode p = root.right;
    while (p != null && p.left != null) {
        p = p.left;
    }
    return p;
}
  1. 二叉搜索树中的插入操作
public TreeNode insertIntoBST(TreeNode root, int val) {
    if (root == null) return new TreeNode(val);
    if (root.val > val) {
        root.left = insertIntoBST(root.left, val);
    } else {
        root.right = insertIntoBST(root.right, val);
    }
    return root;
}

动态规划

单序列DP

子序列/子串问题

子序列问题一般是转化成双序列问题(二维)来解决的,只不过习惯于使用滚动数组进行状态压缩而已

经典股票题

其实股票题目是一个多状态的DP问题

多状态的DP

  • 经典的打家劫舍
  • 铺地板问题

未分类

经典题

  • 爬楼梯
  • 单词拆分

双序列DP

  • 正则表达式匹配
  • 通配符匹配
  • 编辑距离
  • 最长公共子序列

棋盘类DP

背包问题

游戏(博弈)DP

划分型DP

About

Algorithms Exercise: LeetCode Problems, LeetCode Weekly Contest etc.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published