有不含有重复数字的数组构造二叉树_LeetCode: Algorithms Exercise: LeetCode Problems, LeetCode Weekly Contest etc....

LeetCode总结

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

目录

搜索(回溯、BFS、DFS):

回溯:数独、N皇后、37、51、79、93、[212、301]

BFS:矩阵、单词变换

排列、组合、分割、子集:四大类问题,常用回溯、DFS解决

图的搜索:DFS、BFS、并查集、Flood

并查集(TODO)

二分查找:

g 函数,利用边界

K th 问题

旋转数组

双指针:

左右指针:数组(或字符串)问题,二分查找也算是双指针,三数之和,Sunday算法

快慢指针:链表中环的问题

滑动窗口:更新窗口

链表:

链表的基本操作

旋转(K组旋转,奇偶旋转)、拆分

归并

判断环(快慢指针)

二叉树:

遍历

深度、层次

树的结构相关

树的路径相关,递归的过程之中不断更新全局变量

剪枝

二叉搜索树

数学:

概率:洗牌算法、蓄水池抽样、蒙特卡洛

数论:素数,最小公倍数,最大公约数

位运算:异或,与的巧妙用法

特殊的数:有效数字(状态机),第n个丑数,平方数(DP解法),回文数

数字的转化:溢出检测、模拟运算(时刻注意溢出)、罗马、字符转成数字;分数转小数

其他:Pow()快速幂算法,众数投票法

动态规划

排序

数据结构

单调栈

单调队列

分治

贪心

心法宝典

递归要素:开头-判断边界(退出条件);中间-进行相关的计算、缩小范围递归(经常用到全局变量哦);结尾-返回值(还要学会如何利用返回值)

反转链表:迭代写法返回prev,递归写法每次考虑两个节点(返回值是前递归的返回值)

采样:n个数随机采样m个,knuth采样:对于每个下标[0,n) 每次n--, 若rand()%n < m时才m--

Shuffle:knuth Shuffle,i从后往前,每次从[0,i]选择一个位置与i交换

DP: DP 就是填表法,首先明确初始值(并且要明确这个值是什么值),然后要明确填表的顺序,最后是如何填(状态转移方程)。

使用移位运算一定要注意运算符的优先级

双指针

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

1. 序列

无重复字符的最长子串

给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。注意区分子串与子序列区别

// [i, j), 无重复的时候j++, 有重复的时候i++

public int lengthOfLongestSubstring(String s) {

Set 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;

}

盛最多水的容器

不能倾斜容器,且 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;

}

三数之和

给定一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a, b, c, 使得 a + b + c = 0 ?找出所有满足条件且不重复的三元组。

// 难点: 三数之和 -> 两数之和(哈希、排序+双指针); 排除重复

// 排序+双指针 : valtarget时 high--; val==target时low++ & high--

// 如何跳过重复

public List> threeSum(int[] nums) {

List> 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;

}

最接近的三数之和

给定一个包括 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;

}

四数之和

给定一个包含 n 个整数的数组 nums 和一个目标值 target,判断 nums 中是否存在四个元素 a,b,c 和 d , 使得 a + b + c + d 的值与 target 相等? 找出所有满足条件且不重复的四元组。和第15题一个思路。

// 双指针 + 去重

public List> fourSum(int[] nums, int target) {

List> 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;

}

删除排序数组的重复项

不要使用额外的数组空间,你必须在原地修改输入数组并在使用 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;

}

移除元素

原地移除元素,分很多种情况。

// 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;

}

模式匹配

不要求掌握较难的KMP算法,掌握Sunday算法。

// Sunday算法:如果当前窗口不匹配,比较窗口下一个字符串;下一个字符串在shift数组中的位置,就是窗口要偏移的距离

// 先计算shift数组 every char : len(needle) - max(char) otherwise: len+1

public int strStr(String haystack, String needle) {

// 使用 HashMap 实现shift数组

HashMap 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;

}

颜色分类

给定一个包含红色、白色和蓝色,一共 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++;

}

}

合并两个有序数组

给定两个有序整数数组 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--];

}

}

搜索二维矩阵 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;

}

移动零

给定一个数组 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. 快慢指针

环形链表

给一个链表判断链表是否有环,你能用 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;

}

环形链表 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;

}

相交链表

编写一个程序,找到两个单链表相交的起始节点:如果两个链表没有交点,返回 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;

}

回文链表

请判断一个链表是否为回文链表。用 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. 滑动窗口

长度最小的子数组(双指针滑动窗口)

给定一个含有 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

}

爱生气的书店老板

书店老板有一家店打算试营业 customers.length 分钟。每分钟都有一些顾客(customers[i])会进入书店,所有这些顾客都会在那一分钟结束后离开。在某些时候,书店老板会生气。 如果书店老板在第 i 分钟生气,那么 grumpy[i] = 1,否则 grumpy[i] = 0。 当书店老板生气时,那一分钟的顾客就会不满意,不生气则他们是满意的。书店老板知道一个秘密技巧,能抑制自己的情绪,可以让自己连续 X 分钟不生气,但却只能使用一次。请你返回这一天营业下来,最多有多少客户能够感到满意的数量。

TODO

https://leetcode-cn.com/problems/grumpy-bookstore-owner/

链表

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

基本操作

两数相加

给出两个 非空 的链表用来表示两个非负的整数。其中,它们各自的位数是按照 逆序 的方式存储的,并且它们的每个节点只能存储 一位 数字。如果,我们将这两个数相加起来,则会返回一个新的链表来表示它们的和。

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;

}

对链表进行插入操作

// 优化

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;

}

两数相加2

链表不是逆序方式存储的,该如何去做。

// 不修改输入链表

// 难点:1. 预先不只是位数 2. 进位不清楚 3. 计算顺序是从后往前的

// 使用栈 时间和空间复杂度 O(m+n)

public ListNode addTwoNumbers(ListNode l1, ListNode l2) {

Stack stack1 = new Stack<>();

Stack 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;

}

删除

删除链表的倒数第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;

}

删除排序链表中的重复元素

给定一个排序链表,删除所有含有重复数字的节点,只保留原始链表中 没有重复出现 的数字。

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;

}

删除排序链表中的重复元素 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;

}

移除链表元素

删除链表中等于给定值 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->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;

}

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;

}

旋转链表

给定一个链表,旋转链表,将链表每个节点向右移动 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;

}

反转链表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;

}

反转链表(使用迭代 和 递归)

反转一个单链表。

// 迭代(循环版本)

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:使用 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;

}

奇偶链表(链表的拆分)

给定一个单链表,把所有的奇数节点和偶数节点分别排在一起。请注意,这里的奇数节点和偶数节点指的是节点编号的奇偶性,而不是节点的值的奇偶性。

请尝试使用原地算法完成。你的算法的空间复杂度应为 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;

}

归并

合并两个有序链表(循环写法和递归写法)

将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

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;

}

}

合并K个排序链表(使用堆和不使用堆)

合并 k 个排序链表,返回合并后的排序链表。请分析和描述算法的复杂度。

// 使用堆

public ListNode mergeKLists(ListNode[] lists) {

int k = lists.length;

if (k == 0) return null;

PriorityQueue priorityQueue = new PriorityQueue(k, new Comparator() {

@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;

}

}

排序链表(归并排序)

在 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 ret) {

if (root == null) return;

inorder(root.left, ret);

ret.add(root.val);

inorder(root.right, ret);

}

迭代写法:

前序

List ret = new ArrayList<>();

Stack 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 ret = new ArrayList<>();

Stack 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 ret = new LinkedList<>();

Stack 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;

根据遍历序列构造树

前中(唯一)

假设树中没有重复的元素,根据一棵树的前序遍历与中序遍历构造二叉树。

// 难点:如何在数组上分区域

// 方案:递归的在子数组上进行操作

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;

}

中后(唯一)

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

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;

}

前后(不唯一)

将二叉搜索树转化为排序的双向链表

剑指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);

}

深度

二叉树的最大深度

给定一个二叉树,找出其最大深度。二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。说明: 叶子节点是指没有子节点的节点。

// 递归思路:二叉树的最大深度 = 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

// 在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;

}

二叉树的最小深度

// 难点:当二叉树退化成单侧时

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;

}

层序

二叉树的层次遍历(广度优先遍历)

给定一个二叉树,返回其按层次遍历的节点值。 (即逐层地, 每一层都放到一个链表内,从左到右访问所有节点)。

// 使用一个队列,下一层元素数目等于当前队列中元素的数目

public List> levelOrder(TreeNode root) {

List> ret = new ArrayList<>();

if (root == null) return ret;

Queue queue = new LinkedList<>();

queue.add(root);

while (!queue.isEmpty()) {

int nodeCount = queue.size(); // Key

List 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> 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> levelOrderRecursive(TreeNode root) {

List> res = new LinkedList<>();

helper(root, 0, res);

return res;

}

二叉树的层次遍历 II

给定一个二叉树,返回其节点值自底向上的层次遍历。 (即按从叶子节点所在层到根节点所在的层,逐层从左向右遍历)和102题一模一样,只不过本题需要将(层次)结果翻转一下,使用链表的头插,实现结果的翻转。

结构

相同的树

给定两个二叉树,编写一个函数来检验它们是否相同。如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。

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,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);

}

反转二叉树

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;

}

另一个树的子树

给定两个非空二叉树 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);

}

路径

路径和

判断树中是否存在一个路径,和为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);

}

路径和2

// 难点:如何保存结果,回溯法!!!

private void findPath(TreeNode root, int sum, LinkedList path, List> 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> pathSum(TreeNode root, int sum) {

LinkedList path = new LinkedList();

List> res = new ArrayList<>();

findPath(root, sum, path, res);

return res;

}

求根到叶子节点数字之和

给定一个二叉树,它的每个结点都存放一个 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);

}

二叉搜索树的最近公共祖先

给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。百度百科中最近公共祖先的定义为:对于有根树 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;

}

二叉树的最近公共祖先

本题比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);

}

二叉树的直径

给定一棵二叉树,你需要计算它的直径长度。一棵二叉树的直径长度是任意两个结点路径长度中的最大值。这条路径可能穿过根结点。注意:两结点之间的路径长度是以它们之间边的数目表示。

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);

}

最长同值路径(本题是任意两个节点的路径)

本题和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函数返回当前树的根节点

修剪二叉搜索树

给定一个二叉搜索树,同时给定最小边界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;

}

二叉树剪枝

给定二叉树根结点 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节点,然后在中间的时候(中序遍历的时候)更新全局变量。

验证二叉搜索树

给定一个二叉树,判断其是否是一个有效的二叉搜索树, 空树为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);

}

恢复二叉搜索树

二叉搜索树中的两个节点被错误地交换。请在不改变其结构的情况下,恢复这棵树。

// 难点:找到这两个错误的节点

// 有序的序列,交换两个元素,会导致增长序列出现两个(或者一个)下降点

// 两个下降点: 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。

// 树尽量平衡 —— 要求数组尽量划分均等(二分)(使用下标进行划分,注意数目的奇偶)

// 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;

}

将有序链表转换为二叉搜索树

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;

}

二叉搜索树中第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);

}

删除二叉搜索树中的节点

给定一个二叉搜索树的根节点 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;

}

二叉搜索树中的插入操作

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

你可能感兴趣的:(有不含有重复数字的数组构造二叉树_LeetCode: Algorithms Exercise: LeetCode Problems, LeetCode Weekly Contest etc....)