Leetcode记录库数据结构篇之一:链表
结论
记录一些很主观的东西,有些想法归根结底可能还是自己的实力不够。
与链表有关的算法题,一般需要进行以下考虑:
1.区分所操作节点的类型:首结点、中间节点、尾节点。
2.如果需要对头结点进行相关操作,那么考虑是否要添加虚拟头结点。
3.如果一个问题涉及到数据的顺序和要进行的操作相逆的话,考虑使用栈结构转存。而且使用数组栈效率高一点。
1 160.相交链表
https://leetcode-cn.com/problems/intersection-of-two-linked-lists/
思路描述
- 如果两个链表相交的话,那么他们必有交叉的地方,如图:
而且不会是这样的交叉:
因为链表中的节点只会有一个后继节点。 所以有a+c+b = b+c+a,顺序遍历两个链表,当某个链表结束时,开始遍历另一个链表,直到节点相等。
代码实现
- java代码:
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if (headA == null || headB == null) {
return null;
}
ListNode a = headA;
ListNode b = headB;
while (a != b) {
a = a != null ? a.next : headB;
b = b != null ? b.next : headA;
}
return a;
}
}
注意事项
- 无
拓展延伸
- 如果只是判断是否存在交点,那么就是另一个问题,即 编程之美 3.6 的问题。
有两种解法:把第一个链表的结尾连接到第二个链表的开头,看第二个链表是否存在环;或者直接比较两个链表的最后一个节点是否相同。
2 206.反转链表
https://leetcode-cn.com/problems/reverse-linked-list/
思路描述
- 2019年考研408试卷原题,如果我那年复习的时候不那么投机取巧,这道题就能做上了,明明这么简单。55555……
头插法原地逆置链表,时间复杂度O(n),空间复杂度O(1)。
代码实现
- java代码:
class Solution {
public ListNode reverseList(ListNode head) {
if(head == null) return head;
ListNode newhead, temp, node;
//取下需要逆转的链表的第一个,作为新链表的结尾
newhead = head;
head = head.next;
//结尾置空
newhead.next = null;
while(head != null){
node = head;//先记录下当前需要操作的节点
temp = head.next;//记录下下一个要操作的节点
node.next = newhead;//当前操作节点接到逆转后的链表的头部
newhead = node;//当前操作节点成为逆转后链表的新头
head = temp;//未逆转链表的新头
}
return newhead;
}
}
注意事项
- 1.java中对于链表节点“=”等号赋值好像是两个变量公用一个内存单元,所以说修改一个变量的next,另一个变量的next也已经变了。
拓展延伸
递归的解法:
public ListNode reverseList(ListNode head) {
if (head == null || head.next == null) {
return head;
}
ListNode next = head.next;
ListNode newHead = reverseList(next);
next.next = head;
head.next = null;
return newHead;
}
3 21.归并有序链表
https://leetcode-cn.com/problems/merge-two-sorted-lists/
思路描述
woshishabi.
代码实现
- java代码:
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
if(l1 == null) return l2;
if(l2 == null) return l1;
ListNode tempNode, newhead, newtail;
if(l1.val <= l2.val){
newhead = l1;
newtail = l1;
l1 = l1.next;
}else{
newhead = l2;
newtail = l2;
l2 = l2.next;
}
while(l1 != null && l2 != null){
while((l1 != null && l2 != null) && l1.val <= l2.val){
newtail.next = l1;
newtail = l1;
l1 = l1.next;
}
while((l1 != null && l2 != null) && l2.val <= l1.val){
newtail.next = l2;
newtail = l2;
l2 = l2.next;
}
}
newtail.next = (l2 == null)? l1 : l2;
return newhead;
}
}
注意事项
- 1.脑袋不清楚的时候别写代码!!!
- 2.脑袋不清楚的时候别写代码!!!
- 3.脑袋不清楚的时候别写代码!!!
拓展延伸
递归的解法:
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
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;
}
}
4 83. 删除排序链表中的重复元素
https://leetcode-cn.com/problems/remove-duplicates-from-sorted-list/
思路描述
这也算是双指针吧,一快一慢,如果快指针的val和慢指针的val相同,快指针就向后移。直到链表结束。
代码实现
java代码:
class Solution {
public ListNode deleteDuplicates(ListNode head) {
if(head == null || head.next == null) return head;
ListNode l1 = head, l2 = head;
while(l2 != null){
while(l2 != null && l1.val == l2.val){
l2 = l2.next;
}
l1.next = l2;
l1 = l2;
}
return head;
}
}
注意事项
- 1.在取指针的值之前,需要注意先确定指针是否为空。
拓展延伸
递归的解法:
public ListNode deleteDuplicates(ListNode head) {
if (head == null || head.next == null) return head;
head.next = deleteDuplicates(head.next);
return head.val == head.next.val ? head.next : head;
}
5 19. 删除链表的倒数第 N 个结点
https://leetcode-cn.com/problems/remove-nth-node-from-end-of-list/
思路描述
- 1.删除一个链表的头,我们需要将链表的头改变。
2.删除一个链表中间的一个节点,我们需要知道这个节点的前一个节点和后一个节点,然后将两者相连。
3.需要删除的是尾节点,和2一样。 基本原理:双指针,一快一慢,中间相差k步。一个指需被删节点前,一个指链表尾。
代码实现
java代码:
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
// 前置处理
if(head == null || n == 0) return head;
else if(head.next == null){
head = null;
return head;
}
// 确定prior和tail之间的距离
ListNode prior = head, tail = head;
do{
tail = tail.next;
n--;
}while(n != 0);
// 需要删除的是头结点
if(tail == null){
return head.next;
}
// 否则,双指针同步后移,遍历列表
while(tail.next != null){
prior = prior.next;
tail = tail.next;
}
// 删除节点
prior.next = prior.next.next;
return head;
}
}
注意事项
- 1.在对指针进行操作时,一定要注意区分,节点的类型,链表中的节点分为三种:头结点、中间节点、尾节点,在对他们操作时需要注意。
拓展延伸
- 无
6 24. 两两交换链表中的节点
https://leetcode-cn.com/problems/swap-nodes-in-pairs/
思路描述
- 1.先试试递归:如下。但是这样每一层都会定义两个变量,空间复杂度是O(n)了。
代码实现
java代码:
class Solution {
public ListNode swapPairs(ListNode head) {
if(head == null || head.next == null) return head;
ListNode swap = head, temp = head.next.next;
head = head.next;
head.next = swap;
swap.next = swapPairs(temp);
return head;
}
}
思路描述
- 2.再试试常规解法:
交换前后两个节点,我们需要知道: (从这里开始走偏了)一个窗口大小为4的链表段的节点指针。(最后还是变递归了,而且还很繁琐) - 其实我们需要知道的是,这两个节点的前置节点就好了。交换3号和4节点,我们只需要知道2号节点既可以了。但是,交换1号和2号,我们需要表头节点,没有的话虚拟出一个来。这就有了拓展延伸的方法。
代码实现
java代码:
class Solution {
public ListNode swapPairs(ListNode head) {
//
if(head == null || head.next == null){
return head;
}
if(head.next.next == null){
ListNode swap = head.next;
swap.next = head;
head.next = null;
return swap;
}
if(head.next.next.next == null){
ListNode swap = head.next, tail = head.next.next;
swap.next = head;
head.next = tail;
return swap;
}
ListNode p1, p2, p4, newhead = head.next, swap;
while(head.next.next.next != null){
p1 = head;
p2 = head.next;
p4 = head.next.next.next;
head = head.next.next;
swap = p2;
swap.next = p1;
p1.next = p4;
if(head == null || head.next == null){
return newhead;
}
if(head.next.next == null){
swap = head.next;
swap.next = head;
head.next = null;
return newhead;
}
if(head.next.next.next == null){
swap = head.next;
ListNode tail = head.next.next;
swap.next = head;
head.next = tail;
return newhead;
}
}
return newhead;
}
}
注意事项
- 1.在对指针进行操作时,一定要注意区分,节点的类型,链表中的节点分为三种:头结点、中间节点、尾节点,在对他们操作时需要注意。
拓展延伸
- 1.在链表头之前加一个虚拟节点,便可迎刃而解。时间复杂度O(n),空间O(1)
class Solution {
public ListNode swapPairs(ListNode head) {
// 设置一个链表头结点的前置节点
ListNode node = new ListNode(-1);
node.next = head;
ListNode pre = node;
while (pre.next != null && pre.next.next != null) {
ListNode l1 = pre.next, l2 = pre.next.next;
ListNode next = l2.next;
l1.next = next;
l2.next = l1;
pre.next = l2;
pre = l1;
}
return node.next;
}
}
7 445. 两数相加 II
思路描述
- 1.应该是要用递归,倒着从最后一位加过来,因为会涉及到链表头,所以需要新建一个头结点。
- 不行,递归做不出来
- 因为本题是需要对 两个链表 同时处理。
- 也就是说对 两个链表是并行处理的关系。而递归一般只能对一个 变量做递归处理;那么处理2个链表就成了串行处理,无法做累加。也不是不能用递归对两个链表处理,但是里面还涉及到对齐的问题;特别是因为节点数不一样,一个到链表尾,另一个没到,还是需要递归。
- 也就是说,如果两个链表的节点个数是 一样 ,那就可以把两个链表,抽象成一个来处理。
代码实现
java代码:
思路描述
- 1.使用栈结构保存链表内容,然后一一相加,使用头插法建立链表进行存储。
代码实现
java代码:
class Solution {
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
// 引入栈结构
Stack<Integer> stack1 = listToStack(l1);
Stack<Integer> stack2 = listToStack(l2);
// 初始化进位
int carry = 0, a = 0, b = 0, result;
// 初始化返回的链表头结点
ListNode newhead = new ListNode(-1, null);
while(!stack1.isEmpty() || !stack2.isEmpty() || carry != 0){
a = stack1.isEmpty() ? 0 : stack1.pop();
b = stack2.isEmpty() ? 0 : stack2.pop();
result = a + b + carry;
carry = result / 10;
result = result % 10;
// 头插法建立链表,保证顺序
ListNode node = new ListNode(result);
node.next = newhead.next;
newhead.next = node;
}
return newhead.next;
}
// 私有方法将链表中的val存放到栈中
private Stack<Integer> listToStack(ListNode l){
Stack<Integer> stack = new Stack<>();
while(l != null){
stack.push(l.val);
l = l.next;
}
return stack;
}
}
注意事项
- 注意事项
拓展延伸
- 如果一个问题涉及到数据的顺序和要进行的操作相逆的话,考虑使用栈结构转存。而且使用数组栈效率高一点。
class Solution {
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
// 引入数组栈结构
int stack1[] =new int[100], top1 = -1;
while(l1 != null){
stack1[++top1] = l1.val;
l1 = l1.next;
}
int stack2[] = new int[100], top2 = -1;
while(l2 != null){
stack2[++top2] = l2.val;
l2 = l2.next;
}
// 初始化进位
int carry = 0, a = 0, b = 0, result;
// 初始化返回的链表头结点
ListNode newhead = new ListNode(-1, null);
while(top1 != -1 || top2 != -1 || carry != 0){
a = (top1 == -1) ? 0 : stack1[top1--];
b = (top2 == -1) ? 0 : stack2[top2--];
result = a + b + carry;
carry = result / 10;
result = result % 10;
//System.out.println(top1);
// 头插法建立链表,保证顺序
ListNode node = new ListNode(result);
node.next = newhead.next;
newhead.next = node;
}
return newhead.next;
}
}
8 234. 回文链表
思路描述
- 回文链表,从中间分开两边是对称的。这需要对链表的长度进行区分是偶数还是奇数。
- 1.偶数:使用双指针,快指针一次移动两个位置,慢指针一次一个,同时使用栈结构将内容存下来,然后从慢指针这里继续往后遍历,同时出栈判断元素是否相同。
- 2.奇数:原理相同,不过在判断元素是否相同时,栈结构要先出栈一个。
- 时空间复杂度为O(n)和O(n);
代码实现
java代码:
class Solution {
public boolean isPalindrome(ListNode head) {
if(head.next == null) return true;
//
ListNode fast = head, slow = head;
int i = 0;
Stack<Integer> stack = new Stack<>();
while(fast != null){
i++;
stack.push(slow.val);
// fast后移两个并进行判断链表是否结束
fast = fast.next;
if(fast == null) break;
else {
i++;
fast = fast.next;
}
if(fast == null) break;
// slow后移一个
slow = slow.next;
}
// 区分链表的长度
if(i % 2 == 0){// 偶数
slow = slow.next;
while(slow != null){
if(slow.val != stack.pop()){
return false;
}
slow = slow.next;
}
}else{// 奇数
i = stack.pop();
slow = slow.next;
while(slow != null){
if(slow.val != stack.pop()){
return false;
}
slow = slow.next;
}
}
return true;
}
}
思路描述
- 没说不能破坏原有链表,那么还是快慢指针,等到快指针移动到最后时,区分链表长度的是技术还是偶数,将后半部分逆置。
- 然后遍历前半部分和被逆置的后半部分链表,查看是否相同。
- 时空间复杂度O(n)和O(1)
代码实现
java代码:
class Solution {
public boolean isPalindrome(ListNode head) {
if(head == null || head.next == null) return true;
//
ListNode fast = head.next, slow = head, temp;
ListNode newhead = new ListNode(-1, null);
while(fast != null && fast.next != null){
slow = slow.next;
fast = fast.next.next;
}
//开始逆置后续链表
if(fast == null){// 链表长度为奇数
while(slow != null){
temp = slow.next;
slow.next = newhead.next;
newhead.next = slow;
slow = temp;
}
}else{// 链表长度为偶数
slow = slow.next;
while(slow != null){
temp = slow.next;
slow.next = newhead.next;
newhead.next = slow;
slow = temp;
}
}
// 判断元素是否相同
while(newhead.next != null){
if(head.val == newhead.next.val){
}else{
return false;
}
head = head.next;
newhead = newhead.next;
}
return true;
}
}
注意事项
- 1.快指针和慢指针的位置关系。
拓展延伸
- 快慢指针之间的距离关系,标准写法。
- 比如说想要快慢指针位置关系为1:2
ListNode fast = head.next, slow = head;
while(fast != null && fast.next != null){
slow = slow.next;
fast = fast.next.next;
}
- 1:3
ListNode fast = head.next.next, slow = head;
while(fast != null && fast.next != null && fast.next.next != null){
slow = slow.next;
fast = fast.next.next.next;
}
9 725. 分隔链表
思路描述
- 1.首先获取链表的长度,然后判断链表够不够长。也就是说len是否大于k,如果不大于则不够长,结果中肯定有空链表。而且各个非空链表肯定长度为1。
- 2.如果够长则需要考虑怎么将链表进行划分,才能满足题目要求。靠前的链表长而且只会比后面的列表长1个单位。所以首先将链表均分,然后剩下的长度,从头依次向后添加给每个需要划分的小链链表,直到用光。
代码实现
java代码:
class Solution {
public ListNode[] splitListToParts(ListNode head, int k) {
// 预处理
if(k == 1) {
ListNode[] array = new ListNode[k];
array[0] = head;
return array;
}
// 获取链表的长度
ListNode h = head;
int len = 0;
while(h != null){
len++;
h = h.next;
}
// 分割后链表最佳长度
int lenSplited = len / k;
// 链表平分后剩余数量
int mod = len % k;
int[] lensSplited = new int[k];
for(int i =0; i < k; i++){
lensSplited[i] = 0;
}
int i = 0;
// 判断链表是否足够长,用于划分
if(k > len){// 不够长
while(i < len){
lensSplited[i] = lenSplited;
i++;
}
return split(head, len, lensSplited, k);
}else{// 够长
while(i < k){
lensSplited[i] = lenSplited + ((mod-- > 0)? 1 : 0);
i++;
}
return split(head, len, lensSplited, k);
}
}
// 分割链表为指定数量指定长度
private ListNode[] split(ListNode head, int len, int[] lensSplited, int k){
ListNode[] array = new ListNode[k];
ListNode temp;
// 初始值设为null
for(int i = 0; i < k; i++){
array[i] = null;
}
int listNum = 0;
while(listNum < k && head != null){
array[listNum] = head;
int l = 1;
while(l < lensSplited[listNum]){
head = head.next;
l++;
}
if(head != null){
temp = head.next;
head.next = null;
head = temp;
}
listNum++;
}
return array;
}
}
注意事项
- 1.java不能对数组整体赋值吗?必须要用循环?
拓展延伸
- 注意分析题目中的数学关系。
这个更简洁。
class Solution {
public ListNode[] splitListToParts(ListNode head, int k) {
int N = 0;
ListNode cur = head;
while (cur != null) {
N++;
cur = cur.next;
}
int mod = N % k;
int size = N / k;
ListNode[] ret = new ListNode[k];
cur = head;
for (int i = 0; cur != null && i < k; i++) {
ret[i] = cur;
int curSize = size + (mod-- > 0 ? 1 : 0);
for (int j = 0; j < curSize - 1; j++) {
cur = cur.next;
}
ListNode next = cur.next;
cur.next = null;
cur = next;
}
return ret;
}
}
10 328. 奇偶链表
思路描述
- 这有什么难的?计数,区分奇偶,尾插法。
- 不好意思,题理解错了。
- 双指针,一个指奇节点一个指偶节点,新建两个头指针作为划分后的奇偶链表的头节点。
代码实现
java代码:
class Solution {
public ListNode oddEvenList(ListNode head) {
// 预处理
if(head == null || head.next == null || head.next.next == null){
return head;
}
// 头结点
ListNode oddHead = head, evenHead = head.next;
ListNode oddNode = evenHead.next, evenNode = oddNode.next, oddTail = oddHead, evenTail = evenHead;
// 遍历区分奇偶节点
while(oddNode != null && evenNode != null){
// 尾插法奇链表
oddTail.next = oddNode;
oddTail = oddNode;
oddNode = oddNode.next;
if(oddNode != null){
oddNode = oddNode.next;
}else{
break;
}
// 尾插法偶链表
evenTail.next = evenNode;
evenTail = evenNode;
evenNode = evenNode.next;
if(evenNode != null){
evenNode = evenNode.next;
}else{
break;
}
}
// 偶链表链到奇链表后
if(oddNode == null){
oddTail.next = evenHead;
evenTail.next = null;
}else{
oddTail.next = oddNode;
oddTail = oddNode;
oddTail.next = evenHead;
evenTail.next = null;
}
return oddHead;
}
}
java代码:
class Solution {
public ListNode oddEvenList(ListNode head) {
// 预处理
if(head == null || head.next == null || head.next.next == null){
return head;
}
//
ListNode evenHead = head.next, oddNode = head, evenNode = head.next;
while(evenNode != null && evenNode.next != null){
oddNode.next = oddNode.next.next;
oddNode = oddNode.next;
evenNode.next = evenNode.next.next;
evenNode = evenNode.next;
}
// 偶链表接到奇链表后面
oddNode.next = evenHead;
return head;
}
}
注意事项
- 注意事项
拓展延伸
- 拓展延伸
11 剑指 Offer 35. 复杂链表的复制
思路描述
- 都在注释里了,我的方法很暴力.
- 虽然random指向是随机的,但是指向的节点在链表中的位置是一定的,所以我们可以通过得到每个节点的序号,来找到每个节点对应的random指向。
- 我现在的方法是遍历链表找序号对应的节点,有没有什么办法能够将遍历的过程省去呢?得到一个节点直接能够知道他的位置。
- 时空复杂度:O(n2) O(1)
代码实现
java代码:
class Solution {
public Node copyRandomList(Node head) {
// 先按照普通的链表进行复制,random指针先赋值为null
Node newH = new Node(-1001); // 头指针尾指针复制链表
Node tail = new Node(-1001);
Node originalHead = head;
newH = tail;
int len = 0; // 获取链表的长度
while (head != null) { // 仅复制next节点
Node tmp = new Node(-1001);
tmp.val = head.val;
tmp.next = head.next;
// tail.random = null; // 初始化的tail的random本身就是空
head = head.next;
tail.next = tmp;
tail = tmp;
len++;
}
tail.next = null; // 尾节点置空
head = originalHead; // 原始链表的头,用于遍历
Node copyListHead = newH.next; // 复制链表的头
Node nowProcess = copyListHead; // 当前正在处理的复制链表中的节点
Node tmpHead = new Node(-1001); // 复制链表的头,用于遍历复制链表,寻找对应的random节点
int copyIdx;
while (head != null) {
// 首先找到当前head节点指向的random节点是链表中的倒数第几个
// 倒数第零个表示null
int idx= 0; // 计数
Node randNode = head.random;
while (randNode != null) {
idx++;
randNode = randNode.next;
}
// 同样找到在copyList中对应的倒数那个
// 这里可以优化一下
copyIdx = len;
tmpHead = copyListHead;
while (copyIdx != idx) {
tmpHead = tmpHead.next;
copyIdx--;
}
nowProcess.random = tmpHead;
head = head.next;
nowProcess = nowProcess.next;
}
return newH.next;
}
}
思路描述
- 将原始链表进行拆分复制,接在原始链表中每一个对应节点的后面。
- 根据原始节点的random指向,获取到新复制出的节点,并修改复制出的节点的random指针。
- 恢复原始链表的next指向,修改复制链表的next指向。
- 时空复杂度:O(3n) O(1)
- 必须要三个循环才可以,官方题解说的“读者们也可以自行尝试在计算拷贝节点的随机指针的同时计算其后继指针,这样只需要遍历两次。”,即使像这样做了,还是要一个循环来复原原始链表的next指向,如果修改原始next指针和修改拷贝节点的next指针同时进行的话,可能会导致拷贝节点的random指针指向了原始节点中的值,这样就不符合题意了。
class Solution {
public Node copyRandomList(Node head) {
// 1 -> 2 -> 3 -> 4
// 1 -> 1 -> 2 -> 2 -> 3 -> 3 -> 4 -> 4
if (head == null) return head; // 特殊情况
Node node = head;
while (node != null) { // 将原始链表复制一份,并穿插在原始链表中,每两个相同的相邻接点中第二个是新节点,未来用于返回
Node newNode = new Node(node.val); // 新建节点
newNode.next = node.next; // 复制next关系
node.next = newNode; // 接在原始节点后面
node = newNode.next; // 遍历
}
Node tmpHead = head;
while (tmpHead != null) {
node = tmpHead.next;
node.random = tmpHead.random == null ? null : tmpHead.random.next; // 查找random节点,并赋值给copy出来的链表
tmpHead = tmpHead.next.next;
}
// 将原始链表的next关系复原,并修改所复制链表的next关系。
tmpHead = head;
Node copyHead = head.next;
while (tmpHead != null) {
node = tmpHead.next;
tmpHead.next = tmpHead.next.next; // 原始链表next复原
node.next = node.next == null ? null : node.next.next; // 复制链表的next关系复原
tmpHead = tmpHead.next; // 遍历
}
return copyHead;
}
}
注意事项
- 注意事项
拓展延伸
- 拓展延伸
12 25. K 个一组翻转链表
思路描述
- 题不难,但是有好多小细节,而且自己的思路有点不完善,导致最终写出的代码有点ugly。
- 时空复杂度:O(n) O(1)
代码实现
class Solution {
int len;
public ListNode reverseKGroup(ListNode head, int k) {
// 统计长度 + 翻转链表
if (head.next == null) return head; // 特殊情况
ListNode h = new ListNode();
ListNode tail = new ListNode();
h.next = null;
tail = null;
ListNode[] headTail = new ListNode[2];
ListNode[] twoHead = new ListNode[2];
do {
len = k;
twoHead = countLength(head);
headTail = reverseLinkList(twoHead[0], twoHead[1]);
if (h.next == null) {
h.next = headTail[0];
}
if (tail == null) {
tail = headTail[1];
} else {
tail.next = headTail[0];
tail = headTail[1];
}
head = twoHead[1];
} while(head != null);
return h.next;
}
private ListNode[] countLength(ListNode head) {
ListNode[] twoHead = new ListNode[2];
twoHead[0] = head;
while (len-- != 0 && head != null) {
head = head.next;
}
twoHead[1] = head;
return twoHead;
}
private ListNode[] reverseLinkList(ListNode head, ListNode nextHead) {
if (nextHead == null && len != -1) { // 剩余长度不够的情况
return new ListNode[]{head, null};
}
// 翻转链表
ListNode h = new ListNode();
ListNode tail = new ListNode();
h.next = null;
tail = null;
ListNode tmp = new ListNode();
while (head != nextHead) {
tmp = head;
head = head.next;
tmp.next = h.next;
h.next = tmp;
if (tail == null) {
tail = tmp;
}
}
tail.next = nextHead;
return new ListNode[]{h.next, tail};
}
}
注意事项
- 注意事项
拓展延伸
- 官方写的就很不错
class Solution {
public ListNode reverseKGroup(ListNode head, int k) {
ListNode hair = new ListNode(0); // 指向head的“头发”结点,头之前是头发23333
hair.next = head;
ListNode pre = hair;
while (head != null) {
ListNode tail = pre;
// 查看剩余部分长度是否大于等于 k
for (int i = 0; i < k; ++i) {
tail = tail.next;
if (tail == null) { // 如果此时剩下的节点个数不够k个,那么就可以直接返回了
return hair.next;
}
}
ListNode nex = tail.next; // 找到了要反转的这一段的尾节点,并且求出下一段的头节点
ListNode[] reverse = myReverse(head, tail); // 将目前head 到 tail这一段进行反转
head = reverse[0]; // 反转后的头
tail = reverse[1]; // 反转后的尾
// 把子链表重新接回原链表
pre.next = head;
tail.next = nex;
// 更新头发和头指针,进行下一段的反转工作
pre = tail;
head = nex;
}
return hair.next;
}
public ListNode[] myReverse(ListNode head, ListNode tail) { // 反转具体的一段链表
ListNode prev = tail.next; // 先求出到哪里终止反转,tail的next
ListNode p = head; //
while (prev != tail) { // 这个反转的方式有点妙啊
ListNode nex = p.next; //
p.next = prev; //
prev = p;
p = nex;
}
return new ListNode[]{tail, head};
}
}
13 92. 反转链表 II
思路描述
- 时空复杂度:O(n) O(1)
代码实现
class Solution {
public ListNode reverseBetween(ListNode head, int left, int right) {
if (head == null || head.next == null) {
return head;
}
if (left >= right) {
return head;
}
ListNode h = new ListNode();
h.next = head;
ListNode lTail, mHead, mTail, rHead;
mHead = h;
while (left != 1) {
mHead = mHead.next;
left--;
right--;
}
lTail = mHead; // 被截断后的左侧链表的尾节点
mHead = mHead.next; // 中间这一段的head
mTail = mHead;
while (right != 1) {
mTail = mTail.next;
right--;
}
rHead = mTail.next; // 被截断后的右侧链表的head
mTail.next = null; // 截断中间这一段链表
ListNode[] headAndTail = new ListNode[2];
headAndTail = reverse(mHead); // 反转中间链表
lTail.next = headAndTail[0];
headAndTail[1].next = rHead;
return h.next;
}
private ListNode[] reverse(ListNode head) { // 反转链表
ListNode h = new ListNode();
h.next = null;
ListNode tmp, tail = head;
while (head != null) {
tmp = head;
head = head.next;
tmp.next = h.next;
h.next = tmp;
}
return new ListNode[]{h.next, tail};
}
}
注意事项
- 注意事项
拓展延伸
- 官
14 143. 重排链表
思路描述
- 时空复杂度:O(n) O(1)
代码实现
// 链表取中 + 反转 + 交错链接
class Solution {
public void reorderList(ListNode head) {
// 先找中间,分成两段
// 后面反转
// 交错链接
if (head == null || head.next == null) {
return;
}
ListNode fast = head.next, slow = head; // 注意快指针一定要先后置一个
while (fast != null && fast.next != null) { // 链表取中一定要这样写!!!
slow = slow.next;
fast = fast.next.next;
}
ListNode head2 = slow.next;
slow.next = null;
head2 = reverse(head2);
ListNode tmp1;
while (head2 != null) {
tmp1 = head;
head = head.next;
tmp1.next = head2;
tmp1 = head2;
head2 = head2.next;
tmp1.next = head;
}
return;
}
private ListNode reverse(ListNode head) {
ListNode hair = new ListNode();
hair.next = null; // 今天链表反转都写错了
ListNode tmp;
while (head != null) {
tmp = head;
head = head.next;
tmp.next = hair.next;
hair.next = tmp;
}
return hair.next;
}
}
注意事项
- 链表取中、取三分之一时,的确使用快慢指针,但是一定要提前判断在进入这次循环前,快指针还能不能走完下一次快走的步数
// 取中
ListNode fast = head.next, slow = head; // 注意快指针一定要先后置一个
while (fast != null && fast.next != null) { // 链表取中一定要这样写!!!
slow = slow.next;
fast = fast.next.next;
}
// 取三分之一
ListNode fast = head.next, slow = head; // 注意快指针一定要先后置一个
while (fast != null && fast.next != null && fast.next.next != null) { // 一定要这样写!!!
slow = slow.next;
fast = fast.next.next.next;
}
拓展延伸
- 官
15 23. 合并K个升序链表
思路描述
- 时空复杂度O(knlogk), O(logk)
代码实现
// 归并 时空复杂度O(knlogk), O(logk)
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
int n = lists.length;
if (n == 0) return null;
if (n == 1) return lists[0]; // 特殊情况处理
return merge(lists, 0, n - 1); // 划分子问题
}
private ListNode merge(ListNode[] lists, int l, int r) { // 切分子问题
if (l > r) {
return null; // 如果当前没有子问题可以切分,直接返回null
}
if (l == r) {
return lists[l]; // 如果当前的区间中只有一个链表,直接返回这个链表
}
int mid = l + ((r - l) >> 1); // 区间中有多个链表,我们取中平分
ListNode l1 = merge(lists, l, mid); // 递归的划分子问题
ListNode l2 = merge(lists, mid + 1, r);
return mergeSort(l1, l2); // 传统的归并过程
}
private ListNode mergeSort(ListNode l1, ListNode l2) {
if (l1 == null || l2 == null) {
return l1 == null ? l2 : l1;
}
ListNode hair = new ListNode();
ListNode tail = hair;
while (l1 != null && l2 != null) {
if (l1.val <= l2.val) {
tail.next = l1;
l1 = l1.next;
} else {
tail.next = l2;
l2 = l2.next;
}
tail = tail.next;
}
tail.next = l1 == null ? l2 : l1;
return hair.next;
}
}
// 优先队列,小顶堆 时空复杂度O(knlogk), O(k) 这里用了优先队列,优先队列中的元素不超过 k 个,故渐进空间复杂度为 O(k)。
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
int n = lists.length;
if (n == 0) return null;
PriorityQueue<ListNode> pq = new PriorityQueue<>(n, (a, b) -> (a.val - b.val)); // 自定义比较器
for (int i = 0; i < n; i++) { // 第一次放入
if (lists[i] != null) {
pq.offer(lists[i]);
}
}
ListNode hair = new ListNode();
ListNode tail = hair;
ListNode tmp;
while (!pq.isEmpty()) {
tmp = pq.poll();
tail.next = tmp;
tail = tail.next;
if (tmp.next != null) {
pq.offer(tmp.next);
}
}
return hair.next;
}
}
注意事项
- 链
拓展延伸
- 官
16 148. 排序链表
思路描述
- 时空复杂度:O(nlogn) O(logn),基于数组的空间复杂度是O(n),但是这道题是链表,不需要额外的空间,是需要开辟递归栈,所以空间复杂度是O(logn).
代码实现
// 归并 时空复杂度:O(nlogn) O(logn)
class Solution {
public ListNode sortList(ListNode head) {
if (head == null || head.next == null) // 如果当前是空或者只有一个节点
return head;
ListNode fast = head.next, slow = head;
while (fast != null && fast.next != null) { // 分割子问题
slow = slow.next;
fast = fast.next.next;
}
ListNode tmp = slow.next; // tmp右侧链表的头
slow.next = null; // 分割链表
ListNode left = sortList(head); // 左右两个区间
ListNode right = sortList(tmp);
ListNode hair = new ListNode(0); // 合并后的头
ListNode tail = hair; // 尾插法
while (left != null && right != null) { // 归并
if (left.val < right.val) {
tail.next = left;
left = left.next;
} else {
tail.next = right;
right = right.next;
}
tail = tail.next;
}
tail.next = left != null ? left : right;
return hair.next;
}
}
// 快排 时空复杂度O(nlogn)O(logn)
class Solution {
public ListNode sortList(ListNode head) {
return quickSort(head);
}
public ListNode quickSort(ListNode head) {
if(head == null || head.next == null) return head; // 如果问题规模过小,直接返回
ListNode slow = head, fast = head.next; // 快慢指针取中间节点
while(fast != null && fast.next != null){
fast = fast.next.next;
slow = slow.next;
}
int val = slow.val; // 基准值
ListNode h1 = new ListNode(); // 基准值左侧的链表头
ListNode h2 = new ListNode(); // 基准值右侧的链表头
ListNode h3 = new ListNode(); // 可能会有和基准值相同的节点,我们把他全部保留在这个头结点后面,这样我们可以减少接下来的子问题的规模
ListNode t1 = h1, t2 = h2; // 基准值左侧和右侧的链表的尾指针,因为我们是要使用尾插法保证顺序
ListNode t3 = h3; // 所有的基准值,为了保持一致,我们一样用尾插法构造链表
ListNode cur = head; // 当前遍历到的元素
while(cur != null) {
ListNode next = cur.next; // 暂存下一个元素
if(cur.val < val) { // 如果当前这个元素的大小小于基准值
cur.next = t1.next; // 使用尾插法将当前这个节点链到基准值左侧的链表上
t1.next = cur;
t1 = t1.next;
} else if(cur.val > val) { // 如果当前这个元素的大小大于基准值
cur.next = t2.next; // 使用尾插法将当前这个节点链到基准值右侧的链表上
t2.next = cur;
t2 = t2.next;
} else { // 相等的部分,留在这里,等着链接
cur.next = t3.next;
t3.next = cur;
t3 = t3.next;
}
cur = next; // 遍历下一个元素
}
h1 = quickSort(h1.next); // 递归左侧链表,注意这里的h1已经不是原来的h1了。
h2 = quickSort(h2.next); // 递归右侧链表
h3 = h3.next;
t3.next = h2; // 基准值链表链接右侧较大部分链表
if(h1 == null) { // 如果左侧链表为空,直接返回基准值以及基准值之后的元素
return h3;
} else { // 否则,我们需要找到左侧传回链表的尾部,因为原来左侧只是经过了划分,而没有排序,排完序后我们需要重新寻找它的尾节点
t1 = h1; // 寻找尾节点
while(t1.next != null) {
t1 = t1.next;
}
t1.next = h3; // 链接上基准值和基准值右侧的节点
return h1;
}
}
}
注意事项
- 链
拓展延伸
- 官
17 146. LRU 缓存
思路描述
- 时空复杂度:
代码实现
public class LRUCache {
// 定义内部类,
class DLinkedNode {
int key;
int value;
DLinkedNode prev;
DLinkedNode next;
public DLinkedNode() {}
public DLinkedNode(int _key, int _value) {key = _key; value = _value;}
}
// 定义成员变量
private Map<Integer, DLinkedNode> cache = new HashMap<Integer, DLinkedNode>(); // hashmap用于get
private int size; // 当前大小
private int capacity; // 容量上限
private DLinkedNode head, tail; // 双向链表的头尾指针
public LRUCache(int capacity) { // 初始化
this.size = 0; // 初始容量为零
this.capacity = capacity; // 容量上限
// 使用伪头部和伪尾部节点
head = new DLinkedNode();
tail = new DLinkedNode();
head.next = tail; // 初始化指向
tail.prev = head;
}
public int get(int key) { // get 方法
DLinkedNode node = cache.get(key); // 先获取这个节点
if (node == null) { // 不存在的话返回 -1
return -1;
}
moveToHead(node); // 如果 key 存在,先通过哈希表定位,再移到头部
return node.value;
}
public void put(int key, int value) {
DLinkedNode node = cache.get(key); // 通过hashmap获取对应节点
if (node == null) {
DLinkedNode newNode = new DLinkedNode(key, value); // 如果 key 不存在,创建一个新的节点
cache.put(key, newNode); // 添加进哈希表
addToHead(newNode); // 添加一个新节点至双向链表的头部
++size; // 当前容量++
if (size > capacity) { // 如果超出容量,删除双向链表的尾部节点
DLinkedNode tail = removeTail();
cache.remove(tail.key); // 删除哈希表中对应的项
--size; // 容量--
}
} else {
node.value = value; // 如果 key 存在,先通过哈希表定位,再修改 value,并移到头部
moveToHead(node); // 移动旧有节点到队头
}
}
// 双向链表的操作
private void addToHead(DLinkedNode node) { // 新添加进来的节点放到队头
node.prev = head;
node.next = head.next;
head.next.prev = node;
head.next = node;
}
private void removeNode(DLinkedNode node) { // 删除一个节点
node.prev.next = node.next;
node.next.prev = node.prev;
}
private void moveToHead(DLinkedNode node) { // 移动一个已存在的节点到队头,相当于把这个节点从原来的双向链表中删除,再新建一个节点到队头
removeNode(node);
addToHead(node);
}
private DLinkedNode removeTail() { // 删除最久没有使用的节点
DLinkedNode res = tail.prev; // 先获取队尾的节点
removeNode(res); // 删除这个节点
return res; // 返回被删除的节点,用于将hashmap中的节点一并删除
}
}
注意事项
- 链
拓展延伸
- 官