203 移除链表元素
难度:简单
在创建虚拟头结点dummyhead和用于遍历的指针cur时,我都搞不清楚要用ListNode还是ListNode*,查资料得知:使用new时, C++ 会为新创建的对象在程序的内存堆(heap)上分配足够的空间,然后返回这块内存的地址,这样你就可以通过这个地址来访问这个新对象。也就是说
ListNode* dummyhead = new ListNode(0);
这句话的意思是为新创建的ListNode类型的对象分配一块内存并返回这块内存的地址,然后把这个地址赋给新创建的指针dummyhead,所以这里用的应该是ListNode*也就是一个指向ListNode类型的指针,因为new返回的就是指针类型,所以创建的对象也应该是指针类型。
tmp、cur也是同理。
代码:
/**
* 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) {
ListNode* dummyhead = new ListNode(0);
dummyhead->next = head;
ListNode* cur = dummyhead; //表示定义了一个指向ListNode类型的指
//针,可以储存ListNode类型对象的内存地址
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 = dummyhead->next;
delete dummyhead;
return head;
}
};
707 设计链表
难度:中等
要点:
- 初始化cur时是指向dummyhead还是dummyhead->next
- 添加节点时两个指针先对哪一边进行赋值才不会出错
- 搞不清楚while里面的条件时可以取特殊情况n=0即n为头结点的情况进行判断
- 链表长度变化时要记得加上size++/size- -
问题:
跑的时候一直报错,后面改了几个地方之后突然可以了,不知道是改了哪里有效果的,改动主要在
- 把一些NULL改成了nullptr
- 删除了一些多余的代码,比如不需要引入cur的时候又引入了
- 其他我也不太记得了
下次再重新做的时候看看能不能写对吧。。。
代码:
class MyLinkedList {
public:
struct LinkedNode {
int val;
LinkedNode* next;
LinkedNode(int val): val(val), next(nullptr) {}
};
LinkedNode* _dummyhead;
int _size;
MyLinkedList() { //初始化链表
_dummyhead = new LinkedNode(0);
_size = 0;
}
int get(int index) { //获取下标为index的节点的值
LinkedNode* cur = _dummyhead->next;
if(index < 0 || index > (_size - 1) ) {
return -1;
}
else {
while(index) {
cur = cur->next;
index--;
}
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* cur = _dummyhead;
LinkedNode* NewNode = new LinkedNode(val);
while(cur->next != nullptr) {
cur = cur->next;
}
cur->next = NewNode;
_size++;
}
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;
index--;
}
NewNode->next = cur->next;
cur->next = NewNode;
_size++;
}
void deleteAtIndex(int index) {
LinkedNode* cur = _dummyhead;
if(index < 0 || index > _size - 1) {
return;
}
while(index) {
cur = cur->next;
index--;
}
LinkedNode* tmp = cur->next;
cur->next = cur->next->next;
delete tmp;
tmp = nullptr;
_size--;
}
};
/**
* Your MyLinkedList object will be instantiated and called as such:
* MyLinkedList* obj = new MyLinkedList();
* int param_1 = obj->get(index);
* obj->addAtHead(val);
* obj->addAtTail(val);
* obj->addAtIndex(index,val);
* obj->deleteAtIndex(index);
*/
206 翻转链表
难度:简单
思路:
首先是易于理解的双指针法,可以先手绘一下草图搞清楚pre和cur指针赋值、翻转、遍历的过程,想清楚初始化条件和终止条件再动手。代码按照初始化,引入temp指针,翻转指针,pre、cur赋值,往后遍历的顺序来写即可。
递归方法中的三个return一开始还是不太明白,后面大概懂了。
- 首先是reverselist函数中的return。定义了一个新的reverse函数用于递归翻转链表,reverse函数最后会返回翻转之后链表的头结点,所以在主函数reverselist中有return reverse(xx)这句代码。
接下来是递归函数中的两个return
- 第一个出现的return是递归结束时的条件语句,返回最后翻转之后的头结点。
- 第二个return是递归的核心,当函数运行到这里时,说明if条件还不满足,则指针需要继续往后遍历,所以这里return reverse(xx)的意思是返回下一次调用reverse函数的结果。而事实上,在遍历结束之前,也就是cur到达NULL之前,reverse函数都是没有结果的,所以就会不断调用自身函数,直到触发if语句,return pre的时候,代码才会从不断重复的递归中结束,得到一个真正的返回值,返回到reverse函数,也就是主函数那里。
双指针法:
/**
* 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* temp;
ListNode* cur = head;
ListNode* pre = NULL;
while(cur) {
temp = cur->next;
cur->next = pre;
pre = cur;
cur = temp;
}
return pre;
}
};
递归法:
/**
* 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* reverse(ListNode* pre, ListNode* cur) {
// 专门写个递归函数来反转
if(cur == NULL) return pre;
ListNode* temp = cur->next;
cur->next = pre; //翻转指针
return reverse(cur, temp);
}
ListNode* reverseList(ListNode* head) {
return reverse(NULL, head);
}
};
24 两两交换链表中的结点
难度:中等
思路: 本质是通过链表中各个节点指针指向的改变来进行两两的节点交换,可以通过画图想清楚指针指向的变化过程,需不需要引入虚拟头结点?哪些节点需要先赋值给temp?cur要指向谁?cur往后移动多少步?
另外,while中的条件也需要注意,两个条件的先后顺序是有差异的,必须先判断cur->next再判断cur->next->next,否则会出现空指针异常。因为如果cur->next已经为空的时候,却把判断条件cur->next->next写在cur->next前面,则会出现空指针->next的情况,此即为空指针异常。
代码:
/**
* 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) {
ListNode* dummyhead = new ListNode(0);
dummyhead->next = head;
ListNode* cur = dummyhead;
while(cur->next != NULL && cur->next->next != NULL) {
//这里是在寻找链表末尾,即判断什么时候结束遍历,当到达最后一个节点或者
//只剩一个节点时,都不需要再往后遍历了,没得两两交换了
//注意,上面必须先判断next在判断next->next,否则会出现
//空指针异常!
ListNode* temp = cur->next;
ListNode* temp1 = cur->next->next->next;
cur->next = cur->next->next;
cur->next->next = temp;
cur->next->next->next = temp1;
cur = cur->next->next;
}
ListNode* result = dummyhead->next;
delete dummyhead;
return result;
}
};
19 删除链表的倒数第n个结点
难度:中等
思路: 一开始以为可以直接通过函数获取链表长度,之后利用一个指针从虚拟头结点往后遍历size-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* dummyhead = new ListNode(0);
dummyhead->next = head;
ListNode* cur = dummyhead;
int size = 0;
while(cur->next != NULL) {
cur = cur->next;
size++;
}
cur = dummyhead;
for(int i = 0; i < size-n; i++) {
cur = cur->next;
}
cur->next = cur->next->next;
ListNode* tmp = dummyhead->next;
delete dummyhead;
return tmp;
}
};
也可以用双指针。
快慢指针都从虚拟头结点开始往后走,快指针先走n+1步,然后快慢指针同时往前移动直到快指针指向链表末尾的NULL。快指针走n+1步的目的是为了让快指针到达NULL时慢指针指向删除结点的前一位,方便执行删除操作。
题目限定了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* dummyhead = new ListNode(0);
dummyhead->next = head;
ListNode* fast = dummyhead;
ListNode* slow = dummyhead;
while(n--) {
fast = fast->next;
}
fast = fast->next; //让fast多走一步,slow最终才能停在
//被删除结点的上一个结点
while(fast != NULL) {
slow = slow->next;
fast = fast->next;
}
slow->next = slow->next->next;
ListNode* result = dummyhead->next;
delete dummyhead;
return result;
}
};
面试题 02.07 链表相交
难度:简单
这题需要注意链表相交是什么意思?不是数值相等,而是指针相等!
思路: 首先求出两个链表的长度,然后将链表从末尾对齐,将两个链表的指针都指向他们重合处的第一个节点,从这里依次往后遍历,不比较数值,只对比指针是否相等。
代码:
/**
* 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) {
int lenA = 0, lenB = 0;
ListNode* curA = headA;
ListNode* curB = headB;
//求链表长度
while(curA != NULL) {
curA = curA->next;
lenA++;
}
while(curB != NULL) {
curB = curB->next;
lenB++;
}
curA = headA;
curB = headB;
if(lenA > lenB) { // 如果链表A比B长
for(int i = 0; i < lenA - lenB; i++) {
curA = curA->next;
}
}
else {
for(int i = 0; i < lenB - lenA; i++) {
curB = curB->next;
}
}
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) {
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;
}
};
142 环形链表II
难度:中等
思路: 一开始没想清楚第一个while里面的条件是什么,其实这里的条件针对的是链表中不存在环的情况,这里有两个需要注意的点:
一是快指针走在前面,所以只需要判断快指针是否为空即可;
二是快指针一次走两步,为了避免出现空指针异常,即出现让空指针指向next的情况,需要判断fast->next也不为空才可以。
当满足了上述条件之后,说明链表中存在环,则进入下一步,当两指针相遇时需要做什么操作。这时候就需要引入index1和index2,引入这两个指针是为了求解开始入环的第一个节点。当这两个指针相遇时即在环的入口处,即为所求节点。
代码:
/**
* 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* slow = head;
ListNode* fast = head;
while(fast!= NULL && fast->next != NULL) {
// 这里是考虑链表是否有环,没有的时候则会存在NULL
slow = slow->next;
fast = fast->next->next;
if(fast == slow) {
ListNode* index1 = head;
ListNode* index2 = slow;
while(index1 != index2) {
index1 = index1->next;
index2 = index2->next;
}
return index1;
}
}
return NULL;
}
};
链表专题小结
203 移除链表元素: 虚拟头结点,cur指向目标节点的前一位,判断cur->next->val是否为目标值val,是的话执行删除操作,直接从前一个节点(即当前cur所指的节点)指向后一节点,再把当前节点删除即可。
707 设计链表: 涵盖链表增删改查的大综合题目,多多练习,即可熟练掌握链表基本操作!
206 翻转链表: 双指针法,需想清楚翻转过程的指针变化。
24 两两交换链表中的结点: 同上,需想清楚指针变化过程。
19 删除链表的倒数第n个结点: 可用普通方法也可用双指针。
面试题 02.07 链表相交: 搞清楚相交的定义,通过对齐末尾来判断。
142 环形链表II: 首先判断是否有环存在,其次寻找环的入口节点。
小结: 链表章节主要需要掌握虚拟头结点和双指针法,其他特殊题目可能涉及数学公式推导或其他方法,只能具体情况具体分析。需要搞清楚cur指针的指向,是dummyhead还是head,链表长度变化时size也要随之变化,删除节点时要释放内存,等。