链表提供了高效的节点重排能力,以及顺序性的节点访问方式,并且可以通过增删节点来灵活地调整链表的长度。作为一种常用的数据结构,链表内置在很多高级编程语言里面。既比数组复杂又比树简单,所以链表经常被面试官用来考察面试者的编程基本功。因此,链表是程序员必须熟练掌握的数据结构之一。近日在LeetCode上刷了很多道关于链表的题目,有几道非常经典巧妙,有助于理解链表的核心思想,所以特写此文进行总结。个人能力有限,如有纰漏,欢迎留言!

反转链表

/*
* 题目:反转一个单链表。
*
* 示例 1:
* 输入: 1->2->3->4->5->NULL
* 输出: 5->4->3->2->1->NULL
*
* 进阶:你可以迭代或递归地反转链表。你能否用两种方法解决这道题?
*/ struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
}; class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode *reverse = NULL;
ListNode *next;
while (head != NULL) {
next = head->next;
head->next = reverse;
reverse = head;
head = next;
}
return reverse;
}
};

反转链表的主要思路是:以迭代的方法,从头结点head开始,先保存当前节点的下一个节点next,然后将head节点的next指针指向reverse,reverse指向head,再将head指向next节点,next保存下一个节点。。。直至结束,reverse指向的链表就是所求。讲得貌似挺复杂,其实看代码实现就能一目了然。

两两交换链表中的节点

/*
* 题目:给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。
* 你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
*
* 示例:
* 给定 1->2->3->4, 你应该返回 2->1->4->3.
*/ struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
}; class Solution {
public:
ListNode* swapPairs(ListNode* head) {
shared_ptr<ListNode> ptr = make_shared<ListNode>();
ptr.get()->next = head;
ListNode* list = ptr.get();
while (list->next && list->next->next) {
ListNode* a = list->next;
ListNode* b = list->next->next;
a->next = b->next;
b->next = a;
list->next = b;
list = a;
}
return ptr.get()->next;
}
};

K个一组反转链表

/*
* 题目:给你一个链表,每 k 个节点一组进行翻转,请你返回翻转后的链表。
* k 是一个正整数,它的值小于或等于链表的长度。
* 如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。
*
* 示例:
* 给定一个链表: 1->2->3->4->5
* 当 k = 2 时,应当返回: 2->1->4->3->5
* 当 k = 3 时,应当返回: 3->2->1->4->5
*
* 说明:
* 你的算法只能使用常数的额外空间。
* 你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
*/ struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
}; class Solution {
public:
ListNode* reverseKGroup(ListNode* head, int k) {
shared_ptr<ListNode> ptr = make_shared<ListNode>();
ptr.get()->next = head;
ListNode* prev = ptr.get();
ListNode* tail = ptr.get();
while () {
int count = k;
while (count-- && tail) {
tail = tail->next;
}
if (!tail) break;
head = prev->next;
while (prev->next != tail) {
ListNode* cur = prev->next;
prev->next = cur->next;
cur->next = tail->next;
tail->next = cur;
}
prev = head;
tail = head;
}
return ptr.get()->next;
}
};

删除当前节点

/*
* 题目:删除链表当前节点。请编写一个函数,使其可以删除某个链表中给定节点,你将只被给定要求被删除的节点。
*
* 示例 1:
* 输入: head = [4,5,1,9], node = 5
* 输出: [4,1,9]
* 解释:给定你链表中值为 5 的第二个节点,那么在调用了你的函数之后,该链表应变为 4 -> 1 -> 9.
*/ struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
}; class Solution {
public:
// 这题目就是删除当前节点的意思
void deleteNode(ListNode* node) {
if (node->next == NULL) {
node = NULL;
} else {
node->val = node->next->val;
node->next = node->next->next;
} }
};

这道题难点在于,在单向链表中,要求你删除当前节点,只给出当前节点,无法求出前续节点,无法用常规的方法删除链表节点。这里将后继节点的值赋给当前节点,然后删除后继节点,同样达到删除当前节点的效果。这里需要发散思维,灵活运用才能解决问题。

合并链表

/*
* 题目:将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
*
* 示例 1:
* 输入: 1->2->4, 1->3->4
* 输出: 1->1->2->3->4->4
*
*/ struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
}; class Solution {
public:
// 迭代方法
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
shared_ptr<ListNode> ptr = make_shared<ListNode>(); // 用shared_ptr可自动析构头结点
ListNode* list = ptr.get();
while (l1 != NULL && l2 != NULL) {
if (l1->val < l2->val) {
list->next = l1;
l1 = l1->next;
} else {
list->next = l2;
l2 = l2->next;
}
list = list->next;
} if (l1 != NULL) list->next = l1;
if (l2 != NULL) list->next = l2; return ptr.get()->next;
} // 递归方法
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;
}
}
};

合并两个有序链表主要有迭代和递归两种方法。迭代思路:新建一个带头结点的链表,将两链表最小的节点一个个廉洁到新链表上,返回头结点的下一个节点就是所求。递归就更简单些:将两链表最小的节点抽出,用该节点的下一个节点和另一条链表递归,不断缩小范围。

/*
* 题目:合并 k 个排序链表,返回合并后的排序链表。请分析和描述算法的复杂度。
*
* 示例 1:
* 输入: [
* 1->4->5,
* 1->3->4,
* 2->6
* ]
* 输出: 1->1->2->3->4->4->5->6
*
*/ struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
}; class Solution {
public:
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
shared_ptr<ListNode> ptr = make_shared<ListNode>(); // 用shared_ptr可自动析构头结点
ListNode* list = ptr.get();
while (l1 != NULL && l2 != NULL) {
if (l1->val < l2->val) {
list->next = l1;
l1 = l1->next;
} else {
list->next = l2;
l2 = l2->next;
}
list = list->next;
} if (l1 != NULL) list->next = l1;
if (l2 != NULL) list->next = l2; return ptr.get()->next;
} // 合并 k 个排序链表,巧用上面两个链表合并的方法
ListNode* mergeKLists(vector<ListNode*>& lists) {
if (lists.empty()) return NULL; int step = lists.size();
while (step != ) {
for (int i = ; i < step/; ++i) {
lists[i] = mergeTwoLists(lists[i], lists[step--i]);
}
step = step - step/;
}
return lists[];
}
};

合并 k 个排序链表。需要调用合并两个链表的方法,这里的实现非常巧妙:首尾链表两两配对,不断两两合并,最后合并成一条链表即是所求。

链表是否有环

/*
* 题目:给定一个链表,判断链表中是否有环。
*
* 为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。
*
* 示例 1:
* 输入: head = [3,2,0,-4], pos = 1
* 输出: true
* 解释:链表中有一个环,其尾部连接到第二个节点。
*
* 进阶:你能用 O(1)(即,常量)内存解决此问题吗?
*/ struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
}; class Solution {
public:
bool hasCycle(ListNode *head) {
ListNode *slow = head;
ListNode *fast = head;
while (fast != NULL && fast->next != NULL) {
slow = slow->next;
fast = fast->next->next;
if (slow == fast)
return true;
}
return false;
}
};

判断链表是否有环的方法非常巧妙,用快慢指针的方法同时从链表的头结点出发,满指针每次走一步,快指针每次走两步,如果链表有环两者会相交,否则退出循环。

链表环的入口

/*
* 题目:给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
*
* 为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。
*
* 说明:不允许修改给定的链表。
*
* 示例 1:
* 输入: head = [3,2,0,-4], pos = 1
* 输出: tail connects to node index 1
* 解释:链表中有一个环,其尾部连接到第二个节点。
*
* 进阶:你是否可以不用额外空间解决此题?
*/ struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
}; class Solution {
public:
ListNode *connectNode(ListNode *head) {
ListNode *slow = head;
ListNode *fast = head;
while (fast != NULL && fast->next != NULL) {
slow = slow->next;
fast = fast->next->next;
if (slow == fast)
return slow;
}
return NULL;
}
ListNode *detectCycle(ListNode *head) {
ListNode *node = connectNode(head);
if (node != NULL) {
while () {
if (head == node)
return node;
head = head->next;
node = node->next;
}
}
return NULL;
}
};

求链表环的入口只需要在判断是否有环的基础上再稍加改进就可以求得,当快慢指针相交时,头结点和满指针同时每次走一步,相交处即为环的入口处,这里暂不证明。

链表相交节点

/*
* 题目:编写一个程序,找到两个单链表相交的起始节点。
*
* 示例 1:
* 输入: intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5],
* skipA = 2, skipB = 3
* 输出: Reference of the node with value = 8
* 输入解释:相交节点的值为 8 (注意,如果两个列表相交则不能为 0)。从各自的表头开始算起,
* 链表 A 为 [4,1,8,4,5],链表 B 为 [5,0,1,8,4,5]。在 A 中,相交节点前有 2 个节点;
* 在 B 中,相交节点前有 3 个节点。
*
* 注意:
* 如果两个链表没有交点,返回 null.
* 在返回结果后,两个链表仍须保持原有的结构。
* 可假定整个链表结构中没有循环。
* 程序尽量满足 O(n) 时间复杂度,且仅用 O(1) 内存。
*/ struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
}; class Solution {
public:
//法1:求两链表的长度之差 n,长的链表先走 n 步,然后同时走判断是否有相同点
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
int lenA = ;
int lenB = ;
ListNode* nodeA = headA;
ListNode* nodeB = headB;
while (nodeA != NULL) {
lenA++;
nodeA = nodeA->next;
}
while (nodeB != NULL) {
lenB++;
nodeB = nodeB->next;
} nodeA = headA;
nodeB = headB;
int n = lenA - lenB;
if (n > ) {
while (n--) {
nodeA = nodeA->next;
}
} else {
while (n++) {
nodeB = nodeB->next;
}
} while (nodeA != NULL && nodeB != NULL) {
if (nodeA == nodeB) {
return nodeA;
}
nodeA = nodeA->next;
nodeB = nodeB->next;
}
return NULL;
} // 法2: 分别从 A 和 B 链表头结点开始走,A 走到 NULL 就接着从 B 头结点走,
// B 走到NULL 就接着从 A 头节点头走,即 A->NULL->B->NULL 和 B->NULL->A->NULL 两个方向
// 此过程节点相等就退出循环,只存在两种情况:1.找到相交节点,2.两方向走到头,均为NULL,
// 是最佳方法
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
ListNode* nodeA = headA;
ListNode* nodeB = headB;
while (nodeA != nodeB) {
nodeA = nodeA ? nodeA->next : headB;
nodeB = nodeB ? nodeB->next : headA;
// 不能用下面这样:
// nodeA = nodeA->next ? nodeA->next : headB;
// nodeB = nodeB->next ? nodeB->next : headA;
// 上面两个链表会连在一起,貌似更合理,但如果输出的两条链表无交点则不满足,
// 所以都引入一个空节点可避免上述问题,当同时走完两链表时nodeA和nodeB都为NULL,退出循环
}
return nodeA;
} ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
if (headA == NULL || headB == NULL)
return NULL; // 让A首尾相连成环,变为求环入口节点的问题
ListNode* tailA = headA;
while (tailA->next != NULL) {
tailA = tailA->next;
}
tailA->next = headA; ListNode* slow = headB;
ListNode* fast = headB;
while (fast != NULL && fast->next != NULL) {
slow = slow->next;
fast = fast->next->next;
if (slow == fast)
break;
} if (fast == NULL || fast->next == NULL) {
// 满足此条件即链表无环
tailA->next = NULL; // 恢复A链表的形状
return NULL;
} fast = headB;
while () {
if (slow == fast) {
tailA->next = NULL; // 恢复A链表的形状
return slow;
}
slow = slow->next;
fast = fast->next;
}
}
};

上面这道题在今年换工作的面试过程中又遇到这道题,当时第一时间没有思路,后来是在面试官的再三提示之下才想出了法1的思路,最后理所当然被拒。所以我对这道题印象深刻,这次我总共总结了三种都不错的解法,以后面试再也不担心遇到这道题了,哈哈。具体的思路这里不再注解,看代码和注释就很容易弄懂。

删除链表倒数第N个节点

/*
* 题目:给定一个链表,删除链表的倒数第 n 个节点,并且返回链表的头结点。
*
* 示例:
* 给定一个链表: 1->2->3->4->5, 和 n = 2.
* 当删除了倒数第二个节点后,链表变为 1->2->3->5.
*
* 说明:给定的 n 保证是有效的。
*
* 进阶:你能尝试使用一趟扫描实现吗?
*/ struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
}; class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
if (head == NULL || n <= ) return head; ListNode* preK = head;
ListNode* list = head;
int k = ;
while (list->next != NULL) {
if (k < n) {
k++;
} else {
preK = preK->next;
}
list = list->next;
}
if (k == n) { //此时prek指向的是要删除的目标节点的前续节点
ListNode* tmp = preK->next;
preK->next = preK->next->next;
delete tmp;
} else if (k == n-) { //当k为n-1,差一步,此时prek肯定指向head,该删除head节点
ListNode* tmp = head;
head = head->next;
delete tmp;
}
return head;
}
};

链表排序

/*
* 题目:在 O(n log n) 时间复杂度和常数级空间复杂度下,对链表进行排序。
*
* 示例 1:
* 输入: 4->2->1->3
* 输出: 1->2->3->4
*
* 示例 2:
* 输入: -1->5->3->4->0
* 输出: -1->0->3->4->5
*
*/ struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
}; class Solution {
public:
// SGI STL方法
template<class T>
void swap(T& a, T& b) {
T tmp = a;
a = b;
b = tmp;
} // 合并链表
ListNode* merge(ListNode* l1, ListNode* l2) {
shared_ptr<ListNode> ptr = make_shared<ListNode>(); // 用shared_ptr可自动析构头结点
ListNode* list = ptr.get();
while (l1 != NULL && l2 != NULL) {
if (l1->val < l2->val) {
list->next = l1;
l1 = l1->next;
} else {
list->next = l2;
l2 = l2->next;
}
list = list->next;
} if (l1 != NULL) list->next = l1;
if (l2 != NULL) list->next = l2; return ptr.get()->next;
} // 切割头结点,即将头结点与原链表分类并返回
ListNode* spiceHead(ListNode*& head) {
ListNode* pos = head;
head = head->next;
pos->next = NULL;
return pos;
} ListNode* sortList(ListNode* head) {
if (head == NULL || head->next == NULL)
return head; ListNode* carry;
ListNode* counter[];
int refill = ; while (head) {
carry = spiceHead(head);
int i = ;
while (i < refill && counter[i]) {
counter[i] = merge(carry, counter[i]);
carry = NULL;
swap(carry, counter[i++]);
}
swap(carry, counter[i]);
if (i == refill) refill++;
} for (int i = ; i < refill; ++i) {
counter[i] = merge(counter[i], counter[i-]);
} return counter[refill-];
} // 归并的方法
ListNode* sortList(ListNode* head) {
if (head == NULL || head->next == NULL)
return head;
ListNode* slow = head;
ListNode* fast = head;
ListNode* brk = head;
while (fast != NULL && fast->next != NULL) {
fast = fast->next->next;
brk = slow;
slow = slow->next;
}
brk->next = NULL;
head = sortList(head);
slow = sortList(slow);
return merge(head, slow);
} ListNode* merge(ListNode* l1, ListNode* l2) {
shared_ptr<ListNode> ptr = make_shared<ListNode>(); // 用shared_ptr可自动析构头结点
ListNode* list = ptr.get();
while (l1 != NULL && l2 != NULL) {
if (l1->val < l2->val) {
list->next = l1;
l1 = l1->next;
} else {
list->next = l2;
l2 = l2->next;
}
list = list->next;
} if (l1 != NULL) list->next = l1;
if (l2 != NULL) list->next = l2; return ptr.get()->next;
}
};

链表的排序上面有两种方法实现,一种是我最近在研究的 SGI STL的实现方式,另一种是比较常规的归并实现,第一种方式我是将 SGI 的实现原理照搬在这里,具体的分析过程比较复杂,这里不再讲解,有兴趣的小伙伴可自己研究。

LeetCode之链表总结的更多相关文章

  1. Leetcode解题-链表(2.2.0)基础类

    1 基类的作用 在开始练习LeetCode链表部分的习题之前,首先创建好一个Solution基类,其作用就是: Ø  规定好每个子Solution都要实现纯虚函数test做测试: Ø  提供了List ...

  2. LeetCode 单链表专题 (一)

    目录 LeetCode 单链表专题 <c++> \([2]\) Add Two Numbers \([92]\) Reverse Linked List II \([86]\) Parti ...

  3. 【算法题 14 LeetCode 147 链表的插入排序】

    算法题 14 LeetCode 147 链表的插入排序: 解题代码: # Definition for singly-linked list. # class ListNode(object): # ...

  4. 关于leetcode中链表中两数据相加的程序说明

    * Definition for singly-linked list. * struct ListNode { * int val; * struct ListNode *next; * }; */ ...

  5. LeetCode之“链表”:Reverse Linked List && Reverse Linked List II

    1. Reverse Linked List 题目链接 题目要求: Reverse a singly linked list. Hint: A linked list can be reversed ...

  6. 关于LeetCode上链表题目的一些trick

    最近在刷leetcode上关于链表的一些高频题,在写代码的过程中总结了链表的一些解题技巧和常见题型. 结点的删除 指定链表中的某个结点,将其从链表中删除. 由于在链表中删除某个结点需要找到该结点的前一 ...

  7. Leetcode中单链表题总结

    以下是个人对所做过的LeetCode题中有关链表类型题的总结,博主小白啊,若有错误的地方,请留言指出,谢谢. 一.有关反转链表 反转链表是在单链表题中占很大的比例,有时候,会以各种形式出现在题中,是比 ...

  8. leetcode 876. 链表的中间结点 签到

    题目: 给定一个带有头结点 head 的非空单链表,返回链表的中间结点. 如果有两个中间结点,则返回第二个中间结点. 示例 1: 输入:[1,2,3,4,5] 输出:此列表中的结点 3 (序列化形式: ...

  9. leetcode 反转链表部分节点

    反转从位置 m 到 n 的链表.请使用一趟扫描完成反转. 说明:1 ≤ m ≤ n ≤ 链表长度. 示例: 输入: 1->2->3->4->5->NULL, m = 2, ...

随机推荐

  1. 洛谷P4979 矿洞:坍塌

    洛谷题目链接 珂朵莉树吼啊!!! 又是一道水题,美滋滋~~~ $A$操作完全模板区间赋值 $B$操作也是一个模板查询,具体看代码 注意:读入不要用$cin$,会$T$,如果你是大佬,会玄学东西当我没说 ...

  2. [Luogu] 树

    https://www.luogu.org/problemnew/show/P4092 树剖 + 线段树区间修改,单点查询 #include <bits/stdc++.h> using n ...

  3. 集合家族——ArrayList

    一.概述: ArrayList 是实现 List 接口的动态数组,所谓动态就是它的大小是可变的.实现了所有可选列表操作,并允许包括 null 在内的所有元素.除了实现 List 接口外,此类还提供一些 ...

  4. Codeforces 1009 F. Dominant Indices(长链剖分/树上启发式合并)

    F. Dominant Indices 题意: 给一颗无向树,根为1.对于每个节点,求其子树中,哪个距离下的节点数量最多.数量相同时,取较小的那个距离. 题目: 这类题一般的做法是树上的启发式合并,复 ...

  5. CodeForces Good Bye 2016

    A题,水题略过. B题,也水,但是想复杂了.只要运动超出[0,20000]的范围就算不可能了. C题,我自己的方法是解不等式,然后取最大的答案即可.代码如下: #include <stdio.h ...

  6. CF981D

    CF981D 题意: 给你n个数,要求你分成k堆.每堆的内部加和,每堆之间是相与.问最大的值. 解法: 二进制下最大的数的所有位一定是1,所以贪心去找是否最大一定是正确的. 然后DP记录+贪心就可以A ...

  7. 使用 in_memory 工作空间的注意事项

    来自:https://pro.arcgis.com/zh-cn/pro-app/tool-reference/appendices/using-the-in-memory-output-workspa ...

  8. opencv配置运行问题

    opencv是图像处理常用的一个库文件,对于一些新手来说,配置完后运行,总会有这样或者那样的错误,会挫伤其学习积极性,这里将常见的几种错误列举出来,供其参考和使用. 方法/步骤第一种错误叫no suc ...

  9. 2.2 Go语言基础之位运算操作

    一.位运算符 位运算符对整数在内存中的二进制位进行操作. 运算符 描述 & 参与运算的两数各对应的二进位相与. (两位均为1才为1) | 参与运算的两数各对应的二进位相或. (两位有一个为1就 ...

  10. SpringCloud(四)之Netflix开源组件断路器Hystrix介绍

    一.前言? 1.Netflix Hystrix断路器是什么? Netflix Hystrix是SOA/微服务架构中提供服务隔离.熔断.降级机制的工具/框架.Netflix Hystrix是断路器的一种 ...