剑指Offer

数组与矩阵

5. 替换空格

请实现一个函数,把字符串 s 中的每个空格替换成”%20”。

示例1:

1
2
输入:s = "We are happy."
输出:"We%20are%20happy."

限制:

0 <= s 的长度 <= 10000

在网络编程中,如果URL参数中含有特殊字符,如空格、#等,则可能导致服务器端无法获得正确的参数值。我们需要将这些特殊符号转换成服务器可以识别的字符。转换的规则是在后面跟上ASCII码的两位十六进制的表示。比如空格的ASCII码是32,即十六进制的0x20,因此空格被替换成”%20”。再比如#的ASCII码为35,即十六进制的0x23,它在URL中被替换为”%23”。

如果是在原来的字符串上进行替换,就有可能覆盖修改在该字符串后面的内存。如果是创建新的字符串并在新的字符串上进行替换,那么我们可以自己分配足够多的内存。由于有这样两种不同的解决方案,我们应该向面试官问清楚,让他明确告诉我们他的需求。假设面试官让我们在原来的字符串上进行替换,并且保证输入的字符串后面有足够多的空余内存

最直观的做法是从头到尾扫描字符串,每次碰到空格时进行替换。这样做,每次碰到空格就需要把空格后面的所有字符都后移2字节。假设字符串的长度是n。对每个空格字符,需要移动后面$O(n)$个字符,因此对于含有$O(n)$个空格字符的字符串而言,总的时间效率是$O(n^2)$。这显然不是一个好的解决方案。

那么,怎么操作能减少移动次数呢?—— 试试把从前向后替换改成从后向前替换。(对于Java来说,String是不可变的,因此,严谨地说,OJ题提供的方法头的参数应该由String改为StringBuffer。)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public String replaceSpace(String s) {
StringBuffer str = new StringBuffer(s); //将String转换为StringBuffer(可变长度)
int P1 = str.length() - 1; //模拟指针
for (int i = 0; i <= P1; i++)
if (str.charAt(i) == ' ')
str.append(" "); //出现一个空格,就在字符串末尾添加2个任意字符

int P2 = str.length() - 1; // 模拟指针,初始指向 将空格替换后的最后一位
while (P1 >= 0 && P2 > P1) { //从后往前遍历,能保证原来的字符串不会因为替换字符而改变
char c = str.charAt(P1--); // 获取原始字符串的最后一位字符
if (c == ' ') {
str.setCharAt(P2--, '0');
str.setCharAt(P2--, '2');
str.setCharAt(P2--, '%');
} else {
str.setCharAt(P2--, c);
}
}
return str.toString();
}

时间复杂度和空间复杂度都是$O(n)$​。


29. 顺时针打印矩阵

输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字。

示例 1:

1
2
输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出:[1,2,3,6,9,8,7,4,5]

示例 2:

1
2
输入:matrix = [[1,2,3,4],[5,6,7,8],[9,10,11,12]]
输出:[1,2,3,4,8,12,11,10,9,5,6,7]

限制:

  • 0 <= matrix.length <= 100
  • 0 <= matrix[i].length <= 100

矩阵不一定是方阵,四个顶点的坐标是临界点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public int[] spiralOrder(int[][] matrix) {
if (matrix.length == 0) { // 矩阵为空
return new int[0];
}

int left = 0;
int right = matrix[0].length - 1;
int top = 0;
int bottom = matrix.length - 1;
int[] result = new int[(right + 1) * (bottom + 1)];
int index = 0;

while(true) {
for (int i = left; i <= right; i++) // 遍历top行
result[index++] = matrix[top][i];
if (++top > bottom) // 没有新的行,则遍历完成
break;

for (int i = top; i <= bottom; i++)
result[index++] = matrix[i][right];
if (--right < left) // 没有新的列
break;

for (int i = right; i >= left; i--)
result[index++] = matrix[bottom][i];
if (--bottom < top) // 没有新的行
break;

for (int i = bottom; i >= top; i--)
result[index++] = matrix[i][left];
if (++left > right) // 没有新的列
break;
}
return result;
}

50. 第一个只出现一次的字符位置

在字符串 s 中找出第一个只出现一次的字符。如果没有,返回一个单空格。 s 只包含小写字母。

示例:

1
2
3
4
5
s = "abaccdeff"
返回 "b"

s = ""
返回 " "

限制:

0 <= s 的长度 <= 50000

统计字符是否仅出现一次,可以用容器存放每个字符是否只出现一次。这个容器可以根据<字符>来存放<是否多次出现>的信息。因此,可以利用哈希表

1
2
3
4
5
6
7
8
9
10
11
12
public char firstUniqChar(String s) {
HashMap<Character, Boolean> hm = new HashMap(); // 用布尔值表示是否只有一个字符,比用Integer统计出现次数节约空间
char[] s2c = s.toCharArray();
for(char c : s2c) {
hm.put(c, !hm.containsKey(c)); // 如果已有该字符,false;否则true
}
for(char c : s2c) { // 按字符顺序遍历,能返回第一个只出现一次的字符
if(hm.get(c) == true)
return c;
}
return ' ';
}

时间复杂度$O(n)$​,空间复杂度$O(1)$​


栈、队列、堆

9. 用两个栈实现队列

剑指 Offer 09. 用两个栈实现队列 - 力扣(LeetCode)

  • 栈是LIFO(后进先出),队列是FIFO(先进先出)。
  • 将一个栈专门用于插入整数,另一个栈专门用于删除整数
  • Stack类已被Java不推荐使用LinkedList基于双向链表实现,只能顺序访问,但可以快速插入和删除元素。LinkedList可用作栈、队列和双向队列
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private LinkedList<Integer> list1, list2;

public CQueue() {
list1 = new LinkedList<>();
list2 = new LinkedList<>();
}

public void appendTail(int value) {
list1.addLast(value); // 加到链表尾部
}

public int deleteHead() {
if (!list2.isEmpty()) { //list2中的元素都在list1之前进入栈中
return list2.removeLast();
}
if (list1.isEmpty()) { //模拟的队列中没有元素了
return -1;
}

while (!list1.isEmpty()) { //将list1中已存在的元素都移入list2中
list2.addLast(list1.removeLast());
}
return list2.removeLast();
}

时间复杂度:$O(n)$(deleteHead()函数在N次队首元素删除操作中总共需完成N个元素的倒序),空间复杂度$O(n)$。


30. 包含min函数的栈

剑指 Offer 30. 包含min函数的栈 - 力扣(LeetCode)

既然是定义栈,那么push和pop功能显然不用再变动,重点在于实现min函数。栈中的最小元素会随着元素的入栈和出栈动态变化,因此需要记录每个状态对应的当前最小元素。可以构造一个辅助栈来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private LinkedList<Integer> stack, minStack;

public MinStack() {
stack = new LinkedList<>();
minStack = new LinkedList<>();
}

public void push(int x) { //插入元素,同时记录当前最小的元素
stack.addLast(x);
minStack.addLast(minStack.isEmpty() ? x : Math.min(minStack.getLast(), x));
}

public void pop() { // 同步更新最小值
stack.removeLast();
minStack.removeLast();
}

public int top() {
return stack.getLast();
}

public int min() {
return minStack.getLast();
}

31. 栈的压入、弹出序列

剑指 Offer 31. 栈的压入、弹出序列 - 力扣(LeetCode)

一个序列是否为栈的弹出序列的规律:

  1. 如果当前栈顶的数字是下一个弹出的数字,则弹出;
  2. 如果当前栈顶的数字不是下一个弹出的数字,把还未入栈的数字压入栈中,直至栈顶的数字是下一个弹出的数字
  3. 如果所有数字都入栈的过程中,栈顶数字始终不是需要弹出的数字,则不可能是弹出序列
  4. 栈中数字全部顺利出栈,则为弹出序列
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public boolean validateStackSequences(int[] pushed, int[] popped) {
if(pushed.length == 0)
return true;
LinkedList<Integer> stack = new LinkedList<> ();
int i = 0;
for(int num : pushed) { // 遍历入栈顺序数组
stack.addFirst(num); // 入栈
while(!stack.isEmpty() && stack.getFirst() == popped[i]) { // 当前栈不为空,且 栈顶元素为出栈元素时
stack.removeFirst(); // 出栈
i++;
}
}
return stack.isEmpty(); // 如果都顺利出栈,则说明弹出序列是正确的
}

时间复杂度:$O(n)$(每个元素最多进栈1次,出栈1次,$O(2n)$)​;空间复杂度:$O(n)$


40. 最小的K个数

剑指 Offer 40. 最小的k个数 - 力扣(LeetCode)

快速排序每次都能将选定的哨兵置于排序完成后的最终位置,当前的哨兵最终位置索引为K时,比它小的K个数将全在左侧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public int[] getLeastNumbers(int[] arr, int k) {
if (k >= arr.length)
return arr; //不限制输出顺序
return quickSort(arr, k, 0, arr.length - 1);
}

private int[] quickSort(int[] arr, int k, int left, int right) {
int i = left, j = right;
while (i < j) { // 分别从左右开始,直至"相遇";默认取第一个元素为哨兵(arr[left])
while (i < j && arr[j] >= arr[left]) { // 从右往左,找到比哨兵小的数(且保证左右未相遇)
j--;
}
while (i < j && arr[i] <= arr[left]) { // 从左往右,找到比哨兵大的数
i++;
}
swap(arr, i, j); // 交换位置(比哨兵小的放在左边,大的放在右边)
}
swap(arr, i, left); // 将哨兵置于最终位置i
// 【递归判断】优化时间复杂度的关键
if (i > k) { // 确认最终位置的哨兵,其左侧的数 > k,比最终需要的数多,继续递归
return quickSort(arr, k, left, i - 1);
}
if (i < k) {
return quickSort(arr, k, i + 1, right);
}
return Arrays.copyOf(arr, k); //哨兵左侧的数 = k 个,复制数组左侧的k个数,无需再进行完整的排序
}

private void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
  • 时间复杂度$O(n)$: 其中n为数组元素数量;对于长度为n的数组执行哨兵划分操作的时间复杂度为$O(N)$;每轮哨兵划分后根据k和i的大小关系选择递归,由于i分布的随机性,则向下递归子数组的平均长度为$\frac{N}{2}$;因此平均情况下,哨兵划分操作一共有$N + \frac{N}{2} + \frac{N}{4} + … + \frac{N}{N} = \frac{N - \frac{1}{2}}{1 - \frac{1}{2}} = 2N - 1$(等比数列求和),即总体时间复杂度为$O(n)$。
  • 空间复杂度$O(\log n)$:划分函数的平均递归深度为$O(\log n)$。

41. 数据流中的中位数

剑指 Offer 41. 数据流中的中位数 - 力扣(LeetCode)

  • 通过建立一个大顶堆和一个小顶堆PriorityQueue),将数据流拆分为两部分,根据两个堆顶元素就能得到中位数

  • 添加元素过程中,要保证两个堆的大小平衡

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private Queue<Integer> minHeap, maxHeap;

public MedianFinder() {
minHeap = new PriorityQueue<>(); // 默认构造小顶堆,保存较大的一半
maxHeap = new PriorityQueue<>((x, y) -> (y - x)); //lambda表达式,大顶堆,保存较小的一半
}

public void addNum(int num) {
if (minHeap.size() == maxHeap.size()) { // 添加新的整数后,会是奇数个数值
maxHeap.offer(num);
minHeap.offer(maxHeap.poll()); //取出最大堆中最大的数放入最小堆中 --> 中位数在最小堆的堆顶
} else { // 添加新的整数后,会是偶数个数值(奇数个数值时,最小堆的数值更多)
minHeap.offer(num);
maxHeap.offer(minHeap.poll()); // 取出最小堆中的最大数放入最大堆
}
}

public double findMedian() {
return minHeap.size() == maxHeap.size() ? (minHeap.peek() + maxHeap.peek()) / 2.0 : minHeap.peek();
}

时间复杂度:

  • 查找中位数$O(1)$:获取堆顶元素使用$O(1)$时间;
  • 添加数字$O(\log N)$:堆的插入和弹出操作使用$O(\log N)$时间。

空间复杂度:$O(N)$


59.1 滑动窗口的最大值

剑指 Offer 59 - I. 滑动窗口的最大值(单调队列,清晰图解) - 滑动窗口的最大值 - 力扣(LeetCode)

如何在窗口每次滑动时,获取最大值?使用双端队列存储当前滑动窗口中的最大值以及在后续窗口中潜在的最大值

每轮窗口滑动:

  1. 如果滑出窗口的数是队首数字,则队首数字也出队(队列的大小必然<=窗口大小
  2. 如果新进入窗口的数比队尾的数大,说明队尾的数不可能成为某个窗口的最大值。将队尾元素移除,直到队尾的数字>=新进入窗口的数队列已空
  3. 如果新进入窗口的数比队尾的数小,说明等之前的数滑出窗口后有可能会成为后续窗口中的最大值,因此,直接加入队尾
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
private Deque<Integer> deque;
public int[] maxSlidingWindow(int[] nums, int k) {
if(nums.length == 0) {
return new int[0];
}
int[] result = new int[nums.length - k + 1];
int index = 0;
deque = new LinkedList<>(); //双端队列,存储每个窗口中的最大值
// 先形成一个窗口
for(int i = 0; i < k; i++) {
while(!deque.isEmpty() && deque.getLast() < nums[i]) { // 队列不为空,且队尾元素 < 当前进入窗口的数字
deque.removeLast();
}
deque.addLast(nums[i]);
}
result[index++] = deque.getFirst(); // 第一个窗口中的最大值入队
// 窗口形成后
for(int i = k; i < nums.length; i++) {
if(nums[i - k] == deque.getFirst()) // 如果滑出窗口的数是队首数字
deque.removeFirst(); // 队首数字也出队
while(!deque.isEmpty() && deque.getLast() < nums[i]) { // 队列不为空,且队尾元素 < 当前进入窗口的数字
deque.removeLast();
}
deque.addLast(nums[i]);
result[index++] = deque.getFirst();
}
return result;
}

时间复杂度:$O(n)$

空间复杂度:$O(n)$


双指针

57.1 和为S的两个数字

剑指 Offer 57. 和为s的两个数字 - 力扣(LeetCode)

递增排序的数组,查找两个数使得和为s——双指针

1
2
3
4
5
6
7
8
9
10
11
12
13
public int[] twoSum(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while(left < right) {
if(nums[left] + nums[right] == target) {
return new int[] {nums[left], nums[right]};
} else if (nums[left] + nums[right] < target) { //需要更大的数
left++;
} else (nums[left] + nums[right] > target) { //需要更小的数
right--;
}
}
return new int[0];
}

时间复杂度:$O(n)$

空间复杂度:$O(1)$


57.2 和为s的连续正数序列

剑指 Offer 57 - II. 和为s的连续正数序列 - 力扣(LeetCode)

连续正整数序列 & 求和——双指针

  • 遍历的终点:序列中最小的数 * 2 + 1 > target

  • 双指针可以模拟滑动窗口

    滑动窗口的右指针无需向左移动,必然能遍历所有解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public int[][] findContinuousSequence(int target) {
int left = 1, right = 2;
List<int[]> list = new ArrayList<>();
int sum = 3; // 初始滑动窗口中数字的和
while (left <= target / 2) {
if (sum == target) {
int[] result = new int[right - left + 1];
for(int i = 0; i < right - left + 1; i++) {
result[i] = left + i;
}
list.add(result);
}
if (sum < target) { // 向右移动右指针
sum += ++right;
} else { // 连续正数的和 >= target
sum -= left++; //向右移动左指针
}
}
return list.toArray(new int[0][]); //参数为 返回的数组类型(存储数组的数组),不指定则为Object
}

时间复杂度:$O(n)$

空间复杂度:$O(n)$


58.1 翻转单词顺序列

剑指 Offer 58 - I. 翻转单词顺序 - 力扣(LeetCode)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public String reverseWords(String s) {
s = s.trim(); // 去除首尾空格
StringBuilder sb = new StringBuilder();
int i = s.length() - 1;
int j = i;
while (i >= 0) {
while(i >= 0 && s.charAt(i) != ' ') { //找到句中空格或遍历结束
i--;
}
sb.append(s.substring(i+1, j + 1) + ' ');
while(i >= 0 && s.charAt(i) == ' ') { //跳过多余的空格
i--;
}
j = i;
}
return sb.toString().trim();
}

时间复杂度:$O(n)$

空间复杂度:$O(n)$


58.2 左旋转字符串

剑指 Offer 58 - II. 左旋转字符串 - 力扣(LeetCode)

1
2
3
4
5
6
public String reverseLeftWords(String s, int n) {
StringBuilder sb = new StringBuilder();
sb.append(s.substring(n, s.length()));
sb.append(s.substring(0, n));
return sb.toString();
}

时间复杂度:$O(n)$

空间复杂度:$O(n)$


链表

6. 从尾到头打印链表

剑指 Offer 06. 从尾到头打印链表 - 力扣(LeetCode)

1
2
3
4
5
6
7
8
9
10
11
12
public int[] reversePrint(ListNode head) {
List<Integer> list = new ArrayList<>();
while(head != null) {
list.add(head.val);
head = head.next;
}
int[] result = new int[list.size()];
for(int i = result.length - 1; i >= 0; i--) {
result[i] = list.get(result.length - 1 - i);
}
return result;
}

时间复杂度:$O(n)$

空间复杂度:$O(n)$


18. 删除链表的节点

剑指 Offer 18. 删除链表的节点 - 力扣(LeetCode)

  • 无需存储所有节点,找到被删的节点,修改前一个节点的next节点即可——双指针
1
2
3
4
5
6
7
8
9
10
11
12
13
public ListNode deleteNode(ListNode head, int val) {
if(head.val == val) { // 头结点即为被删的节点
return head.next;
}
ListNode pre = head;
ListNode cur = pre.next;
while(cur.val != val) {
pre = cur;
cur = cur.next;
}
pre.next = cur.next;
return head;
}

时间复杂度:$O(n)$

空间复杂度:$O(1)$​


22. 链表中倒数第K个节点

剑指 Offer 22. 链表中倒数第k个节点 - 力扣(LeetCode)

双指针

1
2
3
4
5
6
7
8
9
10
11
12
public ListNode getKthFromEnd(ListNode head, int k) {
ListNode right = head;
ListNode left = head;
for(int i = 0; i < k; i++) { //只要让第二个指针落后于第一个指针k位,遍历完成时,即能得到倒数第k个节点
right = right.next;
}
while(right != null) {
right = right.next;
left = left.next;
}
return left;
}

时间复杂度:$O(n)$

空间复杂度:$O(1)$


23. 链表中环的入口节点

剑指 Offer II 022. 链表中环的入口节点 - 力扣(LeetCode)

方式一:利用HashSet存储ListNode,判断后续遍历的节点是否已经在集合中。时间复杂度:$O(n\log n)$;空间复杂度$O(n)$。

方式二一快一慢的双指针

  • 一快一慢的指针,让快的速度是慢的2倍如果存在环,快的指针将会追上慢的指针

  • 指针到达相同节点后,如何得知入口节点

    当前指针继续往前走向入口节点的距离 == 头结点到达入口节点的距离

    假设环入口节点为$y_1$,相遇所在节点为$z_1$。(头结点到达入口节点的距离为$X$,入口节点到达相遇节点的距离为$Y$,相遇节点到达入口节点的距离为$Z$)

    假设快指针 fast 在圈内绕了$N$圈,则总路径长度为$X+NY+(N-1)Z$​。$Z$为$N-1 $倍是因为快慢指针最后已经在$z_1$节点相遇了,后面就不需要再走了。

    而慢指针slow总路径长度为$X+Y$。

    因为快指针是慢指针的两倍,因此$X+NY+(N-1)Z = 2(X+Y)$。

    我们要找的是环入口节点$y_1$​​​,也可以看成寻找长度$X$​​的值,因此我们先将上面的等式转换为:$X=(N-2)Y+(N-1)Z = (N-2)(Y+Z)+Z$​​​。$Y+Z$​​​是圆环的总长度右边可以看作从相遇点$z_1$​开始在圆环中走过$N-2$​圈,再走长度为$Z$​​​的长度可以发现如果让两个指针同时从起点$x_1$​和相遇点$z_1$​开始,每次只走过一个距离,那么最后他们会在环入口节点相遇

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public ListNode detectCycle(ListNode head) {
ListNode fast = head; // 创建一快一慢的指针,快的速度是慢的2倍
ListNode slow = head;
while(fast != null && slow != null) {
fast = fast.next.next; // 一次前进两个节点
slow = slow.next; // 一次前进一个节点
if(fast == slow) { // 快的指针追上了慢的指针,说明存在环
slow = head;
while (slow != fast) {
slow = slow.next;
fast = fast.next;
}
return fast;
}
}
return null;
}

时间复杂度:$O(n)$

空间复杂度:$O(1)$​


24. 反转链表

剑指 Offer 24. 反转链表 - 力扣(LeetCode)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public ListNode reverseList(ListNode head) {
ListNode cur = head; // 1
if(cur == null) {
return null;
}
ListNode nextNode = cur.next; // 2
head.next = null;
ListNode preNode = head; // 1
while(nextNode != null) {
cur = nextNode; // 2
nextNode = nextNode.next; // 3
cur.next = preNode; // 1
preNode = cur;
}
return cur;
}

时间复杂度:$O(n)$

空间复杂度:$O(1)$


25. 合并两个排序的链表

剑指 Offer 25. 合并两个排序的链表 - 力扣(LeetCode)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode head = new ListNode(1); // 创建一个任意的节点
ListNode cur = head;
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;
}
cur.next = l1 != null ? l1 : l2;
return head.next;
}

时间复杂度:$O(n)$

空间复杂度:$O(1)$


35. 复杂链表的复制

剑指 Offer 35. 复杂链表的复制 - 力扣(LeetCode)

  • 利用哈希表,构建原链表节点新链表对应节点映射关系。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public Node copyRandomList(Node head) {
Map<Node, Node> hm = new HashMap<>();
Node cur = head;
while(cur != null) { // 构建节点的映射
hm.put(cur, new Node(cur.val));
cur = cur.next;
}
cur = head;
while(cur != null) {
hm.get(cur).next = hm.get(cur.next); // 只有这样,才能获得新地址下的Node
hm.get(cur).random = hm.get(cur.random);
cur = cur.next;
}
return hm.get(head);
}

时间复杂度:$O(n)$

空间复杂度:$O(n)$


52. 两个链表的第一个公共节点

剑指 Offer 52. 两个链表的第一个公共节点 - 力扣(LeetCode)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
int step1 = 0, step2 = 0;
ListNode A = headA, B = headB;
while(A != null) {
step1++;
A = A.next;
}
while(B != null) {
step2++;
B = B.next;
}
int step = step1 - step2;
A = headA;
B = headB;
if(step < 0) { // 第一个链表比较短
while(step != 0) {
B = B.next;
step++;
}
} else if(step > 0) { // 第一个链表比较长
while (step != 0) {
A = A.next;
step--;
}
}
while (A != B) {
A = A.next;
B = B.next;
}
return A;
}

时间复杂度:$O(n+m)$

空间复杂度:$O(1)$


二分查找

11. 旋转数组的最小数字

剑指 Offer 11. 旋转数组的最小数字 - 力扣(LeetCode)


分治


数学

39. 数组中出现次数超过一半的数字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public int majorityElement(int[] nums) {
int target = nums[0], times = 1;
for(int i = 1; i < nums.length; i++) {
if (target != nums[i]) {
times--;
} else {
times++;
}
if (times == 0) {
target = nums[i];
times = 1;
}
}
return target;
}

时间复杂度:$O(n)$

空间复杂度:$O(1)$​


43. 1~n整数中1出现的次数

剑指 Offer 43. 1~n 整数中 1 出现的次数 - 力扣(LeetCode)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public int countDigitOne(int n) {
int digit = 1, res = 0;
int high = n / 10, cur = n % 10, low = 0; // 高位,当前位,低位;从个位开始统计
while(high != 0 || cur != 0) {
if(cur == 0) { // 当前位最大为0
res += high * digit; // 该位出现1的次数,由高位决定
} else if(cur == 1) { //当前位最大为1
res += high * digit + low + 1; // 该位出现1的次数 由高位和低位决定
} else { // 当前位最大 > 1
res += (high + 1) * digit;
}
low += cur * digit;
cur = high % 10;
high /= 10;
digit *= 10;
}
return res;
}