代码随想录打卡 Day 2
1.链表的定义与操作
链表作为基本的数据结构类型,其主要形式有三种:
-
单链表
-
双链表
-
循环链表
由于刷代码题平时在OJ上默认定义已经给出,可以直接使用。而在面试/机试时,一般需要需要自己手写链表。因此,正确写出链表的定义是非常有必要的。一个单链表的定义如下所示:
struct ListNode{int val;ListNode *next;ListNode(int x):val(x),next(nullptr){} //节点的构造函数
};
其中,注意到,第三行给出了一个构造函数,通过这个构造函数,我们可以对一个新节点的val
变量直接赋值。
链表的基本操作包括了:
- 删除节点
- 添加节点
- 查询链表内元素(包括按照
val
值和索引值)
相比较数组,链表的长度可以是不固定的,并且可以动态增删,适合数据量不固定,频繁增删,较少查询的场景。
2.链表常见的6个操作
leetcode
题号:707.设计链表
【题目描述】
设计一个链表类,实现六个接口:
- 获取链表第
index
个节点的值(按索引查询) - 在链表的最前面插入一个节点(头插法)
- 在链表的最后面插入一个节点(尾插法)
- 在链表的第
index
个节点插入一个节点(按索引插入) - 删除链表的第
index
个节点(按索引删除) - 打印当前链表
【题目分析】
该题是练习链表操作的一道非常好的题目,六个结构覆盖了链表的常见操作,其代码如下:
class MyLinkedList {
public:struct Node {int val;Node* next;Node(int x) : val(x), next(NULL) {}};private:Node* _dummyhead; //设置一个伪头指针,方便后续操作int _size; // _size表示链表元素个数MyLinkedList() { //构造函数_dummyhead = new Node(0);_size = 0;}int get(int index) { //按索引查询元素if(index > _size -1 || index < 0)return -1;Node* cur=_dummyhead->next;while (index--){cur=cur->next;}return cur->val;}void addAtHead(int val){ //头插法Node* newnode = new Node(val);newnode->next = _dummyhead->next;_dummyhead->next = newnode;_size++;}void addAtTail(int val) { //尾插法Node* newnode =new Node(val);Node* cur=_dummyhead;while (cur->next != NULL){cur=cur->next;}cur->next=newnode;_size++;}void addAtIndex(int index, int val) { //按索引插入if(index>_size) return;if(index<0) index=0;Node* newnode = new Node(val);Node* cur=_dummyhead;while(index--){cur=cur->next;}newnode->next=cur->next;cur->next=newnode;_size++;}void deleteAtIndex(int index) { //按索引删除if(index>=_size || index<0) return;Node* cur=_dummyhead;while(index--){cur=cur->next;}Node* tmp=cur->next;cur->next=cur->next->next;delete tmp;tmp=nullptr;_size--;}void printList(){ //打印链表Node* cur=_dummyhead;while (cur->next != NULL){cout<<cur->next->val<<" ";cur=cur->next;}cout<<endl;}
};
3.利用伪头节点(dummy_Head)来简化操作
在单链表中,删除头节点与删除其他节点的操作不一样,除了共同的释放内存和指针移动之外,还需要移动头指针。通过设置一个伪头节点,最后将dummy_Head->next
设置为新的头节点即可。
以leetcode
203题移除链表元素为例,设置伪头节点可以方便处理待处理元素为头节点的情况。
本题的代码如下:
ListNode* removeElements(ListNode* head, int val) {ListNode* dummy = new ListNode(-1); //伪头节点的设置dummy->next = head;ListNode* cur = dummy;while(cur->next!=NULL){if(cur->next->val == val){ListNode* tmp = cur->next;cur->next = cur->next->next;delete tmp;}else{cur = cur->next;}}head = dummy->next; //设置新的头节点delete dummy; //删除伪头节点return head;
}
4.链表的反转
leetcode
题号:206.反转列表
【题目描述】
反转一个单链表,要求不能申请额外的内存空间
【题目分析】
反转链表是链表操作中的一个重要思路,这里使用双指针法和递归法来实现。对于双指针发,只需要改变next
指针的指向,不需要重新定义一个新的链表。
双指针法
双指针法的思想是定义一个cur
指针和prev
指针,分别进行遍历,并且使用nxt
指针保存下一个指针,便于遍历。
双指针法的代码如下所示:
ListNode* reverseList(ListNode *head) {if (head == NULL || head->next == NULL) { //对链表只有至多一个元素的边界条件处理return head;}ListNode *p = head->next;ListNode *prev = head;ListNode *nxt= NULL; //nct指针保存p所指的下一个结点while(p!=NULL){ //遍历nxt = p->next;p->next = prev;prev = p;p = nxt;}return prev;
}
递归法
递归法的思路实质上与双指针法的思路一致,但是递归法的代码较为抽象,比较难写。代码如下所示:
ListNode* reverse(ListNode * pre, ListNode * cur) {if (cur == NULL) return pre;ListNode * nxt = cur->next;cur->next = pre;//如下递归的算法,实质上完成了://pre=cur;//cur=temp;return reverse(cur, nxt);
}ListNode* reverseList(ListNode *head) {//与双指针法的初始化逻辑一致return reverse(NULL, head);
}
5.双指针法在链表中的应用
1)删除倒数第N个节点
【基本思路】
如果要删除倒数第N个节点,可以先让快指针fast
移动N步,然后让fast
和slow
同时移动,删除slow
所在的节点即可。
删除链表倒数第N个节点又应该使用伪头节点,让slow
节点指向待删除节点前一个节点。因此,可以让fast先动一步,然后让fast移动N步,最后让fast
和slow
同时移动一步。
基于以上思路,本题的代码如下所示:
ListNode* removeNthFromEnd(ListNode* head, int n) {ListNode* dummyHead = new ListNode(0, head);dummyHead->next = head; //伪头节点设置ListNode* fast = dummyHead;ListNode* slow = dummyHead;while(n-- && fast->next != nullptr) { //fast移动N步fast = fast->next;}fast = fast->next; //需要让slow指向要删除节点的前一个节点while(fast!=NULL){ //fast与slow同时移动一步fast = fast->next;slow = slow->next;}slow->next = slow->next->next;return dummyHead->next;
}
对快慢指针进行移动方法调整,就可以获得单链表的中间节点。
2)判断链表是否有环
【基本思路】
通过快慢指针法,分别定义fast
与slow
指针,从头节点出发,fast
移动两步,slow
移动一步,若fast
与slow
相遇,则说明有环,否则则无环。
但是,如果要寻找到环的入口,则是一个比较困难的问题。在这里,我们假设从头节点到环的入口的节点数为$x$,环形入口节点到fast
与slow
节点相遇节点数为$y$。从相遇节点再到环形入口节点书为$z$.
那么,相遇时,slow
走过了$(x+y)$个节点,fast
走过了$2(x+y)=x+y+n*(y+z)$个节点,其中$n$为fast
指针与slow
相遇时已经走过的圈数。
通过化简,得到
$$x=(n-1)(y+z)+z$$,
$$x+y=n(y+z)$$
此时,可以在相遇节点处定义一个指针index1
,在头节点处定义index2
,让两个指针同时移动一步,两者相遇的位置即为环形入口的起点。
根据以上的思路,可以整理出本题的解
ListNode *detectCycle(ListNode *head) {ListNode *slow = head, *fast = head;while (fast && fast->next) {slow = slow->next;fast = fast->next->next;if (slow == fast) { //找到环后的处理slow = head;while (slow != fast) {slow = slow->next;fast = fast->next;}return slow;}}return NULL;
}
3)判断两个链表的共同后缀
【基本思路】
两个单链表的头节点 headA
和 headB
,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null
。题目数据保证整个链式结构中不存在环。
本题可以通过先遍历A,B链表的长度,再使用双指针法进行操作。需要注意的是,交错链表并非数值相等,而是指针相等。
基于以上思考,可以给出如下的算法:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {if (headA == NULL || headB == NULL) {return NULL;}ListNode *pA = headA, *pB = headB;int lenA = 0, lenB = 0;while(pA!=NULL){lenA++;pA = pA->next;}while(pB!=NULL){lenB++;pB = pB->next;}pA = headA;pB = headB;if (lenB > lenA) {swap(lenA, lenB);swap(pA, pB);}//保证headA是长的,便于算法的求解int gap = lenA - lenB;//计算两者长度插值,为接下来使用快慢指针法做准备while (gap--){pA = pA->next;}while (pA != NULL) {if(pA == pB) return pA;pA = pA->next;pB = pB->next;}return NULL;}
6. 总结
对于以单链表为基本数据结构的算法题,主要的技巧就是伪头节点,双指针,以及反转链表。
- 伪头节点:其主要作用时简化链表的边界条件,简化操作,并保持链表结构的一致性。
- 双指针法:通过快慢指针的方法,可以快速查找中间节点,倒数K个节点等特殊节点,以及探测链表中的环。
- 反转链表:通过设置
prev
,curr
,next
三个指针,可以将链表进行反转操作,便于判断回文等操作的实现。