用函数 f ( n ) f(n) f(n) 表示算法效率与数据规模的关系,假设每次解决问题需要 1 微秒( 1 0 − 6 10^{-6} 10−6 秒),进行估算:
参考解答
要点:减而治之,可以用递归或非递归实现
给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1
例如
输入: nums = [-1,0,3,5,9,12], target = 9
输出: 4
解释: 9 出现在 nums 中并且下标为 4
输入: nums = [-1,0,3,5,9,12], target = 2
输出: -1
解释: 2 不存在 nums 中因此返回 -1
参考答案:略,可以用讲过的任意一种二分求解
要点:理解谁代表插入位置
给定一个排序数组和一个目标值
例如
输入: nums = [1,3,5,6], target = 5
输出: 2
输入: nums = [1,3,5,6], target = 2
输出: 1
输入: nums = [1,3,5,6], target = 7
输出: 4
参考答案1:用二分查找基础版代码改写,基础版中,找到返回 m,没找到 i 代表插入点,因此有
public int searchInsert(int[] a, int target) {
int i = 0, j = a.length - 1;
while (i <= j) {
int m = (i + j) >>> 1;
if (target < a[m]) {
j = m - 1;
} else if (a[m] < target) {
i = m + 1;
} else {
return m;
}
}
return i; // 原始 return -1
}
参考答案2:用二分查找平衡版改写,平衡版中
public static int searchInsert(int[] a, int target) {
int i = 0, j = a.length;
while (1 < j - i) {
int m = (i + j) >>> 1;
if (target < a[m]) {
j = m;
} else {
i = m;
}
}
return (target <= a[i]) ? i : i + 1;
// 原始 (target == a[i]) ? i : -1;
}
参考答案3:用 leftmost 版本解,返回值即为插入位置(并能处理元素重复的情况)
public int searchInsert(int[] a, int target) {
int i = 0, j = a.length - 1;
while(i <= j) {
int m = (i + j) >>> 1;
if(target <= a[m]) {
j = m - 1;
} else {
i = m + 1;
}
}
return i;
}
给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target,返回 [-1, -1]。
你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题
例如
输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]
输入:nums = [5,7,7,8,8,10], target = 6
输出:[-1,-1]
输入:nums = [], target = 0
输出:[-1,-1]
参考答案
public static int left(int[] a, int target) {
int i = 0, j = a.length - 1;
int candidate = -1;
while (i <= j) {
int m = (i + j) >>> 1;
if (target < a[m]) {
j = m - 1;
} else if (a[m] < target) {
i = m + 1;
} else {
candidate = m;
j = m - 1;
}
}
return candidate;
}
public static int right(int[] a, int target) {
int i = 0, j = a.length - 1;
int candidate = -1;
while (i <= j) {
int m = (i + j) >>> 1;
if (target < a[m]) {
j = m - 1;
} else if (a[m] < target) {
i = m + 1;
} else {
candidate = m;
i = m + 1;
}
}
return candidate;
}
public static int[] searchRange(int[] nums, int target) {
int x = left(nums, target);
if(x == -1) {
return new int[] {-1, -1};
} else {
return new int[] {x, right(nums, target)};
}
}
public static int binarySearch(int[] a, int target) {
return recursion(a, target, 0, a.length - 1);
}
public static int recursion(int[] a, int target, int i, int j) {
if (i > j) {
return -1;
}
int m = (i + j) >>> 1;
if (target < a[m]) {
return recursion(a, target, i, m - 1);
} else if (a[m] < target) {
return recursion(a, target, m + 1, j);
} else {
return m;
}
}
public static void main(String[] args) {
int[] a = {3, 2, 6, 1, 5, 4, 7};
bubble(a, 0, a.length - 1);
System.out.println(Arrays.toString(a));
}
private static void bubble(int[] a, int low, int high) {
if(low == high) {
return;
}
int j = low;
for (int i = low; i < high; i++) {
if (a[i] > a[i + 1]) {
swap(a, i, i + 1);
j = i;
}
}
bubble(a, low, j);
}
private static void swap(int[] a, int i, int j) {
int t = a[i];
a[i] = a[j];
a[j] = t;
}
public static void main(String[] args) {
int[] a = {3, 2, 6, 1, 5, 7, 4};
insertion(a, 1, a.length - 1);
System.out.println(Arrays.toString(a));
}
private static void insertion(int[] a, int low, int high) {
if (low > high) {
return;
}
int i = low - 1;
int t = a[low];
while (i >= 0 && a[i] > i) {
a[i + 1] = a[i];
i--;
}
if(i + 1 != low) {
a[i + 1] = t;
}
insertion(a, low + 1, high);
}
n n n 个人排成圆圈,从头开始报数,每次数到第 m m m 个人( m m m 从 1 1 1 开始)杀之,继续从下一个人重复以上过程,求最后活下来的人是谁?
方法1
根据最后的存活者 a 倒推出它在上一轮的索引号
f(n,m) | 本轮索引 | 为了让 a 是这个索引,上一轮应当这样排 | 规律 |
---|---|---|---|
f(1,3) | 0 | x x x a | (0 + 3) % 2 |
f(2,3) | 1 | x x x 0 a | (1 + 3) % 3 |
f(3,3) | 1 | x x x 0 a | (1 + 3) % 4 |
f(4,3) | 0 | x x x a | (0 + 3) % 5 |
f(5,3) | 3 | x x x 0 1 2 a | (3 + 3) % 6 |
f(6,3) | 0 | x x x a |
方法2
设 n 为总人数,m 为报数次数,解返回的是这些人的索引,从0开始
f(n, m) | 解 | 规律 |
---|---|---|
f(1, 3) | 0 | |
f(2, 3) | 0 1 => 1 | 3%2=1 |
f(3, 3) | 0 1 2 => 0 1 | 3%3=0 |
f(4, 3) | 0 1 2 3 => 3 0 1 | 3%4=3 |
f(5, 3) | 0 1 2 3 4 => 3 4 0 1 | 3%5=3 |
f(6, 3) | 0 1 2 3 4 5 => 3 4 5 0 1 | 3%6=3 |
一. 找出等价函数
规律:下次报数的起点为 k = m % n k = m \% n k=m%n
这个函数称之为 g ( n − 1 , m ) g(n-1,m) g(n−1,m),它的最终结果与 f ( n , m ) f(n,m) f(n,m) 是相同的。
二. 找到映射函数
现在想办法找到 g ( n − 1 , m ) g(n-1,m) g(n−1,m) 与 f ( n − 1 , m ) f(n-1, m) f(n−1,m) 的对应关系,即
3 → 0 4 → 1 5 → 2 0 → 3 1 → 4 3 \rightarrow 0 \\ 4 \rightarrow 1 \\ 5 \rightarrow 2 \\ 0 \rightarrow 3 \\ 1 \rightarrow 4 \\ 3→04→15→20→31→4
映射函数为
m a p p i n g ( x ) = { x − k x = [ k . . n − 1 ] x + n − k x = [ 0.. k − 2 ] mapping(x) = \begin{cases} x-k & x=[k..n-1] \\ x+n-k & x=[0..k-2] \end{cases} mapping(x)={x−kx+n−kx=[k..n−1]x=[0..k−2]
等价于下面函数
m a p p i n g ( x ) = ( x + n − k ) % n mapping(x) = (x + n - k)\%{n} mapping(x)=(x+n−k)%n
代入测试一下
3 → ( 3 + 6 − 3 ) % 6 → 0 4 → ( 4 + 6 − 3 ) % 6 → 1 5 → ( 5 + 6 − 3 ) % 6 → 2 0 → ( 0 + 6 − 3 ) % 6 → 3 1 → ( 1 + 6 − 3 ) % 6 → 4 3 \rightarrow (3+6-3)\%6 \rightarrow 0 \\ 4 \rightarrow (4+6-3)\%6 \rightarrow 1 \\ 5 \rightarrow (5+6-3)\%6 \rightarrow 2 \\ 0 \rightarrow (0+6-3)\%6 \rightarrow 3 \\ 1 \rightarrow (1+6-3)\%6 \rightarrow 4 \\ 3→(3+6−3)%6→04→(4+6−3)%6→15→(5+6−3)%6→20→(0+6−3)%6→31→(1+6−3)%6→4
综上有
f ( n − 1 , m ) = m a p p i n g ( g ( n − 1 , m ) ) f(n-1,m) = mapping(g(n-1,m)) f(n−1,m)=mapping(g(n−1,m))
三. 求逆映射函数
映射函数是根据 x 计算 y,逆映射函数即根据 y 得到 x
m a p p i n g − 1 ( x ) = ( x + k ) % n mapping^{-1}(x) = (x + k)\%n mapping−1(x)=(x+k)%n
代入测试一下
0 → ( 0 + 3 ) % 6 → 3 1 → ( 1 + 3 ) % 6 → 4 2 → ( 2 + 3 ) % 6 → 5 3 → ( 3 + 3 ) % 6 → 0 4 → ( 4 + 3 ) % 6 → 1 0 \rightarrow (0+3)\%6 \rightarrow 3 \\ 1 \rightarrow (1+3)\%6 \rightarrow 4 \\ 2 \rightarrow (2+3)\%6 \rightarrow 5 \\ 3 \rightarrow (3+3)\%6 \rightarrow 0 \\ 4 \rightarrow (4+3)\%6 \rightarrow 1 \\ 0→(0+3)%6→31→(1+3)%6→42→(2+3)%6→53→(3+3)%6→04→(4+3)%6→1
因此可以求得
g ( n − 1 , m ) = m a p p i n g − 1 ( f ( n − 1 , m ) ) g(n-1,m) = mapping^{-1}(f(n-1,m)) g(n−1,m)=mapping−1(f(n−1,m))
四. 递推式
代入推导
f ( n , m ) = g ( n − 1 , m ) = m a p p i n g − 1 ( f ( n − 1 , m ) ) = ( f ( n − 1 , m ) + k ) % n = ( f ( n − 1 , m ) + m % n ) % n = ( f ( n − 1 , m ) + m ) % n \begin{aligned} f(n,m) = \ & g(n-1,m) \\ = \ & mapping^{-1}(f(n-1,m)) \\ = \ & (f(n-1,m) + k) \% n \\ = \ & (f(n-1,m) + m\%n) \% n \\ = \ & (f(n-1,m) + m) \% n \\ \end{aligned} f(n,m)= = = = = g(n−1,m)mapping−1(f(n−1,m))(f(n−1,m)+k)%n(f(n−1,m)+m%n)%n(f(n−1,m)+m)%n
最后一步化简是利用了模运算法则
( a + b ) % n = ( a % n + b % n ) % n (a+b)\%n = (a\%n + b\%n) \%n (a+b)%n=(a%n+b%n)%n 例如
最终递推式
f ( n , m ) = { ( f ( n − 1 , m ) + m ) % n n > 1 0 n = 1 f(n,m) = \begin{cases} (f(n-1,m) + m) \% n & n>1\\ 0 & n = 1 \end{cases} f(n,m)={(f(n−1,m)+m)%n0n>1n=1
Tower of Hanoi,是一个源于印度古老传说:大梵天创建世界时做了三根金刚石柱,在一根柱子从下往上按大小顺序摞着 64 片黄金圆盘,大梵天命令婆罗门把圆盘重新摆放在另一根柱子上,并且规定
下面的动图演示了4片圆盘的移动方法
使用程序代码模拟圆盘的移动过程,并估算出时间复杂度
思路
假设每根柱子标号 a,b,c,每个圆盘用 1,2,3 … 表示其大小,圆盘初始在 a,要移动到的目标是 c
如果只有一个圆盘,此时是最小问题,可以直接求解
如果有两个圆盘,那么
如果有三个圆盘,那么
如果有四个圆盘,那么
题解
public class E02HanoiTower {
/*
源 借 目
h(4, a, b, c) -> h(3, a, c, b)
a -> c
h(3, b, a, c)
*/
static LinkedList<Integer> a = new LinkedList<>();
static LinkedList<Integer> b = new LinkedList<>();
static LinkedList<Integer> c = new LinkedList<>();
static void init(int n) {
for (int i = n; i >= 1; i--) {
a.add(i);
}
}
static void h(int n, LinkedList<Integer> a,
LinkedList<Integer> b,
LinkedList<Integer> c) {
if (n == 0) {
return;
}
h(n - 1, a, c, b);
c.addLast(a.removeLast());
print();
h(n - 1, b, a, c);
}
private static void print() {
System.out.println("-----------------------");
System.out.println(a);
System.out.println(b);
System.out.println(c);
}
public static void main(String[] args) {
init(3);
print();
h(3, a, b, c);
}
}
分析
把它斜着看
1
1 1
1 2 1
1 3 3 1
1 4 6 4 1
题解
public static void print(int n) {
for (int i = 0; i < n; i++) {
if (i < n - 1) {
System.out.printf("%" + 2 * (n - 1 - i) + "s", " ");
}
for (int j = 0; j < i + 1; j++) {
System.out.printf("%-4d", element(i, j));
}
System.out.println();
}
}
public static int element(int i, int j) {
if (j == 0 || i == j) {
return 1;
}
return element(i - 1, j - 1) + element(i - 1, j);
}
优化1
是 multiple recursion,因此很多递归调用是重复的,例如
这里 recursion(2, 1) 就重复调用了,事实上它会重复很多次,可以用 static AtomicInteger counter = new AtomicInteger(0) 来查看递归函数的调用总次数
事实上,可以用 memoization 来进行优化:
public static void print1(int n) {
int[][] triangle = new int[n][];
for (int i = 0; i < n; i++) {
// 打印空格
triangle[i] = new int[i + 1];
for (int j = 0; j <= i; j++) {
System.out.printf("%-4d", element1(triangle, i, j));
}
System.out.println();
}
}
public static int element1(int[][] triangle, int i, int j) {
if (triangle[i][j] > 0) {
return triangle[i][j];
}
if (j == 0 || i == j) {
triangle[i][j] = 1;
return triangle[i][j];
}
triangle[i][j] = element1(triangle, i - 1, j - 1) + element1(triangle, i - 1, j);
return triangle[i][j];
}
优化2
public static void print2(int n) {
int[] row = new int[n];
for (int i = 0; i < n; i++) {
// 打印空格
createRow(row, i);
for (int j = 0; j <= i; j++) {
System.out.printf("%-4d", row[j]);
}
System.out.println();
}
}
private static void createRow(int[] row, int i) {
if (i == 0) {
row[0] = 1;
return;
}
for (int j = i; j > 0; j--) {
row[j] = row[j - 1] + row[j];
}
}
注意:还可以通过每一行的前一项计算出下一项,不必借助上一行,这与杨辉三角的另一个特性有关,暂不展开了
力扣对应题目,但递归不适合在力扣刷高分,因此只列出相关题目,不做刷题讲解了
对应力扣题目 206. 反转链表 - 力扣(LeetCode)
输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]
输入:[1,2]
输出:[2,1]
输入:[]
输出:[]
方法1
构造一个新链表,从旧链表依次拿到每个节点,创建新节点添加至新链表头部,完成后新链表即是倒序的
public ListNode reverseList(ListNode o1) {
ListNode n1 = null;
ListNode p = o1;
while (p != null) {
n1 = new ListNode(p.val, n1);
p = p.next;
}
return n1;
}
评价:简单直白,就是得新创建节点对象
方法2
与方法1 类似,构造一个新链表,从旧链表头部移除节点,添加到新链表头部,完成后新链表即是倒序的,区别在于原题目未提供节点外层的容器类,这里提供一个,另外一个区别是并不去构造新节点
static class List {
ListNode head;
public List(ListNode head) {
this.head = head;
}
public ListNode removeFirst(){
ListNode first = head;
if (first != null) {
head = first.next;
}
return first;
}
public void addFirst(ListNode first) {
first.next = head;
head = first;
}
}
代码
public ListNode reverseList(ListNode head) {
List list1 = new List(head);
List list2 = new List(null);
ListNode first;
while ((first = list1.removeFirst()) != null) {
list2.addFirst(first);
}
return list2.head;
}
评价:更加面向对象,如果实际写代码而非刷题,更多会这么做
方法3
递归,在归时让 5 → 4 5 \rightarrow 4 5→4, 4 → 3 4 \rightarrow 3 4→3 …
首先,写一个递归方法,返回值用来拿到最后一个节点
public ListNode reverseList(ListNode p) {
if (p == null || p.next == null) { // 不足两个节点
return p; // 最后一个节点
}
ListNode last = reverseList(p.next);
return last;
}
可以先测试一下
ListNode o5 = new ListNode(5, null);
ListNode o4 = new ListNode(4, o5);
ListNode o3 = new ListNode(3, o4);
ListNode o2 = new ListNode(2, o3);
ListNode o1 = new ListNode(1, o2);
ListNode n1 = new E01Leetcode206().reverseList(o1);
System.out.println(n1);
会打印
[5]
下面为伪码调用过程,假设节点分别是 1 → 2 → 3 → 4 → 5 → n u l l 1 \rightarrow 2 \rightarrow 3 \rightarrow 4 \rightarrow 5 \rightarrow null 1→2→3→4→5→null,先忽略返回值
reverseList(ListNode p = 1) {
reverseList(ListNode p = 2) {
reverseList(ListNode p = 3) {
reverseList(ListNode p = 4) {
reverseList(ListNode p = 5) {
if (p == null || p.next == null) {
return p; // 返回5
}
}
// 此时p是4, p.next是5
}
// 此时p是3, p.next是4
}
// 此时p是2, p.next是3
}
// 此时p是1, p.next是2
}
接下来,从 p = 4 开始,要让 5 → 4 5 \rightarrow 4 5→4, 4 → 3 4 \rightarrow 3 4→3 …
reverseList(ListNode p = 1) {
reverseList(ListNode p = 2) {
reverseList(ListNode p = 3) {
reverseList(ListNode p = 4) {
reverseList(ListNode p = 5) {
if (p == null || p.next == null) {
return p; // 返回5
}
}
// 此时p是4, p.next是5, 要让5指向4,代码写成 p.next.next=p
// 还要注意4要指向 null, 否则就死链了
}
// 此时p是3, p.next是4
}
// 此时p是2, p.next是3
}
// 此时p是1, p.next是2
}
最终代码为:
public ListNode reverseList(ListNode p) {
if (p == null || p.next == null) { // 不足两个节点
return p; // 最后一个节点
}
ListNode last = reverseList(p.next);
p.next.next = p;
p.next = null;
return last;
}
Q:为啥不能在递的过程中倒序?
A:比如
评价:单向链表没有 prev 指针,但利用递归的特性【记住了】链表每次调用时相邻两个节点是谁
方法4
从链表每次拿到第二个节点,将其从链表断开,插入头部,直至它为 null 结束
n 1 o 1 1 → o 2 2 → 3 → 4 → 5 → n u l l \frac{n1 \ o1}{1} \rightarrow \frac{o2}{2} \rightarrow 3 \rightarrow 4 \rightarrow 5 \rightarrow null 1n1 o1→2o2→3→4→5→null
$ \frac{n1 \ o1}{1} \rightarrow 3 \rightarrow 4 \rightarrow 5 \rightarrow null$ , o 2 2 \frac{o2}{2} 2o2
o 2 2 → n 1 o 1 1 → 3 → 4 → 5 → n u l l \frac{o2}{2} \rightarrow \frac{n1 \ o1}{1} \rightarrow 3 \rightarrow 4 \rightarrow 5 \rightarrow null 2o2→1n1 o1→3→4→5→null
n 1 o 2 2 → o 1 1 → 3 → 4 → 5 → n u l l \frac{n1 \ o2}{2} \rightarrow \frac{o1}{1} \rightarrow 3 \rightarrow 4 \rightarrow 5 \rightarrow null 2n1 o2→1o1→3→4→5→null
n 1 2 → o 1 1 → o 2 3 → 4 → 5 → n u l l \frac{n1}{2} \rightarrow \frac{o1}{1} \rightarrow \frac{o2}{3} \rightarrow 4 \rightarrow 5 \rightarrow null 2n1→1o1→3o2→4→5→null
重复以上 2 ∼ 5 2\sim5 2∼5 步,直到 o2 指向 null
还应当考虑边界条件,即链表中不满两个元素时,无需走以上逻辑
参考答案
public ListNode reverseList(ListNode o1) {
if (o1 == null || o1.next == null) { // 不足两个节点
return o1;
}
ListNode o2 = o1.next;
ListNode n1 = o1;
while (o2 != null) {
o1.next = o2.next;
o2.next = n1;
n1 = o2;
o2 = o1.next;
}
return n1;
}
方法5
要点:把链表分成两部分,思路就是不断从链表2的头,往链表1的头搬移
n 1 n u l l \frac{n1}{null} nulln1, o 1 1 → 2 → 3 → 4 → 5 → n u l l \frac{o1}{1} \rightarrow 2 \rightarrow 3 \rightarrow 4 \rightarrow 5 \rightarrow null 1o1→2→3→4→5→null
n 1 n u l l \frac{n1}{null} nulln1, o 1 1 → o 2 2 → 3 → 4 → 5 → n u l l \frac{o1}{1} \rightarrow \frac{o2}{2} \rightarrow 3 \rightarrow 4 \rightarrow 5 \rightarrow null 1o1→2o2→3→4→5→null
o 1 1 → n 1 n u l l \frac{o1}{1} \rightarrow \frac{n1}{null} 1o1→nulln1 , o 2 2 → 3 → 4 → 5 → n u l l \frac{o2}{2} \rightarrow 3 \rightarrow 4 \rightarrow 5 \rightarrow null 2o2→3→4→5→null
n 1 1 → n u l l \frac{n1}{1} \rightarrow null 1n1→null , o 1 o 2 2 → 3 → 4 → 5 → n u l l \frac{o1 \ o2}{2} \rightarrow 3 \rightarrow 4 \rightarrow 5 \rightarrow null 2o1 o2→3→4→5→null
参考答案
public ListNode reverseList(ListNode o1) {
if (o1 == null || o1.next == null) {
return o1;
}
ListNode n1 = null;
while (o1 != null) {
ListNode o2 = o1.next;
o1.next = n1;
n1 = o1;
o1 = o2;
}
return n1;
}
评价:本质上与方法2 相同,只是方法2更为面向对象
例如
输入:head = [1,2,6,3,6], val = 6
输出:[1,2,3]
输入:head = [], val = 1
输出:[]
输入:head = [7,7,7,7], val = 7
输出:[]
方法1
图中 s 代表 sentinel 哨兵(如果不加哨兵,则删除第一个节点要特殊处理),例如要删除 6
p1 p2
s -> 1 -> 2 -> 6 -> 3 -> 6 -> null
p1 p2
s -> 1 -> 2 -> 6 -> 3 -> 6 -> null
p1 p2
s -> 1 -> 2 -> 6 -> 3 -> 6 -> null
p1 p2
s -> 1 -> 2 -> 3 -> 6 -> null
p1 p2
s -> 1 -> 2 -> 3 -> 6 -> null
p1 p2
s -> 1 -> 2 -> 3 -> null
最后代码
public ListNode removeElements(ListNode head, int val) {
ListNode sentinel = new ListNode(-1, head);
ListNode p1 = sentinel;
ListNode p2;
while ((p2 = p1.next) != null) {
if (p2.val == val) {
p1.next = p2.next;
} else {
p1 = p1.next;
}
}
return sentinel.next;
}
方法2
思路,递归函数负责返回:从当前节点(我)开始,完成删除的子链表
removeElements(ListNode p=1, int v=6){
1.next=removeElements(ListNode p=2, int v=6){
2.next=removeElements(ListNode p=6, int v=6){
removeElements(ListNode p=3, int v=6){
3.next=removeElements(ListNode p=6, int v=6){
removeElements(ListNode p=null, int v=6){
// 没有节点,返回
return null
}
}
return 3
}
}
return 2
}
return 1
}
代码
public ListNode removeElements(ListNode head, int val) {
if (head == null) {
return null;
}
if (head.val == val) {
return removeElements(head.next, val);
} else {
head.next = removeElements(head.next, val);
return head;
}
}
例如
输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]
输入:head = [1], n = 1
输出:[]
输入:head = [1,2], n = 1
输出:[1]
另外题目提示
方法1
思路,写一个递归函数,用来返回下一个节点的倒数序号
recursion(ListNode p=1, int n=2) {
recursion(ListNode p=2, int n=2) {
recursion(ListNode p=3, int n=2) {
recursion(ListNode p=4, int n=2) {
recursion(ListNode p=5, int n=2) {
recursion(ListNode p=null, int n=2) {
return 0; // 最内层序号0
}
return 1; // 上一次返回值+1
}
return 2;
}
if(返回值 == n == 2) {
// 删除 next
}
return 3;
}
return 4;
}
return 5;
}
但上述代码有一个问题,就是若删除的是第一个节点,它没有上一个节点,因此可以加一个哨兵来解决
代码
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode sentinel = new ListNode(-1, head);
recursion(sentinel, n);
return sentinel.next;
}
public int recursion(ListNode p, int n) {
if (p == null) {
return 0;
}
int nth = recursion(p.next, n);
if (nth == n) {
p.next = p.next.next;
}
return nth + 1;
}
Q:p.next.next 不怕空指针吗?
A:
方法2
快慢指针,p1 指向待删节点的上一个,p2 先走 n + 1 步
i=0
p2
s -> 1 -> 2 -> 3 -> 4 -> 5 -> null
i=1
p2
s -> 1 -> 2 -> 3 -> 4 -> 5 -> null
i=2
p2
s -> 1 -> 2 -> 3 -> 4 -> 5 -> null
i=3 从此开始 p1 p2 依次向右平移, 直到 p2 移动到末尾
p1 p2
s -> 1 -> 2 -> 3 -> 4 -> 5 -> null
p1 p2
s -> 1 -> 2 -> 3 -> 4 -> 5 -> null
代码
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode s = new ListNode(-1, head);
ListNode p1 = s;
ListNode p2 = s;
for (int i = 0; i < n + 1; i++) {
p2 = p2.next;
}
while (p2 != null) {
p1 = p1.next;
p2 = p2.next;
}
p1.next = p1.next.next;
return s.next;
}
方法3
public ListNode removeNthFromEnd(ListNode head, int n) {
Composite c = recursion(head, n);
return c.node;
}
static class Composite {
ListNode node;
int nth;
public Composite(ListNode node, int nth) {
this.node = node;
this.nth = nth;
}
}
public Composite recursion(ListNode p, int n) {
if (p == null) {
return new Composite(null, 1);
}
Composite c = recursion(p.next, n);
if (c.nth != n) {
p.next = c.node;
c.node = p;
}
c.nth +=1;
return c;
}
例如
输入:head = [1,1,2]
输出:[1,2]
输入:head = [1,1,2,3,3]
输出:[1,2,3]
注意:重复元素保留一个
方法1
p1 p2
1 -> 1 -> 2 -> 3 -> 3 -> null
p1 p2
1 -> 2 -> 3 -> 3 -> null
p1 p2
1 -> 2 -> 3 -> 3 -> null
p1 p2
1 -> 2 -> 3 -> 3 -> null
p1 p2
1 -> 2 -> 3 -> null
代码
public ListNode deleteDuplicates(ListNode head) {
// 链表节点 < 2
if (head == null || head.next == null) {
return head;
}
// 链表节点 >= 2
ListNode p1 = head;
ListNode p2;
while ((p2 = p1.next) != null) {
if (p1.val == p2.val) {
p1.next = p2.next;
} else {
p1 = p1.next;
}
}
return head;
}
方法2
递归函数负责返回:从当前节点(我)开始,完成去重的链表
deleteDuplicates(ListNode p=1) {
deleteDuplicates(ListNode p=1) {
1.next=deleteDuplicates(ListNode p=2) {
2.next=deleteDuplicates(ListNode p=3) {
deleteDuplicates(ListNode p=3) {
// 只剩一个节点,返回
return 3
}
}
return 2
}
return 1
}
}
代码
public ListNode deleteDuplicates(ListNode p) {
if (p == null || p.next == null) {
return p;
}
if(p.val == p.next.val) {
return deleteDuplicates(p.next);
} else {
p.next = deleteDuplicates(p.next);
return p;
}
}
例如
输入:head = [1,2,3,3,4,4,5]
输出:[1,2,5]
输入:head = [1,1,1,2,3]
输出:[2,3]
注意:重复元素一个不留
方法1
递归函数负责返回:从当前节点(我)开始,完成去重的链表
deleteDuplicates(ListNode p = 1) {
// 找下个不重复的
deleteDuplicates(ListNode p = 1) {
deleteDuplicates(ListNode p = 1) {
deleteDuplicates(ListNode p = 2) {
2.next=deleteDuplicates(ListNode p = 3) {
// 只剩一个节点,返回
return 3
}
return 2
}
}
}
}
代码
public ListNode deleteDuplicates(ListNode p) {
if (p == null || p.next == null) {
return p;
}
if (p.val == p.next.val) {
ListNode x = p.next.next;
while (x != null && x.val == p.val) {
x = x.next;
}
return deleteDuplicates(x);
} else {
p.next = deleteDuplicates(p.next);
return p;
}
}
方法2
p1 是待删除的上一个节点,每次循环对比 p2、p3 的值
p1 p2 p3
s, 1, 1, 1, 2, 3, null
p1 p2 p3
s, 1, 1, 1, 2, 3, null
p1 p2 p3
s, 1, 1, 1, 2, 3, null
p1 p3
s, 2, 3, null
p1 p2 p3
s, 2, 3, null
p1 p2 p3
s, 2, 3, null
代码
public ListNode deleteDuplicates(ListNode head) {
if (head == null || head.next == null) {
return head;
}
ListNode s = new ListNode(-1, head);
ListNode p1 = s;
ListNode p2;
ListNode p3;
while ((p2 = p1.next) != null && (p3 = p2.next) != null) {
if (p2.val == p3.val) {
while ((p3 = p3.next) != null
&& p3.val == p2.val) {
}
p1.next = p3;
} else {
p1 = p1.next;
}
}
return s.next;
}
例
输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]
输入:l1 = [], l2 = []
输出:[]
输入:l1 = [], l2 = [0]
输出:[0]
方法1
p1
1 3 8 9 null
p2
2 4 null
p
s null
代码
public ListNode mergeTwoLists(ListNode p1, ListNode p2) {
ListNode s = new ListNode(-1, null);
ListNode p = s;
while (p1 != null && p2 != null) {
if (p1.val < p2.val) {
p.next = p1;
p1 = p1.next;
} else {
p.next = p2;
p2 = p2.next;
}
p = p.next;
}
if (p1 != null) {
p.next = p1;
}
if (p2 != null) {
p.next = p2;
}
return s.next;
}
方法2
递归函数应该返回
mergeTwoLists(p1=[1,3,8,9], p2=[2,4]) {
1.next=mergeTwoLists(p1=[3,8,9], p2=[2,4]) {
2.next=mergeTwoLists(p1=[3,8,9], p2=[4]) {
3.next=mergeTwoLists(p1=[8,9], p2=[4]) {
4.next=mergeTwoLists(p1=[8,9], p2=null) {
return [8,9]
}
return 4
}
return 3
}
return 2
}
return 1
}
例
输入:lists = [[1,4,5],[1,3,4],[2,6]]
输出:[1,1,2,3,4,4,5,6]
解释:链表数组如下:
[
1->4->5,
1->3->4,
2->6
]
将它们合并到一个有序链表中得到。
1->1->2->3->4->4->5->6
方法1
递归
public ListNode mergeKLists(ListNode[] lists) {
if (lists.length == 0) {
return null;
}
return merge(lists, 0, lists.length - 1);
}
public ListNode split(ListNode[] lists, int i, int j) {
System.out.println(i + " " + j);
if (j == i) {
return lists[i];
}
int m = (i + j) >>> 1;
return mergeTwoLists(
split(lists, i, m),
split(lists, m + 1, j)
);
}
还可以用优先级队列求解,这个放在后面讲
例如
输入:[1,2,3,4,5]
输出:此列表中的结点 3 (序列化形式:[3,4,5])
输入:[1,2,3,4,5,6]
输出:此列表中的结点 4 (序列化形式:[4,5,6])
解法:快慢指针,快指针一次走两步,慢指针一次走一步,当快指针到链表结尾时,慢指针恰好走到链表的一半
public ListNode middleNode(ListNode head) {
ListNode p1 = head; // 慢指针,中间点
ListNode p2 = head; // 快指针
while (p2 != null && p2.next != null) {
p1 = p1.next;
p2 = p2.next;
p2 = p2.next;
}
return p1;
}
所谓回文指正着读、反着读,结果一样,例如
[1,2,2,1]
[1,2,3,2,1]
它们都是回文链表,不是回文的例子
[1,2,3,1] --反过来--> [1,3,2,1]
解法
/*
步骤1. 找中间点
步骤2. 中间点后半个链表反转
步骤3. 反转后链表与原链表逐一比较
*/
public boolean isPalindrome(ListNode head) {
ListNode middle = middle(head);
ListNode newHead = reverse(middle);
while (newHead != null) {
if (newHead.val != head.val) {
return false;
}
newHead = newHead.next;
head = head.next;
}
return true;
}
private ListNode reverse(ListNode o1) {
ListNode n1 = null;
while (o1 != null) {
ListNode o2 = o1.next;
o1.next = n1;
n1 = o1;
o1 = o2;
}
return n1;
}
private ListNode middle(ListNode head) {
ListNode p1 = head; // 慢
ListNode p2 = head; // 快
while (p2 != null && p2.next != null) {
p1 = p1.next;
p2 = p2.next.next;
}
return p1;
}
优化后解法
public boolean isPalindrome(ListNode h1) {
if (h1 == null || h1.next == null) {
return true;
}
ListNode p1 = h1; // 慢指针,中间点
ListNode p2 = h1; // 快指针
ListNode n1 = null; // 新头
ListNode o1 = h1; // 旧头
// 快慢指针找中间点
while (p2 != null && p2.next != null) {
p1 = p1.next;
p2 = p2.next.next;
// 反转前半部分
o1.next = n1;
n1 = o1;
o1 = p1;
}
if (p2 != null) { // 节点数为奇数
p1 = p1.next;
}
// 同步比较新头和后半部分
while (n1 != null) {
if (n1.val != p1.val) {
return false;
}
p1 = p1.next;
n1 = n1.next;
}
return true;
}
本题以及下题,实际是 Floyd’s Tortoise and Hare Algorithm (Floyd 龟兔赛跑算法)4
除了 Floyd 判环算法外,还有其它的判环算法,详见 https://en.wikipedia.org/wiki/Cycle_detection
如果链表上存在环,那么在环上以不同速度前进的两个指针必定会在某个时刻相遇。算法分为两个阶段
阶段1
阶段2
为什么呢?
阶段1 参考代码(判断是否有环)
public boolean hasCycle(ListNode head) {
ListNode h = head; // 兔
ListNode t = head; // 龟
while (h != null && h.next != null) {
t = t.next;
h = h.next.next;
if(h == t){
return true;
}
}
return false;
}
阶段2 参考代码(找到环入口)
public ListNode detectCycle(ListNode head) {
ListNode t = head; // 龟
ListNode h = head; // 兔
while (h != null && h.next != null) {
t = t.next;
h = h.next.next;
if (h == t) {
t = head;
while (true) {
if (h == t) {
return h;
}
h = h.next;
t = t.next;
}
}
}
return null;
}
这道题目比较简单,留给大家自己练习
例如
输入:head = [4,5,1,9], node = 5
输出:[4,1,9]
输入:head = [4,5,1,9], node = 1
输出:[4,5,9]
注意:被删除的节点不是末尾节点
参考答案
public class Ex1Leetcode237 {
/**
*
* @param node 待删除节点, 题目已说明肯定不是最后一个节点
*/
public void deleteNode(ListNode node) {
node.val = node.next.val; // 下一个节点值赋值给待"删除"节点
node.next = node.next.next; // 把下一个节点删除
}
public static void main(String[] args) {
ListNode o5 = new ListNode(5, null);
ListNode o4 = new ListNode(4, o5);
ListNode o3 = new ListNode(3, o4);
ListNode o2 = new ListNode(2, o3);
ListNode o1 = new ListNode(1, o2);
System.out.println(o1);
new E0xLeetcode237().deleteNode(o3);
System.out.println(o1);
}
}
输出
[1,2,3,4,5]
[1,2,4,5]
原题叫做相交链表,个人觉得用共尾链表更形象些,此题更像是一道脑筋急转弯,留给大家练习
例如,下图的两个链表 [1, 2, 4, 5] 与 [3, 4, 5] 它们中 [4, 5] 是相同的,此时应返回节点 4
非共尾的情况,如下图所示,此时返回 null
思路,称两个链表为 a=[1, 2, 4, 5],b=[3, 4, 5],图中用 N 代表 null
1 2 4 5 N 3 4 5 N
3 4 5 N 1 2 4 5 N
如果两个链表长度相同,则可以更早找到目标,例如 a=[1, 4, 5],b=[3, 4, 5],第一次出现 4 时,即可返回
1 4 5 N 3 4 5 N
3 4 5 N 1 4 5 N
如果是非共尾的情况,如 a=[1, 2, 4],b=[3, 5],可以看到,唯一相等的情况,是遍历到最后那个 N 此时退出循环
1 2 4 N 3 5 N
3 5 N 1 2 4 N
代码
public ListNode getIntersectionNode(ListNode a, ListNode b) {
ListNode p1 = a;
ListNode p2 = b;
while (true) {
if (p1 == p2) {
return p1;
}
if (p1 == null) {
p1 = b;
} else {
p1 = p1.next;
}
if (p2 == null) {
p2 = a;
} else {
p2 = p2.next;
}
}
}
将数组内两个区间内的有序元素合并
例
[1, 5, 6, 2, 4, 10, 11]
可以视作两个有序区间
[1, 5, 6] 和 [2, 4, 10, 11]
合并后,结果仍存储于原有空间
[1, 2, 4, 5, 6, 10, 11]
方法1
递归
merge(left=[1,5,6],right=[2,4,10,11],a2=[]){
merge(left=[5,6],right=[2,4,10,11],a2=[1]){
merge(left=[5,6],right=[4,10,11],a2=[1,2]){
merge(left=[5,6],right=[10,11],a2=[1,2,4]){
merge(left=[6],right=[10,11],a2=[1,2,4,5]){
merge(left=[],right=[10,11],a2=[1,2,4,5,6]){
// 拷贝10,11
}
}
}
}
}
}
代码
public static void merge(int[] a1, int i, int iEnd, int j, int jEnd,
int[] a2, int k) {
if (i > iEnd) {
System.arraycopy(a1, j, a2, k, jEnd - j + 1);
return;
}
if (j > jEnd) {
System.arraycopy(a1, i, a2, k, iEnd - i + 1);
return;
}
if (a1[i] < a1[j]) {
a2[k] = a1[i];
merge(a1, i + 1, iEnd, j, jEnd, a2, k + 1);
} else {
a2[k] = a1[j];
merge(a1, i, iEnd, j + 1, jEnd, a2, k + 1);
}
}
测试
int[] a1 = {1, 5, 6, 2, 4, 10, 11};
int[] a2 = new int[a1.length];
merge(a1, 0, 2, 3, 6, a2, 0);
方法2
代码
public static void merge(int[] a1, int i, int iEnd,
int j, int jEnd,
int[] a2) {
int k = i;
while (i <= iEnd && j <= jEnd) {
if (a1[i] < a1[j]) {
a2[k] = a1[i];
i++;
} else {
a2[k] = a1[j];
j++;
}
k++;
}
if (i > iEnd) {
System.arraycopy(a1, j, a2, k, jEnd - j + 1);
}
if (j > jEnd) {
System.arraycopy(a1, i, a2, k, iEnd - i + 1);
}
}
测试
int[] a1 = {1, 5, 6, 2, 4, 10, 11};
int[] a2 = new int[a3.length];
merge(a1, 0, 2, 3, 6, a2);
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> result = new ArrayList<>();
if(root == null) {
return result;
}
LinkedListQueue<TreeNode> queue = new LinkedListQueue<>();
queue.offer(root);
int c1 = 1; // 本层节点个数
while (!queue.isEmpty()) {
int c2 = 0; // 下层节点个数
List<Integer> level = new ArrayList<>();
for (int i = 0; i < c1; i++) {
TreeNode node = queue.poll();
level.add(node.val);
if (node.left != null) {
queue.offer(node.left);
c2++;
}
if (node.right != null) {
queue.offer(node.right);
c2++;
}
}
c1 = c2;
result.add(level);
}
return result;
}
// 自定义队列
static class LinkedListQueue<E> {
private static class Node<E> {
E value;
Node<E> next;
public Node(E value, Node<E> next) {
this.value = value;
this.next = next;
}
}
private final Node<E> head = new Node<>(null, null);
private Node<E> tail = head;
int size = 0;
private int capacity = Integer.MAX_VALUE;
{
tail.next = head;
}
public LinkedListQueue() {
}
public LinkedListQueue(int capacity) {
this.capacity = capacity;
}
public boolean offer(E value) {
if (isFull()) {
return false;
}
Node<E> added = new Node<>(value, head);
tail.next = added;
tail = added;
size++;
return true;
}
public E poll() {
if (isEmpty()) {
return null;
}
Node<E> first = head.next;
head.next = first.next;
if (first == tail) {
tail = head;
}
size--;
return first.value;
}
public E peek() {
if (isEmpty()) {
return null;
}
return head.next.value;
}
public boolean isEmpty() {
return head == tail;
}
public boolean isFull() {
return size == capacity;
}
}
}
由于与课堂例题差别不大,这里只给出参考解答
基于链表的实现
public class Ex1Leetcode622 {
private static class Node {
int value;
Node next;
Node(int value, Node next) {
this.value = value;
this.next = next;
}
}
private final Node head = new Node(-1, null);
private Node tail = head;
private int size = 0;
private int capacity = 0;
{
tail.next = head;
}
public Ex1Leetcode622(int capacity) {
this.capacity = capacity;
}
public boolean enQueue(int value) {
if(isFull()) {
return false;
}
Node added = new Node(value, head);
tail.next = added;
tail = added;
size++;
return true;
}
public boolean deQueue() {
if(isEmpty()) {
return false;
}
Node first = head.next;
head.next = first.next;
if (first == tail) {
tail = head;
}
size--;
return true;
}
public int Front() {
if(isEmpty()) {
return -1;
}
return head.next.value;
}
public int Rear() {
if(isEmpty()) {
return -1;
}
return tail.value;
}
public boolean isEmpty() {
return head == tail;
}
public boolean isFull() {
return size == capacity;
}
}
注意:
一个字符串中可能出现 []
()
和 {}
三种括号,判断该括号是否有效
有效的例子
()[]{}
([{}])
()
无效的例子
[)
([)]
([]
思路
答案(用到了课堂案例中的 ArrayStack 类)
public boolean isValid(String s) {
ArrayStack<Character> stack = new ArrayStack<>(s.length() / 2 + 1);
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (c == '(') {
stack.push(')');
} else if (c == '[') {
stack.push(']');
} else if (c == '{') {
stack.push('}');
} else {
if (!stack.isEmpty() && stack.peek() == c) {
stack.pop();
} else {
return false;
}
}
}
return stack.isEmpty();
}
后缀表达式也称为逆波兰表达式,即运算符写在后面
示例
输入:tokens = ["2","1","+","3","*"]
输出:9
即:(2 + 1) * 3
输入:tokens = ["4","13","5","/","+"]
输出:6
即:4 + (13 / 5)
题目假设
代码
public int evalRPN(String[] tokens) {
LinkedList<Integer> numbers = new LinkedList<>();
for (String t : tokens) {
switch (t) {
case "+" -> {
Integer b = numbers.pop();
Integer a = numbers.pop();
numbers.push(a + b);
}
case "-" -> {
Integer b = numbers.pop();
Integer a = numbers.pop();
numbers.push(a - b);
}
case "*" -> {
Integer b = numbers.pop();
Integer a = numbers.pop();
numbers.push(a * b);
}
case "/" -> {
Integer b = numbers.pop();
Integer a = numbers.pop();
numbers.push(a / b);
}
default -> numbers.push(Integer.parseInt(t));
}
}
return numbers.pop();
}
public class E03InfixToSuffix {
/*
思路
1. 遇到数字, 拼串
2. 遇到 + - * /
- 优先级高于栈顶运算符 入栈
- 否则将栈中高级或平级运算符出栈拼串, 本运算符入栈
3. 遍历完成, 栈中剩余运算符出栈拼串
- 先出栈,意味着优先运算
4. 带 ()
- 左括号直接入栈
- 右括号要将栈中直至左括号为止的运算符出栈拼串
| |
| |
| |
_____
a+b
a+b-c
a+b*c
a*b+c
(a+b)*c
*/
public static void main(String[] args) {
System.out.println(infixToSuffix("a+b"));
System.out.println(infixToSuffix("a+b-c"));
System.out.println(infixToSuffix("a+b*c"));
System.out.println(infixToSuffix("a*b-c"));
System.out.println(infixToSuffix("(a+b)*c"));
System.out.println(infixToSuffix("a+b*c+(d*e+f)*g"));
}
static String infixToSuffix(String exp) {
LinkedList<Character> stack = new LinkedList<>();
StringBuilder sb = new StringBuilder(exp.length());
for (int i = 0; i < exp.length(); i++) {
char c = exp.charAt(i);
switch (c) {
case '+', '-', '*', '/' -> {
if (stack.isEmpty()) {
stack.push(c);
} else {
if (priority(c) > priority(stack.peek())) {
stack.push(c);
} else {
while (!stack.isEmpty()
&& priority(stack.peek()) >= priority(c)) {
sb.append(stack.pop());
}
stack.push(c);
}
}
}
case '(' -> {
stack.push(c);
}
case ')' -> {
while (!stack.isEmpty() && stack.peek() != '(') {
sb.append(stack.pop());
}
stack.pop();
}
default -> {
sb.append(c);
}
}
}
while (!stack.isEmpty()) {
sb.append(stack.pop());
}
return sb.toString();
}
static int priority(char c) {
return switch (c) {
case '(' -> 0;
case '*', '/' -> 2;
case '+', '-' -> 1;
default -> throw new IllegalArgumentException("不合法字符:" + c);
};
}
}
给力扣题目用的自实现栈,可以定义为静态内部类
class ArrayStack<E> {
private E[] array;
private int top; // 栈顶指针
@SuppressWarnings("all")
public ArrayStack(int capacity) {
this.array = (E[]) new Object[capacity];
}
public boolean push(E value) {
if (isFull()) {
return false;
}
array[top++] = value;
return true;
}
public E pop() {
if (isEmpty()) {
return null;
}
return array[--top];
}
public E peek() {
if (isEmpty()) {
return null;
}
return array[top - 1];
}
public boolean isEmpty() {
return top == 0;
}
public boolean isFull() {
return top == array.length;
}
}
参考解答,注意:题目已说明
public class E04Leetcode232 {
/*
队列头 队列尾
s1 s2
顶 底 底 顶
abc
push(a)
push(b)
push(c)
pop()
*/
ArrayStack<Integer> s1 = new ArrayStack<>(100);
ArrayStack<Integer> s2 = new ArrayStack<>(100);
public void push(int x) {
s2.push(x);
}
public int pop() {
if (s1.isEmpty()) {
while (!s2.isEmpty()) {
s1.push(s2.pop());
}
}
return s1.pop();
}
public int peek() {
if (s1.isEmpty()) {
while (!s2.isEmpty()) {
s1.push(s2.pop());
}
}
return s1.peek();
}
public boolean empty() {
return s1.isEmpty() && s2.isEmpty();
}
}
给力扣题目用的自实现队列,可以定义为静态内部类
public class ArrayQueue3<E> {
private final E[] array;
int head = 0;
int tail = 0;
@SuppressWarnings("all")
public ArrayQueue3(int c) {
c -= 1;
c |= c >> 1;
c |= c >> 2;
c |= c >> 4;
c |= c >> 8;
c |= c >> 16;
c += 1;
array = (E[]) new Object[c];
}
public boolean offer(E value) {
if (isFull()) {
return false;
}
array[tail & (array.length - 1)] = value;
tail++;
return true;
}
public E poll() {
if (isEmpty()) {
return null;
}
E value = array[head & (array.length - 1)];
head++;
return value;
}
public E peek() {
if (isEmpty()) {
return null;
}
return array[head & (array.length - 1)];
}
public boolean isEmpty() {
return head == tail;
}
public boolean isFull() {
return tail - head == array.length;
}
}
参考解答,注意:题目已说明
public class E05Leetcode225 {
/*
队列头 队列尾
cba
顶 底
queue.offer(a)
queue.offer(b)
queue.offer(c)
*/
ArrayQueue3<Integer> queue = new ArrayQueue3<>(100);
int size = 0;
public void push(int x) {
queue.offer(x);
for (int i = 0; i < size; i++) {
queue.offer(queue.poll());
}
size++;
}
public int pop() {
size--;
return queue.poll();
}
public int top() {
return queue.peek();
}
public boolean empty() {
return queue.isEmpty();
}
}
public class E01Leetcode103 {
public List<List<Integer>> zigzagLevelOrder(TreeNode root) {
List<List<Integer>> result = new ArrayList<>();
if (root == null) {
return result;
}
LinkedList<TreeNode> queue = new LinkedList<>();
queue.offer(root);
boolean leftToRight = true;
int c1 = 1;
while (!queue.isEmpty()) {
int c2 = 0;
LinkedList<Integer> deque = new LinkedList<>();
for (int i = 0; i < c1; i++) {
TreeNode n = queue.poll();
if (leftToRight) {
deque.offerLast(n.val);
} else {
deque.offerFirst(n.val);
}
if (n.left != null) {
queue.offer(n.left);
c2++;
}
if (n.right != null) {
queue.offer(n.right);
c2++;
}
}
c1 = c2;
leftToRight = !leftToRight;
result.add(deque);
}
return result;
}
public static void main(String[] args) {
TreeNode root = new TreeNode(
new TreeNode(
new TreeNode(4),
2,
new TreeNode(5)
),
1,
new TreeNode(
new TreeNode(6),
3,
new TreeNode(7)
)
);
List<List<Integer>> lists = new E01Leetcode103().zigzagLevelOrder(root);
for (List<Integer> list : lists) {
System.out.println(list);
}
}
}
与课堂例题也是差别不大,请参考
这道题目之前解答过,现在用刚学的优先级队列来实现一下
题目中要从小到大排列,因此选择用小顶堆来实现,自定义小顶堆如下
public class MinHeap {
ListNode[] array;
int size;
public MinHeap(int capacity) {
array = new ListNode[capacity];
}
public void offer(ListNode offered) {
int child = size++;
int parent = (child - 1) / 2;
while (child > 0 && offered.val < array[parent].val) {
array[child] = array[parent];
child = parent;
parent = (child - 1) / 2;
}
array[child] = offered;
}
public ListNode poll() {
if (isEmpty()) {
return null;
}
swap(0, size - 1);
size--;
ListNode e = array[size];
array[size] = null; // help GC
down(0);
return e;
}
private void down(int parent) {
int left = 2 * parent + 1;
int right = left + 1;
int min = parent;
if (left < size && array[left].val < array[min].val) {
min = left;
}
if (right < size && array[right].val < array[min].val) {
min = right;
}
if (min != parent) {
swap(min, parent);
down(min);
}
}
private void swap(int i, int j) {
ListNode t = array[i];
array[i] = array[j];
array[j] = t;
}
public boolean isEmpty() {
return size == 0;
}
}
代码
public class E01Leetcode23 {
public ListNode mergeKLists(ListNode[] lists) {
// 1. 使用 jdk 的优先级队列实现
// PriorityQueue queue = new PriorityQueue<>(Comparator.comparingInt(a -> a.val));
// 2. 使用自定义小顶堆实现
MinHeap queue = new MinHeap(lists.length);
for (ListNode head : lists) {
if (head != null) {
queue.offer(head);
}
}
ListNode s = new ListNode(-1, null);
ListNode p = s;
while (!queue.isEmpty()) {
ListNode node = queue.poll();
p.next = node;
p = node;
if (node.next != null) {
queue.offer(node.next);
}
}
return s.next;
}
}
提问:
回答:
算法描述
可以使用之前课堂例题的大顶堆来实现
int[] array = {1, 2, 3, 4, 5, 6, 7};
MaxHeap maxHeap = new MaxHeap(array);
System.out.println(Arrays.toString(maxHeap.array));
while (maxHeap.size > 1) {
maxHeap.swap(0, maxHeap.size - 1);
maxHeap.size--;
maxHeap.down(0);
}
System.out.println(Arrays.toString(maxHeap.array));
小顶堆(可删去用不到代码)
class MinHeap {
int[] array;
int size;
public MinHeap(int capacity) {
array = new int[capacity];
}
private void heapify() {
for (int i = (size >> 1) - 1; i >= 0; i--) {
down(i);
}
}
public int poll() {
swap(0, size - 1);
size--;
down(0);
return array[size];
}
public int poll(int index) {
swap(index, size - 1);
size--;
down(index);
return array[size];
}
public int peek() {
return array[0];
}
public boolean offer(int offered) {
if (size == array.length) {
return false;
}
up(offered);
size++;
return true;
}
public void replace(int replaced) {
array[0] = replaced;
down(0);
}
private void up(int offered) {
int child = size;
while (child > 0) {
int parent = (child - 1) >> 1;
if (offered < array[parent]) {
array[child] = array[parent];
} else {
break;
}
child = parent;
}
array[child] = offered;
}
private void down(int parent) {
int left = (parent << 1) + 1;
int right = left + 1;
int min = parent;
if (left < size && array[left] < array[min]) {
min = left;
}
if (right < size && array[right] < array[min]) {
min = right;
}
if (min != parent) {
swap(min, parent);
down(min);
}
}
// 交换两个索引处的元素
private void swap(int i, int j) {
int t = array[i];
array[i] = array[j];
array[j] = t;
}
}
题解
public int findKthLargest(int[] numbers, int k) {
MinHeap heap = new MinHeap(k);
for (int i = 0; i < k; i++) {
heap.offer(numbers[i]);
}
for (int i = k; i < numbers.length; i++) {
if(numbers[i] > heap.peek()){
heap.replace(numbers[i]);
}
}
return heap.peek();
}
求数组中的第 K 大元素,使用堆并不是最佳选择,可以采用快速选择算法
上题的小顶堆加一个方法
class MinHeap {
// ...
public boolean isFull() {
return size == array.length;
}
}
题解
class KthLargest {
private MinHeap heap;
public KthLargest(int k, int[] nums) {
heap = new MinHeap(k);
for(int i = 0; i < nums.length; i++) {
add(nums[i]);
}
}
public int add(int val) {
if(!heap.isFull()){
heap.offer(val);
} else if(val > heap.peek()){
heap.replace(val);
}
return heap.peek();
}
}
求数据流中的第 K 大元素,使用堆最合适不过
可以扩容的 heap, max 用于指定是大顶堆还是小顶堆
public class Heap {
int[] array;
int size;
boolean max;
public int size() {
return size;
}
public Heap(int capacity, boolean max) {
this.array = new int[capacity];
this.max = max;
}
/**
* 获取堆顶元素
*
* @return 堆顶元素
*/
public int peek() {
return array[0];
}
/**
* 删除堆顶元素
*
* @return 堆顶元素
*/
public int poll() {
int top = array[0];
swap(0, size - 1);
size--;
down(0);
return top;
}
/**
* 删除指定索引处元素
*
* @param index 索引
* @return 被删除元素
*/
public int poll(int index) {
int deleted = array[index];
swap(index, size - 1);
size--;
down(index);
return deleted;
}
/**
* 替换堆顶元素
*
* @param replaced 新元素
*/
public void replace(int replaced) {
array[0] = replaced;
down(0);
}
/**
* 堆的尾部添加元素
*
* @param offered 新元素
*/
public void offer(int offered) {
if (size == array.length) {
grow();
}
up(offered);
size++;
}
private void grow() {
int capacity = size + (size >> 1);
int[] newArray = new int[capacity];
System.arraycopy(array, 0,
newArray, 0, size);
array = newArray;
}
// 将 offered 元素上浮: 直至 offered 小于父元素或到堆顶
private void up(int offered) {
int child = size;
while (child > 0) {
int parent = (child - 1) / 2;
boolean cmp = max ? offered > array[parent] : offered < array[parent];
if (cmp) {
array[child] = array[parent];
} else {
break;
}
child = parent;
}
array[child] = offered;
}
public Heap(int[] array, boolean max) {
this.array = array;
this.size = array.length;
this.max = max;
heapify();
}
// 建堆
private void heapify() {
// 如何找到最后这个非叶子节点 size / 2 - 1
for (int i = size / 2 - 1; i >= 0; i--) {
down(i);
}
}
// 将 parent 索引处的元素下潜: 与两个孩子较大者交换, 直至没孩子或孩子没它大
private void down(int parent) {
int left = parent * 2 + 1;
int right = left + 1;
int min = parent;
if (left < size && (max ? array[left] > array[min] : array[left] < array[min])) {
min = left;
}
if (right < size && (max ? array[right] > array[min] : array[right] < array[min])) {
min = right;
}
if (min != parent) { // 找到了更大的孩子
swap(min, parent);
down(min);
}
}
// 交换两个索引处的元素
private void swap(int i, int j) {
int t = array[i];
array[i] = array[j];
array[j] = t;
}
}
题解
private Heap left = new Heap(10, false);
private Heap right = new Heap(10, true);
/**
为了保证两边数据量的平衡
- 两边数据一样时,加入左边
- 两边数据不一样时,加入右边
但是, 随便一个数能直接加入吗?
- 加入左边前, 应该挑右边最小的加入
- 加入右边前, 应该挑左边最大的加入
*/
public void addNum(int num) {
if (left.size() == right.size()) {
right.offer(num);
left.offer(right.poll());
} else {
left.offer(num);
right.offer(left.poll());
}
}
/**
*
* - 两边数据一致, 左右各取堆顶元素求平均
* - 左边多一个, 取左边元素
*
*/
public double findMedian() {
if (left.size() == right.size()) {
return (left.peek() + right.peek()) / 2.0;
} else {
return left.peek();
}
}
本题还可以使用平衡二叉搜索树求解,不过代码比两个堆复杂
public boolean isSymmetric(TreeNode root) {
return check(root.left, root.right);
}
public boolean check(TreeNode left, TreeNode right) {
// 若同时为 null
if (left == null && right == null) {
return true;
}
// 若有一个为 null (有上一轮筛选,另一个肯定不为 null)
if (left == null || right == null) {
return false;
}
if (left.val != right.val) {
return false;
}
return check(left.left, right.right) && check(left.right, right.left);
}
类似题目:Leetcode 100 题 - 相同的树
后序遍历求解
/*
思路:
1. 得到左子树深度, 得到右子树深度, 二者最大者加一, 就是本节点深度
2. 因为需要先得到左右子树深度, 很显然是后序遍历典型应用
3. 关于深度的定义:从根出发, 离根最远的节点总边数,
注意: 力扣里的深度定义要多一
深度2 深度3 深度1
1 1 1
/ \ / \
2 3 2 3
\
4
*/
public int maxDepth(TreeNode node) {
if (node == null) {
return 0; // 非力扣题目改为返回 -1
}
int d1 = maxDepth(node.left);
int d2 = maxDepth(node.right);
return Integer.max(d1, d2) + 1;
}
后序遍历求解-非递归
/*
思路:
1. 使用非递归后序遍历, 栈的最大高度即为最大深度
*/
public int maxDepth(TreeNode root) {
TreeNode curr = root;
LinkedList<TreeNode> stack = new LinkedList<>();
int max = 0;
TreeNode pop = null;
while (curr != null || !stack.isEmpty()) {
if (curr != null) {
stack.push(curr);
int size = stack.size();
if (size > max) {
max = size;
}
curr = curr.left;
} else {
TreeNode peek = stack.peek();
if(peek.right == null || peek.right == pop) {
pop = stack.pop();
} else {
curr = peek.right;
}
}
}
return max;
}
层序遍历求解
/*
思路:
1. 使用层序遍历, 层数即最大深度
*/
public int maxDepth(TreeNode root) {
if(root == null) {
return 0;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
int level = 0;
while (!queue.isEmpty()) {
level++;
int size = queue.size();
for (int i = 0; i < size; i++) {
TreeNode node = queue.poll();
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
}
}
return level;
}
后序遍历求解
public int minDepth(TreeNode node) {
if (node == null) {
return 0;
}
int d1 = minDepth(node.left);
int d2 = minDepth(node.right);
if (d1 == 0 || d2 == 0) {
return d1 + d2 + 1;
}
return Integer.min(d1, d2) + 1;
}
相较于求最大深度,应当考虑:
上面两种情况满足时,不应该再把为 null 子树的深度 0 参与最小值比较,例如这样
1
/
2
1
\
3
\
4
层序遍历求解
遇到的第一个叶子节点所在层就是最小深度
例如,下面的树遇到的第一个叶子节点 3 所在的层就是最小深度,其他 4,7 等叶子节点深度更深,也更晚遇到
1
/ \
2 3
/ \
4 5
/
7
代码
public int minDepth(TreeNode root) {
if(root == null) {
return 0;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
int level = 0;
while (!queue.isEmpty()) {
level++;
int size = queue.size();
for (int i = 0; i < size; i++) {
TreeNode node = queue.poll();
if (node.left == null && node.right == null) {
return level;
}
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
}
}
return level;
}
效率会高于之前后序遍历解法,因为找到第一个叶子节点后,就无需后续的层序遍历了
public TreeNode invertTree(TreeNode root) {
fn(root);
return root;
}
private void fn(TreeNode node){
if (node == null) {
return;
}
TreeNode t = node.left;
node.left = node.right;
node.right = t;
fn(node.left);
fn(node.right);
}
先交换、再递归或是先递归、再交换都可以
static class TreeNode {
public String val;
public TreeNode left;
public TreeNode right;
public TreeNode(String val) {
this.val = val;
}
public TreeNode(TreeNode left, String val, TreeNode right) {
this.left = left;
this.val = val;
this.right = right;
}
@Override
public String toString() {
return this.val;
}
}
/*
中缀表达式 (2-1)*3
后缀(逆波兰)表达式 21-3*
1.遇到数字入栈
2.遇到运算符, 出栈两次, 与当前节点建立父子关系, 当前节点入栈
栈
| |
| |
| |
_____
表达式树
*
/ \
- 3
/ \
2 1
21-3*
*/
public TreeNode constructExpressionTree(String[] tokens) {
LinkedList<TreeNode> stack = new LinkedList<>();
for (String t : tokens) {
switch (t) {
case "+", "-", "*", "/" -> { // 运算符
TreeNode right = stack.pop();
TreeNode left = stack.pop();
TreeNode parent = new TreeNode(t);
parent.left = left;
parent.right = right;
stack.push(parent);
}
default -> { // 数字
stack.push(new TreeNode(t));
}
}
}
return stack.peek();
}
public class E09Leetcode105 {
/*
preOrder = {1,2,4,3,6,7}
inOrder = {4,2,1,6,3,7}
根 1
pre in
左 2,4 4,2
右 3,6,7 6,3,7
根 2
左 4
根 3
左 6
右 7
*/
public TreeNode buildTree(int[] preOrder, int[] inOrder) {
if (preOrder.length == 0) {
return null;
}
// 创建根节点
int rootValue = preOrder[0];
TreeNode root = new TreeNode(rootValue);
// 区分左右子树
for (int i = 0; i < inOrder.length; i++) {
if (inOrder[i] == rootValue) {
// 0 ~ i-1 左子树
// i+1 ~ inOrder.length -1 右子树
int[] inLeft = Arrays.copyOfRange(inOrder, 0, i); // [4,2]
int[] inRight = Arrays.copyOfRange(inOrder, i + 1, inOrder.length); // [6,3,7]
int[] preLeft = Arrays.copyOfRange(preOrder, 1, i + 1); // [2,4]
int[] preRight = Arrays.copyOfRange(preOrder, i + 1, inOrder.length); // [3,6,7]
root.left = buildTree(preLeft, inLeft); // 2
root.right = buildTree(preRight, inRight); // 3
break;
}
}
return root;
}
}
public TreeNode buildTree(int[] inOrder, int[] postOrder) {
if (inOrder.length == 0) {
return null;
}
// 根
int rootValue = postOrder[postOrder.length - 1];
TreeNode root = new TreeNode(rootValue);
// 切分左右子树
for (int i = 0; i < inOrder.length; i++) {
if (inOrder[i] == rootValue) {
int[] inLeft = Arrays.copyOfRange(inOrder, 0, i);
int[] inRight = Arrays.copyOfRange(inOrder, i + 1, inOrder.length);
int[] postLeft = Arrays.copyOfRange(postOrder, 0, i);
int[] postRight = Arrays.copyOfRange(postOrder, i, postOrder.length - 1);
root.left = buildTree(inLeft, postLeft);
root.right = buildTree(inRight, postRight);
break;
}
}
return root;
}
public static void main(String[] args) {
char[] array = "abcde".toCharArray();
recursion(array, 0, array.length - 1);
System.out.println(Arrays.toString(array));
}
public static void recursion(char[] array, int i, int j) {
if (i >= j) {
return;
}
swap(array, i, j);
recursion(array, ++i, --j);
}
public static void swap(char[] array, int i, int j) {
char c = array[i];
array[i] = array[j];
array[j] = c;
}
引用自 面试最常考的 100 道算法题分类整理! - 知乎 (zhihu.com)
带 ✔️ 是本课程讲解过的
Josephus problem 主要参考 https://en.wikipedia.org/wiki/Josephus_problem ↩︎
汉诺塔图片资料均来自 https://en.wikipedia.org/wiki/Tower_of_Hanoi ↩︎
也称为 Pascal’s triangle https://en.wikipedia.org/wiki/Pascal%27s_triangle ↩︎
龟兔赛跑动画来自于 Floyd’s Hare and Tortoise Algorithm Demo - One Step! Code (onestepcode.com) ↩︎