0%

LeetCode Array,LinkedList,SkipList

天才是长期劳动的结果。 —— 牛顿

包含大量LeetCode链表、数组相关题目及其解决思路和具体代码。

点击题目链接为中国站题目,页面附上英文题目。

对于链表,在操作上实际是有需要注意的地方的。

  1. 利用快慢指针(有时候需要用到三个指针)。例如,链表翻转、寻找倒数第k个元素、寻找链表中间元素、判断链表是否有环
  2. 构建一个虚假的链表头,一般用在需要返回一个新的链表的题目中。(整合两个有序链表、整合奇数偶数链表)。如果不创建空的链表头,每次都要多一个if else判断头结点是否为空。

第二点也可以理解成是一个小技巧:对于链表问题,返回结果为头结点时,通常需要先初始化一个预先指针 pre,该指针的下一个节点指向真正的头结点head。使用预先指针的目的在于链表初始化时无可用节点值,而且链表构造过程需要指针移动,进而会导致头指针丢失,无法返回结果

数组、链表、跳表

283.Move zeros(移动零)(Easy)

Given an array nums , write a function to move all 0’s to the end of it while maintaining the relative order of the non-zero elements.

Examples:

1
2
Input: [0,1,0,3,12]
Output: [1,3,12,0,0]

Note:

  1. You must do this in-place without making a copy of the array.
  2. Minimize the total number of operations.

解:这道题可以使用双指针思想很简洁代码解决。用i作为指针,只要之前元素不为0,就把它移动到j的位置上去,把i位置的元素赋值为0. 但是需要注意corner case, 如果传入的数据全都是非0,那么i和j始终相等,不能给任何元素赋值为0.

需要注意指针j什么时候递增,只要i指的位置不为零,j就要往后移动,j用来记录从左到右,下一个非0元素的位置.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution {
public void moveZeroes(int[] nums) {
int j = 0;
for(int i = 0; i < nums.length; i++) {
if(nums[i] != 0) {
nums[j] = nums[i];
if(i != j) {
nums[i] = 0;
}
j++;
}
}
}
}

时间复杂度:O(n),一重循环

空间复杂度:O(1),按照题目要求,不能使用额外数组空间

11.Container With Most Water(盛水最多的容器)(Mid)

Given n non-negative integers a1, a2, …, an , where each represents a point at coordinate (i, ai). n vertical lines are drawn such that the two endpoints of line i is at (i, ai) and (i, 0). Find two lines, which together with x-axis forms a container, such that the container contains the most water.

Note: You may not slant the container and n is at least 2.

The above vertical lines are represented by array [1,8,6,2,5,4,8,3,7]. In this case, the max area of water (blue section) the container can contain is 49.

Example

1
2
Input: [1,8,6,2,5,4,8,3,7]
Output: 49

解:方法一,枚举。记录左边bar x, 右边 bar y,遍历每一个x和y的组合,计算(x-y)*height_diff,但是时间复杂度太高了,O(n^2),显然不是最优解法。

但是这里可以回顾一下遍历数组的固定写法,形成机械记忆,一定要熟悉。

正常遍历一维数组:

1
for(int i = 0; i < height.length; i++) {

遍历二维数组,实现 i 和 j 两个下标对数组的遍历,而且 i 和 j 不会重复

1
2
3
4
	for(int i = 0; i < height.length-1; i++) {
for(int j = i+1; j < height.length; j++) {
}
}

这一段二维数组的遍历应当形成肌肉式记忆。 整个遍历过程中,j 总是比 i 大,最后j到了最后一个元素,i还在前面。当然,这种暴力的方式很慢,开销很大。

方法二,从左边和右边开始往中间收敛(或者理解是左右往中间夹逼)。因为肯定是外面的棒子组成的面积更大。如果内部的棒子高度也不如外面,那么组成的面积肯定比外面小,这里可以看做是一个tricky的点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public int maxArea(int[] height) {
if(height.length == 0) return 0;

int max = 0;
//循环初始条件巧妙,很简洁
for(int i = 0, j = height.length-1; i < j;) {
// 这句话真的很巧妙,也侧面利用了类似i++和++i的语法内容,把下标的挪动放在了比较选择更低的那一端,会更舒服
int minHeight = (height[i] < height[j]) ? height[i++] : height[j--];
//这里加一是一因为经过上面选择最小内容之后的操作,两个点之间的宽度减小了1,这里加回来
int area = (j-i+1) * minHeight;
max = Math.max(area,max);
}
return max;
}
}

i 和 j 是左下标和右下标,哪个棒子更矮,就挪动哪个。

时间复杂度:O(n)

空间复杂度:O(1)

1. Two Sum(两数之和)(Easy)

Given an array of integers, return indices of the two numbers such that they add up to a specific target.

You may assume that each input would have exactly one solution, and you may not use the same element twice.

Example:

1
2
3
4
Given nums = [2, 7, 11, 15], target = 9,

Because nums[0] + nums[1] = 2 + 7 = 9,
return [0, 1].

解:这里给出两个解法,第一个无脑双指针遍历,第二个基于哈希表,是正解。

方法一,两重循环,枚举下标,如果下标对应的内容相加可以达成target,那么完成。但是这种方法时间复杂度较大,O(n^2)

直接可以利用之前讲过的双指针遍历的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public int[] twoSum(int[] nums, int target) {

int[] result = new int[2];

for(int i = 0; i < nums.length-1; i++) {
for(int j = i+1; j < nums.length; j++) {
if(nums[i] + nums[j] == target) {
result[0] = i;
result[1] = j;
}
}
}
return result;
}
}

这段代码可以通过,但是性能怎么样?——某次LeetCode的统计结果是,击败8%用户。所以如果你面试只想到了这个方法,刷刷写完了,没后续了,还很得意,那对不起,你的面试肯定过不了。除非其他候选者连这个方法都没想到。

方法二,保持数组中的每个元素与其索引相互对应的最好方式是什么?当然是基于数组的哈希表。利用哈希表先把内容存储起来,这样一来查找的过程只需要O(1)了。

这个map中,key是nums元素的值,value是该元素的下标。因为要找到a+b = target,所以 a = target - b,所以查找 target-a 在不在数组里面即可。

注意

  1. 这里是把元素当做key,该元素的位置当做value。
  2. 虽然看起来 map.put(nums[i], i) 这句放在循环体里的前面和后面都可以,但是其实是不能放在前面的,否则如果某个元素的值是target的二分之一,先把它添加进map之后再比较,会直接比较到它自己。换句话说,必须先比较,再put进map。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public int[] twoSum(int[] nums, int target) {

int[] result = new int[2];
Map<Integer, Integer> map = new HashMap<>();

for(int i = 0; i < nums.length; i++) {
if(map.containsKey(target - nums[i])) {
result[0] = i;
result[1] = map.get(target - nums[i]);
break;
}
// 注意这里put不能放在判断前面,必须先比较再放入。否则如果某个元素是target的一半,先添加再比较的话,会比较到它自己。
map.put(nums[i],i);
}
return result;
}
}

167. Two Sum - II(两数之和 II - 输入有序数组)(Easy)

给定一个已按照升序排列 的有序数组,找到两个数使得它们相加之和等于目标数。

函数应该返回这两个下标值 index1 和 index2,其中 index1 必须小于 index2。

说明:

返回的下标值(index1 和 index2)不是从零开始的。
你可以假设每个输入只对应唯一的答案,而且你不可以重复使用相同的元素。
示例:

输入: numbers = [2, 7, 11, 15], target = 9
输出: [1,2]
解释: 2 与 7 之和等于目标数 9 。因此 index1 = 1, index2 = 2 。


解:

因为输入的数组是有序的了,所以做法和第一题的两数之和完全不同了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public int[] twoSum(int[] numbers, int target) {
int i = 0;
int j = numbers.length - 1;
while (i < j) {
int sum = numbers[i] + numbers[j];
if (sum < target) {
i++;
} else if (sum > target) {
j--;
} else {
return new int[]{i+1, j+1};
}
}
return new int[]{-1, -1};
}

15. 3Sum(三数之和)(Mid)

Given an array nums of n integers, are there elements a, b, c in nums such that a + b + c = 0? Find all unique triplets in the array which gives the sum of zero.

Note:
The solution set must not contain duplicate triplets.(答案不能包含重复三元组)

Example:

1
2
3
4
5
6
7
Given array nums = [-1, 0, 1, 2, -1, -4],

A solution set is:
[
[-1, 0, 1],
[-1, -1, 2]
]

解:这道三数之和其实是第一题两数之和的升级版本。

方法一:暴力求解,和第一题非常相似,但是时间复杂度为O(n^3),非常高。

方法二,用类似第一题的哈希表,时间复杂度可以降到O(n^2)

方法三,用左右下标夹逼的方法。其实很难直接想到,比较tricky。

方法三的解法详解:点击这里

需要注意题目输入的nums中可能有重复的元素;最后返回的答案不能有重复的三元组。

步骤:

  1. 排序(排好序之后非常利于判重)
  2. 循环,因为有3个指针,循环length-2即可
  3. 设置判断条件,初始化指针位置
  4. 遍历找符合条件的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
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
Arrays.sort(nums);// 默认有小到大排序,先排序
List<List<Integer>> res = new LinkedList<>();
int len = nums.length;
// 有三个指针,循环 nums.length-2 次就够了
for(int i = 0; i < len-2; i++) {
if(i == 0 || (i > 0 && nums[i] != nums[i-1])) { //第二个条件是去重
int lo = i+1, hi = len-1, sum = 0-nums[i];
while(lo < hi) {
if(nums[lo] + nums[hi] == sum) {
// asList是Arrays的工具类,能把数组转化成List,但是注意只能转换包装类,不能转换基本数据类型
// 此外asList不支持add和remove方法。
// 把所有数组里的内容丢到asList的参数里面来直接把几个数变成list,看得出技巧熟练
res.add(Arrays.asList(nums[i], nums[lo],nums[hi]));
while(lo < hi && nums[lo] == nums[lo+1]) lo++;
while(lo < hi && nums[hi] == nums[hi-1]) hi--;
lo++;hi--;
} else if(nums[lo] + nums[hi] < sum) lo++; // lo++ 把和变大
else hi--; // hi-- 把和变小
}
}
}
return res;
}
}

206. Reverse LinkedList(反转链表)(Easy)

Reverse a singly linked list.

Example:

1
2
Input: 1->2->3->4->5->NULL
Output: 5->4->3->2->1->NULL

Follow up:

A linked list can be reversed either iteratively or recursively. Could you implement both?


解:就按照题目,给出迭代(iteratively)和递归(recursively)两种方法的解法。

方法一:栈

这个方法是参考Kevin大神的思路,因为要反转整个链表,所以最直接的想法就是依次遍历整个链表之后把每一个节点都放到栈里,然后再反过来取出来。但是这样的做法会使得空间复杂度很高,这点可以在方法二的迭代中得到优化。

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
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode reverseList(ListNode head) {
Stack<ListNode> stack = new Stack<>();
while(head != null) {
stack.push(head);
head = head.next;
}
ListNode dummyNode = new ListNode(-1);
head = dummyNode;
while(!stack.isEmpty()) {
// 注意这里需要用current接住pop出来的节点,否则
// pop()之后就没了
ListNode current = stack.pop();
head.next = new ListNode(current.val);
head = head.next;
}
return dummyNode.next;
}
}

方法二:迭代

可以直接利用head,也只需要新定义两个ListNode

具体相等操作的过程可以记成修改指针指向的四步走。

总的来说,head总是在newHeader的next的位置,nextTemp总是在head的next的位置。

反转的四步:

  1. 用nextTemp记录保存head.next
  2. 将head.next往前指,即指向newHeader
  3. 将newHeader往前移动一个位置指到head
  4. 把head往前一个位置,让其与nextTemp位置相同。但是因为下一次循环nextTemp又会在head.next的位置,所以不影响后面的反转。

当跳出循环的时候,head为null,newHeader为反转后的链表的头结点,返回newHeader即可,不像方法一那样从0重新构建一个链表然后返回dummyNode.next。

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
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode reverseList(ListNode head) {
ListNode newHeader = null;

// 最后返回的时候head为null,返回newHeader即可
while(head != null) {
// 第一步,记录head的next,避免head.next丢失
ListNode nextTemp = head.next;
// 第二部,翻转操作,把原先的head.next指向前面
head.next = newHeader;
// newHeader指向旧的head的位置
newHeader = head;
// head往后移动
head = nextTemp;
}
return newHeader;
}
}

方法三:递归

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode reverseList(ListNode head) {

if(head == null || head.next == null) return head;

ListNode newHeader = reverseList(head.next);
head.next.next = head;
head.next = null;
return newHeader;
}
}

针对递归方法值得注意的是,因为newHeader没有被动过,所以在一开始递归结束的时候,就返回的是反转之后的链表的新的head,也就是这个newHeader,所以最后的结果就是每一层的递归返回的都是一开始递归返回的newHeader,问题就迎刃而解了。

92. Reverse LinkedList II(反转链表II)(Mid)

Reverse a linked list from position m to n. Do it in one-pass.

Note: 1 ≤ m ≤ n ≤ length of list.

Example:

1
2
Input: 1->2->3->4->5->NULL, m = 2, n = 4
Output: 1->4->3->2->5->NULL

解:和数组不同,这里的反转下标从1开始。需要初始化一个dummy节点代表0号节点,然后开始遍历。

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
36
37
38
39
40
41
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode reverseBetween(ListNode head, int m, int n) {
if(head == null) return null;

// create a dummy node to mark the head of this list
ListNode dummy = new ListNode(0);
dummy.next = head;

// make a pointer pre as a marker for the node before reversing
ListNode pre = dummy;

for(int i = 0; i < m-1; i++) pre = pre.next;

// a pointer to the beginning of a sub-list that will be reversed
ListNode start = pre.next;
// a pointer to a node that will be reversed
ListNode then = start.next;

// 1 - 2 -3 - 4 - 5 ; m=2; n =4 ---> pre = 1, start = 2, then = 3
// dummy-> 1 -> 2 -> 3 -> 4 -> 5

for(int i=0; i < n-m; i++) {
start.next = then.next;
then.next = pre.next;
pre.next = then;
then = start.next;
}

// first reversing : dummy->1 - 3 - 2 - 4 - 5; pre = 1, start = 2, then = 4
// second reversing: dummy->1 - 4 - 3 - 2 - 5; pre = 1, start = 2, then = 5 (finish)
return dummy.next;
}
}

141. Linked List Cycle(环形链表)(Easy)

Given a linked list, determine if it has a cycle in it.

To represent a cycle in the given linked list, we use an integer pos which represents the position (0-indexed) in the linked list where tail connects to. If pos is -1, then there is no cycle in the linked list.

Example 1:

1
2
3
4
Input: head = [3,2,0,-4], pos = 1
Output: true
Explanation: There is a cycle in the linked list,
where tail connects to the second node.

Example 2:

1
2
3
Input: head = [1,2], pos = 0
Output: true
Explanation: There is a cycle in the linked list, where tail connects to the first node.

Example 3:

1
2
3
Input: head = [1], pos = -1
Output: false
Explanation: There is no cycle in the linked list.

Follow up:

Can you solve it using O(1) (i.e. constant) memory?


解:题目中虽然说会用pos明显标出成环的链表情况,而且貌似人用眼睛可以非常直接地发现,pos只要不是-1就一定有环,但是传入的参数没有pos,不能直接使用它。

判断链表成环可以有思维上的巧妙性,主要是快慢指针的使用。

解法一,用哈希表,但是不符合进阶要求,时间复杂度和空间复杂度都是O(n)

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
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public boolean hasCycle(ListNode head) {
Set<ListNode> set = new HashSet<>();

while(head != null) {
if(set.contains(head)) {
return true;
} else {
set.add(head);
head = head.next;
}
}
return false;
}
}

方法二,如果没有了解,很难想到的双指针法(其实之前很多题目已经用到了双指针,比如283题移动零、11题盛水容器、15题三数之和等。三数之和甚至用到了三指针。)

时间复杂度为O(n),但是符合空间复杂度为O(1)的要求因为它是在原有的链表上进行操作。

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
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public boolean hasCycle(ListNode head) {
// 一开始考虑边界条件,head或head.next可能为空,那么slow或fast就有一个
// 会是空指针异常了。这个边界条件考虑得很重要
if(head == null || head.next == null) {
return false;
}
ListNode slow = head;
ListNode fast = head.next;
while(slow != fast) {
// 只要链表有环,fast和fast.next一定不会为null,会一直走下去
// 不可能跳出false
if(fast == null || fast.next == null) {
return false;
}
slow = slow.next;
fast = fast.next.next;
}
return true;
}
}

142. Linked List Cycle II(环形链表II)(Mid)

Given a linked list, return the node where the cycle begins. If there is no cycle, return null.

To represent a cycle in the given linked list, we use an integer pos which represents the position (0-indexed) in the linked list where tail connects to. If pos is -1, then there is no cycle in the linked list.

Note: Do not modify the linked list.

Example 1:

1
2
3
Input: head = [3,2,0,-4], pos = 1
Output: tail connects to node index 1
Explanation: There is a cycle in the linked list, where tail connects to the second node.

Example 2:

1
2
3
Input: head = [1,2], pos = 0
Output: tail connects to node index 0
Explanation: There is a cycle in the linked list, where tail connects to the first node.

Example 3:

1
2
3
Input: head = [1], pos = -1
Output: no cycle
Explanation: There is no cycle in the linked list.

Follow-up:
Can you solve it without using extra space?


解:还是两个方法,快慢指针是环形链表相关的常用方法。

方法一,哈希表。虽然不符合进阶要求,但是可以解决。

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
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode detectCycle(ListNode head) {
Set<ListNode> visited = new HashSet<>();
ListNode node = head;
while( node != null) {
if(visited.contains(node)) {
return node;
}
visited.add(node);
node = node.next;
}
return null;
}
}

但是这个方法在空间上开辟了新的空间给哈希表。

方法二,快慢指针,空间复杂度为O(1)。和单独判断链表是否成环的操作类似,第一次先让fast和slow指针跑一遍,如果fast或fast.next为null了,就证明不存在成环,返回null。

如果相遇了,证明有环。此时怎么找到环的起点呢?

此时让fast重新从head开始遍历,但是这回fast一次走一步,这样的话当fast和slow再次相遇的时候,它们俩都指向的节点,就是环开始的节点。

这个原因可以用数学公式推到的。因为fast每次走2步(假设fast指针一共走了f步),slow每次走1步(假设slow指针一共走了s步),所以两者相遇的时候fast总共走了2s步(f = 2s),而fast又比slow多走了n个环的周长(不一定是1个环的长度,可能f已经走过了很多次环s才和f相遇),所以f=s+nb

两式相减,得到:f = 2nb, s = nb,即fastslow分别走了2n,n个环的周长。

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
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode detectCycle(ListNode head) {
// 方法二,快慢指针,也叫Floyd算法
ListNode fast = head, slow = head;
while(true) {
if(fast == null || fast.next == null) return null;
fast = fast.next.next;
slow = slow.next;
if(fast == slow) break;
}
// 第一次相遇之后,让fast回到head,再一次走一步,这样当slow和fast再相遇的时候,slow位置就是环入口
fast = head;
while(fast != slow) {
fast = fast.next;
slow = slow.next;
}
return slow;
}
}
  • 时间复杂度:O(N),第二次相遇中,慢指针须走步数 a < a + b;第一次相遇中,慢指针须走步数 a + b - x < a + b其中 x 为双指针重合点与环入口距离;因此总体为线性复杂度;
  • 空间复杂度:O(1):双指针使用常数大小的额外空间。

24. Swap Nodes in Pairs(两两交换链表中的节点)(Mid)

Given a linked list, swap every two adjacent nodes and return its head.

You may not modify the values in the list’s nodes, only nodes itself may be changed.

Example:
Given 1->2->3->4, you should return the list as 2->1->4->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
29
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode swapPairs(ListNode head) {
// 递归

// terminator
if(head == null || head.next == null){
return head;
}

// process current logic
ListNode node = head.next;

// drill down
head.next = swapPairs(node.next);

// reverse current state
node.next = head;

return node;
}
}

方法二,非递归:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode swapPairs(ListNode head) {
ListNode pre = new ListNode(0);
ListNode temp = pre;
pre.next = head;
while(temp.next != null && temp.next.next != null) {
ListNode start = temp.next;
ListNode end = temp.next.next;
temp.next = end;
start.next = end.next;
end.next = start;
temp = start;
}
return pre.next;
}
}

相比于递归,非递归的写法实际对链表指针的操作更明显,需要注意在改变指针的过程中需要利用临时指针temp.next和temp.next.next分别定义start和end的位置。将start和end指向的节点反转之后将temp位置改变再重新定义start和end的位置从而重新操作。

具体过程可以如下图所示:

ldDjSA.jpg

25. Reverse Nodes in k-Group(k个一组翻转链表)(Hard)

Given a linked list, reverse the nodes of a linked list k at a time and return its modified list.

k is a positive integer and is less than or equal to the length of the linked list. If the number of nodes is not a multiple of k then left-out nodes in the end should remain as it is.

Example:

Given this linked list: 1->2->3->4->5

For k = 2, you should return: 2->1->4->3->5

For k = 3, you should return: 3->2->1->4->5

Note:

  • Only constant extra memory is allowed.

  • You may not alter the values in the list’s nodes, only nodes itself may be changed.


解:提供递归和非递归两种解法。

方法一,递归

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
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode reverseKGroup(ListNode head, int k) {
ListNode curr = head;
int count = 0;
while(curr != null && count != k) { // To find the k+1 node
curr = curr.next;
count++;
}

if(count == k) {
curr = reverseKGroup(curr, k); // reverse list with k+1 node as head
// head - head-pointer to direct part
// curr - head-pointer to reversed part
while(count-- > 0) { // reverse the k-group
ListNode tmp = head.next; //tmp - next head in direct part
head.next = curr; // preappending "direct" head to the reversed list
curr = head; //move head of reversed part to a new node
head = tmp; // move ""direct" head to the next node in direct part
}
head = curr;
}
return head;
}
}

递归写法在找到k组节点之后的反转操作和单纯的链表翻转的节点指针的改变操作相同,即把第206题翻转链表非递归写法提取了出来。

方法二,非递归

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
36
37
38
39
40
41
42
43
44
45
46
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode reverseKGroup(ListNode head, int k) {
ListNode dummy = new ListNode(0);
dummy.next = head;

ListNode pre = dummy;
ListNode end = dummy;

while(end.next != null) {
for(int i = 0; i < k && end != null; i++) {
end = end.next;
}
if(end == null) {
break;
}
ListNode start = pre.next;
ListNode next = end.next;
end.next = null;
pre.next = reverse(start);
start.next = next;
pre = start;
end = pre;
}
return dummy.next;
}

private ListNode reverse(ListNode head) {
ListNode prev = null;
ListNode curr = head;
while(curr != null) {
ListNode next = curr.next;
curr.next = prev;
prev = curr;
curr = next;
}
return prev;
}
}

详细题解:
点击这里

26. Remove Duplicates from Sorted Array(删除排序数组中的重复项)(Easy)

Given a sorted array nums, remove the duplicates in-place(原地操作) such that each element appear only once and return the new length.

Do not allocate extra space for another array, you must do this by modifying the input array in-place with O(1) extra memory.

Example 1:

1
2
3
4
5
Given nums = [1,1,2],

Your function should return length = 2, with the first two elements of nums being 1 and 2 respectively.

It doesn't matter what you leave beyond the returned length.

Example 2:

1
2
3
4
5
Given nums = [0,0,1,1,1,2,2,3,3,4],

Your function should return length = 5, with the first five elements of nums being modified to 0, 1, 2, 3, and 4 respectively.

It doesn't matter what values are set beyond the returned length.

Clarfication:
Confused why the returned value is an integer but your answer is an array?

Note that the input array is passed in by reference, which means modification to the input array will be known to the caller as well.

Internally you can think of this:

1
2
3
4
5
6
7
8
// nums is passed in by reference. (i.e., without making a copy)
int len = removeDuplicates(nums);

// any modification to nums in your function would be known by the caller.
// using the length returned by your function, it prints the first len elements.
for (int i = 0; i < len; i++) {
print(nums[i]);
}

解:

方法一,快慢指针:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public int removeDuplicates(int[] nums) {
if(nums.length == 0) return 0;
// 删除重复项之后的数组下标为after_numsLength
int after_numslength = 0;
for(int i = 0; i < nums.length; i++) {
// i在前,after_numslength在后,nums[after_numslength]为去重之后的数组
if(nums[after_numslength] != nums[i]) {
after_numslength++;
nums[after_numslength] = nums[i];
}
}
// 返回长度,为下标最大值+1
return after_numslength+1;
}
}

方法二,i表示删除元素后数组元素的个数,注意因为i是元素个数,所以判断元素是否重复的时候比较的是”n != nums[i-1]”

1
2
3
4
5
6
7
8
9
10
11
class Solution {
public int removeDuplicates(int[] nums) {
int i = 0;
for(int n : nums) {
if(i == 0 || nums[i-1] != n) {
nums[i++] = n;
}
}
return i;
}
}

189. Rotate Array(旋转数组)(Easy)

Given an array, rotate the array to the right by k steps, where k is non-negative.

Example 1:

1
2
3
4
5
6
Input: [1,2,3,4,5,6,7] and k = 3
Output: [5,6,7,1,2,3,4]
Explanation:
rotate 1 steps to the right: [7,1,2,3,4,5,6]
rotate 2 steps to the right: [6,7,1,2,3,4,5]
rotate 3 steps to the right: [5,6,7,1,2,3,4]

Example 2:

1
2
3
4
5
Input: [-1,-100,3,99] and k = 2
Output: [3,99,-1,-100]
Explanation:
rotate 1 steps to the right: [99,-1,-100,3]
rotate 2 steps to the right: [3,99,-1,-100]

Note:

  • Try to come up as many solutions as you can, there are at least 3 different ways to solve this problem.

  • Could you do it in-place with O(1) extra space?


解:给出三种解法。

方法一,双重循环,比较暴力。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public void rotate(int[] nums, int k) {
// 按照轮次来做,每一轮操作一个数字
int len = nums.length;
// 如果k太大,甚至比nums还大,那么移动过程会经过了一轮。这里k表示最后要位移的位数
k %= len;
for(int i = 0; i < k; i++) {
//temp 保存最后一个元素的值
int temp = nums[len - 1];
for(int j = len-1; j > 0; j--) {
nums[j] = nums[j-1];
}
nums[0] = temp;
}
}
}

时间复杂度:O(kn)

空间复杂度:O(1)

方法二

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public void rotate(int[] nums, int k) {
int nums_length = nums.length;
k %= nums_length;
// 第一次交换完毕后,前 k 位数字位置正确,后 n-k 位数字中最后 k 位数字顺序错误,继续交换
for (int start = 0; start < nums.length && k != 0; nums_length -= k, start += k, k %= nums_length) {
for (int i = 0; i < k; i++) {
swap(nums, start + i, nums.length - k + i);
}
}
}

private void swap(int [] nums, int i, int j){
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}

时间复杂度:O(n)
空间复杂度:O(1)

方法三,翻转,最巧妙。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public void rotate(int[] nums, int k) {
int len = nums.length;
// 如果k太大,甚至比nums还大,那么移动过程会经过一轮,在这里k表示最后要位移的位数。
k %= len;
reverse(nums,0,len-1);
reverse(nums,0,k-1);
reverse(nums,k,len-1);

}

private void reverse(int[] nums, int start, int end) {
while(start < end) {
int temp = nums[start];
nums[start++] = nums[end];
nums[end--] = temp;
}
}
}

arr = [1,2,3,4,5] –右移两位–> [4,5,1,2,3]
假设 n = arr.length,k = 右移位数,可得:

[1,2,3,4,5] --翻转索引为[0,n-1]之间的元素--> [5,4,3,2,1] 
            --翻转索引为[0,k-1]之间的元素--> [4,5,3,2,1] 
            --翻转索引为[k,n-1]之间的元素--> [4,5,1,2,3]

注意上面这个区间貌似左和右都是闭合的

旋转数组其实就是把数组分成了两部分,解题关键就是在保证原有顺序的情况下
把后面一部分移到前面去。数组整体翻转满足了第二个要素,但是打乱了数组的
原有顺序,所以此时再次对两部分进行翻转,让他们恢复到原有顺序(翻转之后
再翻转,就恢复成原有顺序了)。没有什么太复杂的思想,但是这种很巧妙的思想或许是神来之笔。

21. Merge Two Sorted Lists(合并两个有序链表)(Easy)

Merge two sorted linked lists and return it as a new list. The new list should be made by splicing together the nodes of the first two lists.

Example:

1
2
Input: 1->2->4, 1->3->4
Output: 1->1->2->3->4->4

解:
方法一,非递归,操作指针。

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
36
37
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
// 创建合并后链表的虚拟头结点
ListNode dummyHead = new ListNode(0);
ListNode curr = dummyHead;

// 若链表不为空,开始合并,实际操作是插入节点
while(l1 != null && l2 != null) {
if(l1.val < l2.val) {
curr.next = l1;
l1 = l1.next;
curr = curr.next;
} else {
curr.next = l2;
l2 = l2.next;
curr = curr.next;
}
}

// 若l1或者l2有一个为空,则直接返回另一条链表
if(l1 == null) {
curr.next = l2;
} else {
curr.next = l1;
}

return dummyHead.next;
}
}

方法二,递归

分析递归的解法:

  • 终止条件:两条链表分别名为l1和l2,当l1为空或者l2为空是结束
  • 返回值:每一层调用都返回排好序的链表头
  • 本级递归内容:如果l1的val值更小,则将l1.next与排序好的链表头相接,l2同理

可以画图分析,具体分析可以参考这篇题解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
// 使用recursion,代码虽然简洁,但是可能导致Stack OverFlow
if(l1 == null) return l2;
if(l2 == null) return l1;
if(l1.val < l2.val) {
l1.next = mergeTwoLists(l1.next,l2);
return l1;
} else{
l2.next = mergeTwoLists(l1,l2.next);
return l2;
}
}
}

88. Merge Sorted Array(合并两个有序数组)(Easy)](https://leetcode-cn.com/problems/merge-sorted-array/)

Given two sorted integer arrays nums1 and nums2, merge nums2 into nums1 as one sorted array.

Note:

  • The number of elements initialized in nums1 and nums2 are m and n respectively.

  • You may assume that nums1 has enough space (size that is greater or equal to m + n) to hold additional elements from nums2.

Example:

1
2
3
4
5
Input:
nums1 = [1,2,3,0,0,0], m = 3
nums2 = [2,5,6], n = 3

Output: [1,2,2,3,5,6]

解:
方法一,调用自带函数,但是面试中肯定不允许使用,可以提一嘴

1
2
3
4
5
6
7
8
class Solution {
public void merge(int[] nums1, int m, int[] nums2, int n) {
// 方法一,使用java的arraycopy进行nums1和nums2的合并,合并之后再排序即可
System.arraycopy(nums2,0,nums1,m,n);
// 使用工具类Arrays的排序方法
Arrays.sort(nums1);
}
}

方法二,使用从后往前的指针,一共三个指针,分别在在nums1元素的最后位置、nums1长度的最后位置和nums2的最后位置。每次进行比较,直到nums1或nums2被遍历完为止。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public void merge(int[] nums1, int m, int[] nums2, int n) {
// 更加酷,不需要开辟额外空间,直接在nums1的末尾开始,用三个指针从后往前
// two pointers for nums1 and nums2
int p1 = m - 1;
int p2 = n - 1;
// set pointer for nums1
int p = m + n - 1;

// while there are still elements to compare
while((p1 >= 0) && (p2 >= 0)) {
// compare two elements from nums1 and nums2 and add the largest
// one in nums1
nums1[p--] = (nums1[p1] < nums2[p2]) ? nums2[p2--] : nums1[p1--];
}
// add missing elements from nums2
// 如果先触发了p1 < 0的条件,nums2不再能copy了,需要手动arraycopy过去
System.arraycopy(nums2, 0, nums1, 0, p2 + 1);
}
}

66. Plus One(加一)(Easy)

Given a non-empty array of digits representing a non-negative integer, plus one to the integer.

The digits are stored such that the most significant digit is at the head of the list, and each element in the array contain a single digit.

You may assume the integer does not contain any leading zero, except the number 0 itself.

Example 1:

1
2
3
Input: [1,2,3]
Output: [1,2,4]
Explanation: The array represents the integer 123.

Example 2:

1
2
3
Input: [4,3,2,1]
Output: [4,3,2,2]
Explanation: The array represents the integer 4321.

解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution {
public int[] plusOne(int[] digits) {
int len = digits.length;
for(int i = len - 1; i >= 0; i--) {
digits[i]++;
digits[i] %= 10;
if(digits[i] != 0) return digits;
}
// 若digits起初所有元素都为9,则需要新开辟一位
digits = new int[len + 1];
digits[0] = 1;
return digits;
}
}

83. Remove Duplicates from Sorted List(删除排序链表中的重复元素)(Easy)

Given a sorted linked list, delete all duplicates such that each element appear only once.

Example 1:

1
2
Input: 1->1->2
Output: 1->2

Example 2:

1
2
Input: 1->1->2->3->3
Output: 1->2->3

解:比较简单,只是考察操作链表节点指针的能力。因为传入的链表已经排序,我们只需要比较当前节点和它之后节点的值是否相等即可,如果相等,让当前节点指针跳过下一个节点,指向下一个节点的next即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode deleteDuplicates(ListNode head) {
ListNode curr = head;
while(curr != null && curr.next != null) {
if(curr.val == curr.next.val) {
curr.next = curr.next.next;
} else {
curr = curr.next;
}
}
return head;
}
}

时间复杂度:O(n)
空间复杂度:O(1)

82. Remove Duplicates from Sorted List II(删除排序链表中的重复元素 II)(Mid)

Given a sorted linked list, delete all nodes that have duplicate numbers, leaving only distinct numbers from the original list.

Example 1:

1
2
Input: 1->2->3->3->4->4->5
Output: 1->2->5

Example 2:

1
2
Input: 1->1->1->2->3
Output: 2->3

解:

方法一,非递归。

重复的元素直接彻底跳过而不是去重,难度上升变成中等。方法中定义虚拟节点FakeNode(用于处理开头节点就是重复节点的情况,最后一定要返回FakeNode.next)、pre节点(用于直接跳过重复的所有元素)、cur(判断当前节点是否为重复元素的节点)

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
36
37
38
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode deleteDuplicates(ListNode head) {
if(head == null ) return null;
ListNode FakeNode = new ListNode(0);
FakeNode.next = head;
ListNode pre = FakeNode;
ListNode cur = head;
while(cur != null ) {
// cur 标记是否当前节点为重复节点,用pre进行过滤,链起来一个新链表
// cur为重复元素的最后一个结点
// 不能把cur.next != null 放入大循环条件,否则可能报空指针
while(cur.next != null && cur.val == cur.next.val) {
cur = cur.next;
}

// 若pre的下一个节点就是cur,说明cur当前元素不是重复的,不需要跳过
// 反之,根据题意需要跳过cur指向的找到的重复的最后一个结点
if(pre.next == cur) {
pre = pre.next;
} else {
pre.next = cur.next;
}

// 相比83题简单地去重,这里需要判断cur重复之后还要删除重复元素,使用pre指针
// 所以不是简单地在判断元素重复之后直接进入下一元素,而是经过一个循环和一个判断之后才可以
cur = cur.next;
}
return FakeNode.next;
}
}

方法二,递归。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode deleteDuplicates(ListNode head) {
if(head == null ) return null;

if(head.next != null && head.val == head.next.val) {
while(head.next != null && head.val == head.next.val) {
head = head.next;
}
return deleteDuplicates(head.next);
} else {
head.next = deleteDuplicates(head.next);
}
return head;
}
}

203. Remove Linked List Elements(移除链表元素)(Easy)

Remove all elements from a linked list of integers that have value val.

Example:

1
2
Input:  1->2->6->3->4->5->6, val = 6
Output: 1->2->3->4->5

解:
方法一,递归

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode removeElements(ListNode head, int val) {
if(head == null) return null;

ListNode next = removeElements(head.next, val);

if(head.val == val) return next;

head.next = next;
return head;


}
}

方法二,非递归,迭代,定义虚拟节点

需要注意,如果第一个元素的值是val,无法返回头结点。所以自然想到需要dummyuNode(或者叫做FakeNode)。当遇到含有val的值的时候,比如2->6->3,val是6,此时需要赋值:2.next=2.next.next,和83题的操作同样很类似。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode removeElements(ListNode head, int val) {
ListNode fakeNode = new ListNode(0);
fakeNode.next = head;
ListNode curr = fakeNode;
while(curr != null && curr.next != null) {
if(curr.next.val == val) {
curr.next = curr.next.next;
}
curr = curr.next;
}
return fakeNode.next;
}
}

方法三,非递归,不使用虚拟节点,单纯删除节点,只是头结点值为val的时候需要单独考虑。(删除节点操作很简单,很类似83题,只是83题是和 x.next 去比较val是否相同,而这道题比较当前节点的 val 是否和传入的val相同)

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
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode removeElements(ListNode head, int val) {

while(head != null && head.val == val) {
head = head.next;
}

// 如果没有这个判断,curr.next在corner edge情况下很可能会报空指针
if(head == null) return null;

ListNode curr = head;

while(curr != null && curr.next != null) {
if(curr.next.val == val) {
curr.next = curr.next.next;
} else {
curr = curr.next;
}
}
return head;
}
}

剑指offer22.链表中倒数第k个节点(Easy)

输入一个链表,输出该链表中倒数第k个节点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点。例如,一个链表有6个节点,从头节点开始,它们的值依次是1、2、3、4、5、6。这个链表的倒数第3个节点是值为4的节点。

示例:

1
2
3
给定一个链表: 1->2->3->4->5, 和 k = 2.

返回链表 4->5.

解:这道题暴力的想法可以遍历两次链表,第一次遍历用变量保存链表的长度n,要访问倒数第k个,第二次直接走n-k+1步即可。

但是这样做要先后遍历一遍链表,一共遍历两遍,时间复杂度太高。

第二种方法,遍历一遍链表即可,用快慢指针。快指针先走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
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode getKthFromEnd(ListNode head, int k) {

ListNode slow = head, fast = head;

for(int i = 0; i < k; i++) {
fast = fast.next;

}
while(fast != null) {
slow = slow.next;
fast = fast.next;
}
return slow;
}
}

但是这道题其实非常重点之一其实是考察代码的鲁棒性。上面这段代码虽然可以解决问题,但是有三个致命缺陷,可以让程序执行的时候崩溃:

  1. k为0,如果直接计算k-1,那么结果会是错误的,甚至如果溢出的话,对指针的操作次数会让内存不够用
  2. 如果k的大小大于链表的长度,会有空指针异常
  3. 如果输入的head为空指针,程序也会崩溃

上面三点中,第三点经常会碰到而且很容易考虑的,但是前两点可能会考虑不到。

针对这三点问题,可以这样解决:

  1. 如果传入的链表头指针head为Null,那么再怎样查询都没有意义,直接返回null即可
  2. 如果k的值为0,也就是要找到倒数第0个节点。但是因为我们是从1开始计数的,所以没有倒数第0个,这个情况也要返回null
  3. 如果k的值比链表长度更大,那么在查询过程中fast可能会指向null,此时我们也需要即时返回null,否则后面再移动fast或者slow的时候会报错。这里可以通过加一个if判断fast是否为空来解决。

增加鲁棒性之后的代码:

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
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode getKthFromEnd(ListNode head, int k) {

if(k == 0 || head == null) return null;

ListNode slow = head, fast = head;

for(int i = 0; i < k; i++) {
if(fast != null) {
fast = fast.next;
} else {
return null;
}

}
while(fast != null) {
slow = slow.next;
fast = fast.next;
}
return slow;
}
}

237. Delete Node in a Linked List(删除链表中的节点)(Easy)

Write a function to delete a node (except the tail) in a singly linked list, given only access to that node.

Given linked list – head = [4,5,1,9], which looks like following:

Example 1:

Input: head = [4,5,1,9], node = 5
Output: [4,1,9]
Explanation: You are given the second node with value 5, the linked list should become 4 -> 1 -> 9 after calling your function.

Example 2:

Input: head = [4,5,1,9], node = 1
Output: [4,5,9]
Explanation: You are given the third node with value 1, the linked list should become 4 -> 5 -> 9 after calling your function.

Note:

  • The linked list will have at least two elements.
  • All of the nodes’ values will be unique.
  • The given node will not be the tail and it will always be a valid node of the linked list.
  • Do not return anything from your function.

解:
注意,这道题只是考如何删除某个指定的节点,考的就是在无法知道这个节点前面指针的时候该如何操作。感觉和脑筋急转弯差不多。

因为我们不能操作这个节点前面的节点的next指针,所以要删除当前这个节点,就只能把下一个节点的值赋给当前这个节点,然后让当前节点跳过下一个节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public void deleteNode(ListNode node) {
node.val = node.next.val;
node.next = node.next.next;
}
}

61. Rotate List(旋转链表)(Mid)

Given a linked list, rotate the list to the right by k places, where k is non-negative.

Example 1:

1
2
3
4
5
Input: 1->2->3->4->5->NULL, k = 2
Output: 4->5->1->2->3->NULL
Explanation:
rotate 1 steps to the right: 5->1->2->3->4->NULL
rotate 2 steps to the right: 4->5->1->2->3->NULL

Example 2:

1
2
3
4
5
6
7
Input: 0->1->2->NULL, k = 4
Output: 2->0->1->NULL
Explanation:
rotate 1 steps to the right: 2->0->1->NULL
rotate 2 steps to the right: 1->2->0->NULL
rotate 3 steps to the right: 0->1->2->NULL
rotate 4 steps to the right: 2->0->1->NULL

解:这道题用双指针来解决,即快慢指针。

需要返回一个新的链表,就要考虑到定义一个虚拟头结点,这样可以省去每次都判断链表头是否为空。

用慢指针来标记反转之后的最后一个节点的位置。但是因为反转的k值可能大于之前的总长度,所以可以先用快指针遍历一遍链表得到总长度(顺便利于最后的rotation操作),然后再移动slow指针过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
34
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode rotateRight(ListNode head, int k) {
// corner case
if(head == null || head.next == null) return head;

ListNode fakeNode = new ListNode(0);
fakeNode.next = head;
ListNode fast = fakeNode, slow = fakeNode;

int i;//i用来存储链表长度,用来给k求余,这个和数组旋转差不多,k太大要求余

for(i = 0; fast.next != null; i++) // Get the total length
fast = fast.next;//此时fast已经在链表尾部了

// 需要注意最后移动完之后,slow的位置是在第 i-i%k 的位置的,相当于尾节点
for(int j = 0; j < i - k % i; j++)//让slow移动到第 i-k%i 个位置
slow = slow.next;

// 最后进行反转,把fast指向原来的头结点
fast.next = fakeNode.next;
fakeNode.next = slow.next;
slow.next = null;

return fakeNode.next;
}
}

2. Add Two Numbers(两数相加)(Mid)

You are given two non-empty linked lists representing two non-negative integers. The digits are stored in reverse order and each of their nodes contain a single digit. Add the two numbers and return it as a linked list.

You may assume the two numbers do not contain any leading zero, except the number 0 itself.

Example:

1
2
3
Input: (2 -> 4 -> 3) + (5 -> 6 -> 4)
Output: 7 -> 0 -> 8
Explanation: 342 + 465 = 807.

解:方法一:

其实这道题看上去比较别扭,实际上可以理解,因为相当于链表的头指针从个位开始指,然后往后依次加起来

因为最后要返回一个新的链表,所以好的方法还是创建一个fakeNode。然后用sum表示当前求和结果,carry表示进位。

比较重要的两个公式是求加之后的位数值(sum%10)和加之后的进位的值(sum/10)。每次只要有加计算,就让current.next指向新创建的节点

需要考虑三个部分:

  1. 两个链表都有数位的部分,也就是从个位,十位开始l1和l2都有数,此时求和(sum)的方法是l1.val+l2.val+carry
  2. l1或者l2有一个为null了,另一个不为null,此时用不为null的节点的值加上carry为新的sum
  3. l1和l2都遍历完了,最后只需要看carry是不是0,如果不是0,意味着最后要开辟一个新节点,也就是返回的求和之后的链表长度为l1的长度+l2的长度+1
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
// corner case
if(l1 == null) return l2;
if(l2 == null) return l1;

ListNode fakeNode = new ListNode(0);
ListNode current = fakeNode;
int carry = 0; // carry表示进位
// 第一段,l1和l2共有的部分
while(l1 != null && l2 != null) {
int sum = l1.val + l2.val + carry;
int val = sum % 10;
carry = sum / 10;
current.next = new ListNode(val);
current = current.next;
l1 = l1.next;
l2 = l2.next;
}
while(l1 != null) { // 此时l1 is not null, but l2 is null
int sum = l1.val + carry;
int val = sum % 10;
carry = sum / 10;
current.next = new ListNode(val);
current = current.next;
l1 = l1.next;
}
while(l2 != null) { // 此时l2 is not null, but l1 is null
int sum = l2.val + carry;
int val = sum % 10;
carry = sum / 10;
current.next = new ListNode(val);
current = current.next;
l2 = l2.next;
}

//此时遍历完了所有l1和l2的节点,只看最后需不需要新进一位
// 进位完之后新链表长度是l1和l2长度和 +1
if(carry != 0) current.next = new ListNode(carry);

return fakeNode.next;
}
}

方法二:

其实可以简化代码,虽然不如上面的好理解,但是公式是完全相同的(就是sum%10sum/10)

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
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
ListNode fakeNode = new ListNode(0);
ListNode curr = fakeNode;
int carry = 0, sum = 0;
while(l1 != null || l2 != null) {
int x = l1==null ? 0 : l1.val;
int y = l2==null ? 0 : l2.val;
sum = x + y + carry;

carry = sum / 10;
sum = sum % 10;

curr.next = new ListNode(sum);

curr = curr.next;
if(l1 != null)
l1 = l1.next;

if(l2 != null)
l2 = l2.next;
}
if(carry == 1)
curr.next = new ListNode(1);
return fakeNode.next;
}
}

54. Spiral Matrix(螺旋矩阵) / 剑指29(顺序打印矩阵)(Mid)

Given a matrix of m x n elements (m rows, n columns), return all elements of the matrix in spiral order.

Example 1:

1
2
3
4
5
6
7
Input:
[
[ 1, 2, 3 ],
[ 4, 5, 6 ],
[ 7, 8, 9 ]
]
Output: [1,2,3,6,9,8,7,4,5]

Example 2:

1
2
3
4
5
6
7
Input:
[
[1, 2, 3, 4],
[5, 6, 7, 8],
[9,10,11,12]
]
Output: [1,2,3,4,8,12,11,10,9,5,6,7]

解:这道题和《剑指offer》上面试29题顺序打印矩阵,顺时针打印矩阵,相同,都是用顺时针旋转的方式打印出来一个二维数组。

可以提取出来访问顺序:从左往右、从上往下、从右往左、从下往上。按照这个规律进行循环,条件是结果集的size()小于目标矩阵的大小。

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
class Solution {
public List<Integer> spiralOrder(int[][] matrix) {
List<Integer> result = new ArrayList<>();
if(matrix == null || matrix.length == 0) return result;

int top = 0;
int bottom = matrix.length - 1;
int left = 0;
int right = matrix[0].length - 1;
int size = matrix.length * matrix[0].length;
while(result.size() < size) {
for(int i = left; i <= right && result.size() < size; i++) {
result.add(matrix[top][i]);
}
top++;
for(int i = top; i <= bottom && result.size() < size; i++) {
result.add(matrix[i][right]);
}
right--;
for(int i = right; i >= left && result.size() < size; i--) {
result.add(matrix[bottom][i]);
}
bottom--;
for(int i = bottom; i >= top && result.size() < size; i--) {
result.add(matrix[i][left]);
}
left++;
}
return result;
}
}

剑指22.找到链表倒数第k个节点

输入一个链表,输出该链表中倒数第k个节点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点。例如,一个链表有6个节点,从头节点开始,它们的值依次是1、2、3、4、5、6。这个链表的倒数第3个节点是值为4的节点。

示例

1
2
3
给定一个链表: 1->2->3->4->5, 和 k = 2.

返回链表 4->5.

解:这道题主要考察代码鲁棒性。常规找到倒数第k个节点很容易,可以用两个快慢指针,快的先走,走k步,然后让慢的走,当快的走到null了,慢的就是倒数第k个节点。

但是主要注意几个很可能出现的边界情况:

  1. 输入的head为null:此时我们要返回null指针
  2. 输入的k为0:如果输入的k为0,意思是删除第k个指针,但是因为我们这里是从1开始计数的,所以也要返回null
  3. 输入的k大于链表长度:这个不能一下子就判断出来,可以在一开始移动fast的过程中加一个判断,如果在循环还没结束的时候fast就指到了null,则提前返回
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
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode getKthFromEnd(ListNode head, int k) {

ListNode fast = head;
ListNode slow = null;

if(k == 0 || head == null) return slow;

for(int i = 0; i < k; i++) {
if(fast != null) {
fast = fast.next;
} else {
return slow;
}
}
slow = head;

while(fast != null) {
fast = fast.next;
slow = slow.next;
}
return slow;
}
}

19. Remove N-th node from end of list(删除链表的倒数第N个节点)(Mid)

Given a linked list, remove the n-th node from the end of list and return its head.

Example:

1
2
3
Given linked list: 1->2->3->4->5, and n = 2.

After removing the second node from the end, the linked list becomes 1->2->3->5.

Note:

Given n will always be valid.

Follow up:

Could you do this in one pass?


解:用快慢两个指针即可。因为这里要删除倒数第n个,所以slow最后要指向倒数第n+1个才能快速顺利删除。

这里用到一个虚拟头结点,方便slow和fast移动,一开始移动fast,移动n+1步,这样保证slow和fast之间距离为n个节点。然后fast和slow一起移动,直到fast移动到null的时候,slow指向倒数第n+1个,移动指针可以很方便地删除第n个节点。

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
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {

ListNode dummyNode = new ListNode(-1);
dummyNode.next = head;

ListNode fast = dummyNode;
ListNode slow = dummyNode;

if(head == null || n == 0) return null;

//让slow和fast拉开n的距离,因为最后要删除倒数第k个
for(int i = 0 ; i <= n; i++) {
fast = fast.next;
}

//移动fast和slow,当fast指到null的时候,slow为倒数第n+1个
while(fast != null) {
fast = fast.next;
slow = slow.next;
}
slow.next = slow.next.next;
return dummyNode.next;
}
}

160. Intersection of Two Linked Lists(相交链表)(Easy)

Write a program to find the node at which the intersection of two singly linked lists begins.

For example, the following two linked lists:

例子

1
2
3
Input: intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA = 2, skipB = 3
Output: Reference of the node with value = 8
Input Explanation: The intersected node's value is 8 (note that this must not be 0 if the two lists intersect). From the head of A, it reads as [4,1,8,4,5]. From the head of B, it reads as [5,0,1,8,4,5]. There are 2 nodes before the intersected node in A; There are 3 nodes before the intersected node in B.

Notes:

  • If the two linked lists have no intersection at all, return null.
  • The linked lists must retain their original structure after the function returns.
  • You may assume there are no cycles anywhere in the entire linked structure.
  • Your code should preferably run in O(n) time and use only O(1) memory.

解:方法是用A、B两个指针循环遍历,A和B各自遍历到末尾一次,然后各自移动到对方的头指针位置。比如A先到末尾的NULL,说明链表A比较短,那么之后pA移动到headB。继续移动pB到末尾NULL,然后再让pB移动到headA。此时两者之间距离就是两个链表之间的节点数量了,后面直接判断这两个节点是否相等即可,如果相等,那个点就是交点。

详细图片解释可以看这篇文章

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if(headA == null || headB == null) return null;
ListNode pA = headA;
ListNode pB = headB;
while(pA != pB) {
pA = pA == null ? headB : pA.next;
pB = pB == null ? headA : pB.next;
}
return pA;
}
}
  • 时间复杂度:O(n)
  • 空间复杂度:O()

23.Merge k Sorted Lists(合并K个排序链表)(Hard)

Merge k sorted linked lists and return it as one sorted list. Analyze and describe its complexity.

Example:

1
2
3
4
5
6
7
Input:
[
1->4->5,
1->3->4,
2->6
]
Output: 1->1->2->3->4->4->5->6

解:解法一:暴力法

  • 用一个数组保存所有链表中的数,之后进行排序,再从头到尾将数组遍历,生成一个排好序的链表
  • 假设每个链表平均长度为n,整体时间复杂度为O(nk * log(nk)),偏大

解法二:最小堆

  • 每次比较k个链表头,时间复杂度为O(k)
  • 对k个链表头创建一个大小为k的最小堆
    • 创建一个大小为k的最小堆所需时间为O(k)
    • 从堆中取最小的数,所需时间都是O(logk)
    • 如果每个链表平均长度为n,则共有nk个元素,即用大小为k的最小堆过滤nk个元素
    • 整体时间复杂度为O(nk*log(k))
    • 空间复杂度为O(k),我们有k个list,占据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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/


class Solution {
public ListNode mergeKLists(ListNode[] lists) {
// 如果输入为[],边界条件
if(lists.length == 0 || lists == null) return null;

// 利用一个空的链表头方便我们插入节点
ListNode fakeHead = new ListNode(0), p = fakeHead;

// 定义一个最小堆来保存k个链表节点
int k = lists.length;

// 将k个链表的头放到最小堆中,因为使用ListNode数据结构,所以重新定义比较器
PriorityQueue<ListNode> heap = new PriorityQueue<>(k, new Comparator<ListNode>() {
public int compare(ListNode a, ListNode b) {
return a.val - b.val;
}
});

// 从k个链表中将当前最小节点取出,插入到结果链表中
// 每条链表都会有一个指针i,这个for把每条链表的第一个加入到小顶堆中
// 因为本身就有k个链表,所以相当于初始化的步骤
for(int i = 0; i < k; i++) {
if(lists[i] != null) {
heap.offer(lists[i]);
}
}

// 真正使用最小堆的操作,将堆中元素一个一个取出,插入到结果链表中
while(!heap.isEmpty()) {
ListNode node = heap.poll();

p.next = node;
p = p.next;

// p是结果链表节点,node是当前遍历的节点
// 如果当前链表有后续节点,在poll()了它之后自然要访问它后续节点
if(node.next != null) {
heap.offer(node.next);
}
}

// 最后返回结果链表
return fakeHead.next;
}
}

上述定义小顶堆的代码也可以用Lambda表达式简化:

1
2
3
PriorityQueue<ListNode> heap = new PriorityQueue<>(
(a,b) -> a.val - b.val
);

解法三:分治法

利用分治思想,非常类似归并排序操作。

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
// 主函数非常类似归并排序的过程
public ListNode mergeKLists(ListNode[] lists, int low, int high) {
// 如果输入为[],边界条件
if(lists.length == 0 || lists == null) return null;

if(low == high) return lists[low];

// 从中间切一刀
int middle = low + (high - low) / 2;

// 递归地处理坐标和右边的列表,最后合并起来
return mergeTwoLists(
mergeKLists(lists, low, middle),
mergeKLists(lists, middle + 1, high)
);
}

public ListNode mergeTwoLists(ListNode a, ListNode b) {
if (a == null) return b;
if (b == null) return a;

if(a.val <= b.val) {
a.next = mergeTwoLists(a.next, b);
return a;
}

b.next = mergeTwoLists(a, b.next);
return b;
}
  • 时间复杂度:O(nk * log(k))
  • 空间复杂度:O(1),可以直接在链表上进行操作

148.Sort List(排序链表)(Mid)

Sort a linked list in O(n log n) time using constant space complexity.

Example 1:

1
2
Input: 4->2->1->3
Output: 1->2->3->4

Example 2:

1
2
Input: -1->5->3->4->0
Output: -1->0->3->4->5

解:需要注意,时间复杂度必须是O(nlogn),空间复杂度也必须是常数级的,不能是O(n)的空间复杂度。

解决方法是归并排序的思想,但是因为空间复杂度必须为常数级,所以不能用递归。

这道题融合了另外两道题,876链表的中间节点,和21,合并两个有序链表

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode sortList(ListNode head) {
// 1、递归结束条件
if (head == null || head.next == null) {
return head;
}

// 2、找到链表中间节点并断开链表 & 递归下探
ListNode midNode = middleNode(head);
ListNode rightHead = midNode.next;
midNode.next = null;

ListNode left = sortList(head);
ListNode right = sortList(rightHead);

// 3、当前层业务操作(合并有序链表)
return mergeTwoLists(left, right);
}

// 找到链表中间节点(876. 链表的中间结点)
private ListNode middleNode(ListNode head) {
if (head == null || head.next == null) {
return head;
}
ListNode slow = head;
ListNode fast = head.next.next;

while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}

return slow;
}

// 合并两个有序链表(21. 合并两个有序链表)
private ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode dummyNode = new ListNode(-1);
ListNode curr = dummyNode;

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

curr = curr.next;
}

curr.next = l1 != null ? l1 : l2;
return dummyNode.next;
}
}