leetcode刷题——链表
单链表:链表是一种通过指针串联在一起的线性结构,每一个节点由两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null(空指针的意思)。
双链表:每一个节点有两个指针域,一个指向下一个节点,一个指向上一个节点。双链表 既可以向前查询也可以向后查询。
循环链表:链表首尾相连。循环链表可以用来解决约瑟夫环问题。
1)存储方式
链表是通过指针域的指针链接在内存中各个节点。所以链表中的节点在内存中不是连续分布的,而是散乱分布在内存中的某地址上,分配机制取决于操作系统的内存管理。
2)定义(可自己构造函数或者使用默认构造函数)
ListNode* head = new ListNode();
head->val = 5;
链表的删除 203、237、19
链表的遍历 430
链表的旋转与反转 61、24、206、92、25
链表高精度加法 2、445
链表的合并 21、23
203、移除链表元素
给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
while(head!=NULL && head->val==val){
//这里需要注意一个事情,&&和||判断如果左表达式判断了符合则不会判断右,所以要先判断空指针,否则访问val就会出现缓存错误
ListNode* temp = head;
head = head->next;
delete temp;
}
ListNode* current = head;
while(current!=NULL &¤t->next!=NULL){
if(current->next->val == val){
ListNode* temp = current->next;
current->next = current->next->next;
delete temp;
}
else current = current->next;
}
return head;
}
};
19、删除链表的倒数第 N 个结点
给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
最便捷的方法就是先计算长度,再遍历到需要删除的节点的位置:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* temp = head;
int length = 0;
while(temp){
length++;
temp = temp->next;
}
ListNode* dummy = new ListNode(0,head);
temp = dummy;
for(int i=1;i<=length-n;i++){
temp = temp->next;
}
temp->next = temp->next->next;
return dummy->next;
}
};
237、删除链表中的节点
有一个单链表的 head,我们想删除它其中的一个节点 node。
给你一个需要删除的节点 node 。你将 无法访问 第一个节点 head。
链表的所有值都是 唯一的,并且保证给定的节点 node 不是链表中的最后一个节点。
删除给定的节点。注意,删除节点并不是指从内存中删除它。这里的意思是:
给定节点的值不应该存在于链表中。
链表中的节点数应该减少 1。
node 前面的所有值顺序相同。
node 后面的所有值顺序相同。
不得不说这题真的很巧妙,因为没有头节点,按照常规思路会没法删除,但是反正都要删除这个元素,就干脆把值赋值成下一个节点的数值,然后删除下一个节点,假装删除的是本节点:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
void deleteNode(ListNode* node) {
node->val = node->next->val;
ListNode* temp = node->next;
node->next = node->next->next;
delete temp;
}
};
21、合并两个有序链表
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
和数组的时候相似类型的题目采用相同的双指针的思路,不过一开始编译的时候会少元素,后来想明白了,因为一个链表遍历完成后,另一个链表还有数据在,所以需要将另一个链表剩下的元素放到末尾:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
ListNode*l1 = list1;
ListNode*l2 = list2;
ListNode*dummy = new ListNode();
ListNode*cur = dummy;
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?l2:l1;
return dummy->next;
}
};
206、反转链表
给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
一开始把temp指向了l2,然后发现最后没有元素,因为第二步操作中已经把l2的指向转换了,所以链表中要特别关注每个节点的指向关系,所以改成指向下一个节点,在进行指针的后移就可以了,既可以说是双指针,也可以说是迭代法:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode* l1 = NULL;
ListNode* l2 = head;
while(l2!=NULL){
ListNode* temp = l2->next;
l2->next = l1;
l1 = l2;
l2 = temp;
}
return l1;
}
};
92、反转链表 II
给你单链表的头指针 head 和两个整数 left 和 right ,其中 left <= right 。请你反转从位置 left 到位置 right 的链表节点,返回 反转后的链表 。
感觉自己和参考答案的思路是一样的,就是把中间需要反转的链表切出来,然后再把剩余的部分加上去,但是我一开始就一直不想说再写一个外函数进行调用,然后测试一些特殊的例子的时候一直无法通过,下面是一开始写的代码:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* reverseBetween(ListNode* head, int left, int right) {
if(head->next == NULL) return head;
if(left == right) return head;
ListNode* dummy = new ListNode(0);
dummy->next = head;
int num = 1;
ListNode* cur = dummy->next;
while(num<left-1){
cur = cur->next;
num++;
}
ListNode* temp1 = cur;
ListNode* l1 = cur->next;
ListNode* end = cur->next;
while(num<right){
cur = cur->next;
num++;
}
ListNode* l2 = cur;
ListNode* temp2 = cur->next;
ListNode* l3 = temp1;
while(l1!=temp2){
ListNode* temp = l1->next;
l1->next = l3;
l3 = l1;
l1 = temp;
}
temp1->next = l3;
end->next = temp2;
return dummy->next;
}
};
后来答案的代码如下,思路相同,采用了外函数进行反转操作:
class Solution {
private:
void reverseLinkedList(ListNode *head) {
// 也可以使用递归反转一个链表
ListNode *pre = NULL;
ListNode *cur = head;
while (cur != nullptr) {
ListNode *next = cur->next;
cur->next = pre;
pre = cur;
cur = next;
}
}
public:
ListNode *reverseBetween(ListNode *head, int left, int right) {
// 因为头节点有可能发生变化,使用虚拟头节点可以避免复杂的分类讨论
ListNode *dummyNode = new ListNode(-1);
dummyNode->next = head;
ListNode *pre = dummyNode;
// 第 1 步:从虚拟头节点走 left - 1 步,来到 left 节点的前一个节点
// 建议写在 for 循环里,语义清晰
for (int i = 0; i < left - 1; i++) {
pre = pre->next;
}
// 第 2 步:从 pre 再走 right - left + 1 步,来到 right 节点
ListNode *rightNode = pre;
for (int i = 0; i < right - left + 1; i++) {
rightNode = rightNode->next;
}
// 第 3 步:切断出一个子链表(截取链表)
ListNode *leftNode = pre->next;
ListNode *curr = rightNode->next;
// 注意:切断链接
pre->next = nullptr;
rightNode->next = nullptr;
// 第 4 步:同第 206 题,反转链表的子区间
reverseLinkedList(leftNode);
// 第 5 步:接回到原来的链表中
pre->next = rightNode;
leftNode->next = curr;
return dummyNode->next;
}
};
24、两两交换链表中的节点
给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
链表的节点与节点之间的交换一定要注意先后顺序,不然就会乱套,画图也是一个很有效的方法:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* swapPairs(ListNode* head) {
if(head==NULL) return head;
if(head->next==NULL) return head;
ListNode* dummy = new ListNode(0);
dummy->next = head;
ListNode* l1 = dummy;
while(l1->next !=NULL && l1->next->next !=NULL){
ListNode* temp1 = l1->next;
ListNode* temp2 = l1->next->next->next;
l1->next = l1->next->next;
l1->next->next = temp1;
l1->next->next->next = temp2;
l1 = l1->next->next;
}
return dummy->next;
}
};
61、旋转链表
给你一个链表的头节点 head ,旋转链表,将链表每个节点向右移动 k 个位置。
一开始的想法是不是写个递归,每次移动一位的递归,然后看答案说是构造一个环形链表,的确会简便很多,链表的灵活性很大,要学会运用:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* rotateRight(ListNode* head, int k) {
if(k==0||head==NULL||head->next==NULL) return head;
int num = 1;
ListNode* cur = head;
while(cur->next!=NULL){
cur = cur->next;
num++;
}
int res = num - (k%num);//移动k次后最后一个数字在原链表中的位数
cur->next = head;//环状
while(res){
cur = cur->next;
res--;
}
ListNode* newhead = cur->next;//切开环状结构
cur->next = NULL;
return newhead;
}
};
2、两数相加
给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。
请你将两个数相加,并以相同形式返回一个表示和的链表。
你可以假设除了数字 0 之外,这两个数都不会以 0 开头。
题目中给出的例子基本上就是从末尾开始计算的实际数字相加的计算过程,要注意的就是进位关系,以及最后一位如果相加还要进位的新建节点,这题也可以作为两数相加计算的标准过程:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
ListNode* start = NULL,*end = NULL;
int carry = 0;//记录进位
while(l1||l2){
int num1 = l1?l1->val:0;
int num2 = l2?l2->val:0;
int sum = num1 + num2 + carry;
if(!start){
start = end = new ListNode(sum%10);
}else{
end->next = new ListNode(sum%10);
end = end->next;//end用于记录下一个相加之和并逐渐移位
}
carry = sum/10;
if(l1)l1 = l1->next;
if(l2)l2 = l2->next;
}
if(carry) {
end->next = new ListNode(carry); //数字遍历之后还存在多一个数字的进位
end = end->next;
}
end->next = NULL;
return start;
}
};
445、两数相加 II
给你两个 非空 链表来代表两个非负整数。数字最高位位于链表开始位置。它们的每个节点只存储一位数字。将这两数相加会返回一个新的链表。
你可以假设除了数字 0 之外,这两个数字都不会以零开头。
如果是顺序结构的链表就用链表遍历,如果是逆序的链表,那么栈就是一个不二之选,这题也是一样的,先将数字入栈,那么就和之前两数相加的操作过程一样了,不过这题有一个新的地方在于要注意如何把出栈数据映射到链表上的,过程就是用一个指针一直指向后一个数字,然后每次结果新建一个节点,将新建节点指向后一个节点,再将后一个节点更新,循环往复:
class Solution {
public:
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
stack<int> s1, s2;
while (l1) {
s1.push(l1 -> val);
l1 = l1 -> next;
}
while (l2) {
s2.push(l2 -> val);
l2 = l2 -> next;
}
int carry = 0;
ListNode* ans = nullptr;
while (!s1.empty() or !s2.empty() or carry != 0) {
int a = s1.empty() ? 0 : s1.top();
int b = s2.empty() ? 0 : s2.top();
if (!s1.empty()) s1.pop();
if (!s2.empty()) s2.pop();
int cur = a + b + carry;
carry = cur / 10;
cur %= 10;
auto curnode = new ListNode(cur);
curnode -> next = ans;
ans = curnode;
}
return ans;
}
};
141、环形链表
给你一个链表的头节点 head ,判断链表中是否有环。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos 不作为参数进行传递 。仅仅是为了标识链表的实际情况。
如果链表中存在环 ,则返回 true 。 否则,返回 false 。
最先想到的肯定是哈希表,这样可以直接记录已经遍历过的节点:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
bool hasCycle(ListNode *head) {
std::unordered_set<ListNode *> set;
ListNode* cur = head;
while(cur!=NULL){
if(set.count(cur))return true;
set.insert(cur);
cur = cur->next;
}
return false;
}
};
从官方又学习了一种快慢指针的方法,就是所谓的龟兔赛跑,快指针每次移动两次,慢指针每次移动一次,如果快慢指针能够相遇,则说明成环
class Solution {
public:
bool hasCycle(ListNode* head) {
if (head == nullptr || head->next == nullptr) {
return false;
}
ListNode* slow = head;
ListNode* fast = head->next;
while (slow != fast) {
if (fast == nullptr || fast->next == nullptr) {
return false;
}
slow = slow->next;
fast = fast->next->next;
}
return true;
}
};
142、环形链表 II
给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。
不允许修改 链表。
如果依旧沿用哈希表的思路,就会很简单,就是直接在判断有重复节点的时候就直接返回这个节点,因为该节点一定就是入环的第一个节点:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
std::unordered_set<ListNode *> set;
ListNode* cur = head;
while(cur!=NULL){
if(set.count(cur))return cur;
set.insert(cur);
cur = cur->next;
}
return NULL;
}
};
如果采用快慢指针的思路,通过图形的分析计算,首先可以知道快慢指针相遇一定是在环内,然后知道当快慢指针相遇之后,说明成环,同时当相遇之后,从头指针到入口的位置与环内剩余的节点数相同,所以只需要在两个同步前进直至相遇就是入口处:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
ListNode* fast = head;
ListNode* slow = head;
while(fast != NULL && fast->next != NULL) {//判断成环条件
slow = slow->next;
fast = fast->next->next;
// 快慢指针相遇,此时从head 和 相遇点,同时查找直至相遇
if (slow == fast) {
ListNode* index1 = fast;
ListNode* index2 = head;
while (index1 != index2) {
index1 = index1->next;
index2 = index2->next;
}
return index2; // 返回环的入口
}
}
return NULL;
}
};
160、相交链表
给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null 。
图示两个链表在节点 c1 开始相交:
一开始都是想用哈希表计算,然后写的第一版代码觉得两个都依次移位就可以了,但是后来测试的时候发现一个问题就是这样如果一个链表长度很短,可能先到NULL了,另一个还没有交叉:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
std::unordered_set<ListNode*>set;
ListNode* curA = headA;
ListNode* curB = headB;
while(curA!=NULL && curB!=NULL){
if(set.count(curA)) return curA;
if(set.count(curB)) return curB;
set.insert(curA);
set.insert(curB);
curA = curA->next;
curB = curB->next;
}
return NULL;
}
};
所以正确的哈希表方法应该是先遍历A链表,在对B链表进行查找:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
std::unordered_set<ListNode*>set;
ListNode* curA = headA;
ListNode* curB = headB;
while(curA!=NULL){
set.insert(curA);
curA = curA->next;
}
while(curB!=NULL){
if(set.count(curB))return curB;
curB = curB->next;
}
return NULL;
}
};
但是同样,如果不采用哈希表这种没有脑子的做法,就要对链表进行操作,其实就是改进了我一开始错误的想法,将指针都移动到可能发生相交的位置:
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
ListNode* curA = headA;
ListNode* curB = headB;
int lenA = 0, lenB = 0;
while (curA != NULL) { // 求链表A的长度
lenA++;
curA = curA->next;
}
while (curB != NULL) { // 求链表B的长度
lenB++;
curB = curB->next;
}
curA = headA;
curB = headB;
// 让curA为最长链表的头,lenA为其长度
if (lenB > lenA) {
swap (lenA, lenB);
swap (curA, curB);
}
// 求长度差
int gap = lenA - lenB;
// 让curA和curB在同一起点上(末尾位置对齐)
while (gap--) {
curA = curA->next;
}
// 遍历curA 和 curB,遇到相同则直接返回
while (curA != NULL) {
if (curA == curB) {
return curA;
}
curA = curA->next;
curB = curB->next;
}
return NULL;
}
};
最后看官方题解,发现了一个更绝的思路,
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
if (headA == nullptr || headB == nullptr) {
return nullptr;
}
ListNode *pA = headA, *pB = headB;
while (pA != pB) {
pA = pA == nullptr ? headB : pA->next;
pB = pB == nullptr ? headA : pB->next;
}
return pA;
}
};
最后的最后,来个链表常规操作的大全集题目,复习一下链表的基本操作,也就暂时链表的题目告一段落了。
707、设计链表
设计链表的实现。您可以选择使用单链表或双链表。单链表中的节点应该具有两个属性:val 和 next。val 是当前节点的值,next 是指向下一个节点的指针/引用。如果要使用双向链表,则还需要一个属性 prev 以指示链表中的上一个节点。假设链表中的所有节点都是 0-index 的。
在链表类中实现这些功能:
get(index):获取链表中第 index 个节点的值。如果索引无效,则返回-1。
addAtHead(val):在链表的第一个元素之前添加一个值为 val 的节点。插入后,新节点将成为链表的第一个节点。
addAtTail(val):将值为 val 的节点追加到链表的最后一个元素。
addAtIndex(index,val):在链表中的第 index 个节点之前添加值为 val 的节点。如果 index 等于链表的长度,则该节点将附加到链表的末尾。如果 index 大于链表长度,则不会插入节点。如果index小于0,则在头部插入节点。
deleteAtIndex(index):如果索引 index 有效,则删除链表中的第 index 个节点。
class MyLinkedList {
public:
// 定义链表节点结构体
struct LinkedNode {
int val;
LinkedNode* next;
LinkedNode(int val):val(val), next(nullptr){}
};
// 初始化链表
MyLinkedList() {
_dummyHead = new LinkedNode(0); // 这里定义的头结点 是一个虚拟头结点,而不是真正的链表头结点
_size = 0;
}
// 获取到第index个节点数值,如果index是非法数值直接返回-1, 注意index是从0开始的,第0个节点就是头结点
int get(int index) {
if (index > (_size - 1) || index < 0) {
return -1;
}
LinkedNode* cur = _dummyHead->next;
while(index--){ // 如果--index 就会陷入死循环
cur = cur->next;
}
return cur->val;
}
// 在链表最前面插入一个节点,插入完成后,新插入的节点为链表的新的头结点
void addAtHead(int val) {
LinkedNode* newNode = new LinkedNode(val);
newNode->next = _dummyHead->next;
_dummyHead->next = newNode;
_size++;
}
// 在链表最后面添加一个节点
void addAtTail(int val) {
LinkedNode* newNode = new LinkedNode(val);
LinkedNode* cur = _dummyHead;
while(cur->next != nullptr){
cur = cur->next;
}
cur->next = newNode;
_size++;
}
// 在第index个节点之前插入一个新节点,例如index为0,那么新插入的节点为链表的新头节点。
// 如果index 等于链表的长度,则说明是新插入的节点为链表的尾结点
// 如果index大于链表的长度,则返回空
// 如果index小于0,则在头部插入节点
void addAtIndex(int index, int val) {
if(index > _size) return;
if(index < 0) index = 0;
LinkedNode* newNode = new LinkedNode(val);
LinkedNode* cur = _dummyHead;
while(index--) {
cur = cur->next;
}
newNode->next = cur->next;
cur->next = newNode;
_size++;
}
// 删除第index个节点,如果index 大于等于链表的长度,直接return,注意index是从0开始的
void deleteAtIndex(int index) {
if (index >= _size || index < 0) {
return;
}
LinkedNode* cur = _dummyHead;
while(index--) {
cur = cur ->next;
}
LinkedNode* tmp = cur->next;
cur->next = cur->next->next;
delete tmp;
_size--;
}
// 打印链表
void printLinkedList() {
LinkedNode* cur = _dummyHead;
while (cur->next != nullptr) {
cout << cur->next->val << " ";
cur = cur->next;
}
cout << endl;
}
private:
int _size;
LinkedNode* _dummyHead;
};