先附上地址方便对照着看:代码随想录
二、链表
1、链表结构:单链表、双链表、循环链表、补充一个静态链表,但其实感觉应该归类进数组
静态链表:数组第一个元素不存储数据,它的指针域存储第一个元素所在的数组下标,第一个元素的指针域存储第二个元素的下标……,最后一个元素的指针域值为-1
链表的定义:构造函数在初始化结点的时候非常好用,但我不太习惯用这个,因为样式太怪了,考场上容易写错,其实不使用构造函数,在初始化的时候赋值也是可以的,多几行代码的事。
// 单链表
struct ListNode {
int val; // 节点上存储的元素
ListNode *next; // 指向下一个节点的指针
ListNode(int x) : val(x), next(NULL) {} // 节点的构造函数
};
2、移除链表元素:我一直以为头结点是不带值的那种,结果原来就是第一个带值结点吗……
如果直接移除的话会比较麻烦,因为头结点和非头结点的处理不太一样,非头结点都是通过更改待移除结点的前一个结点的next指针,指向待移除结点的后一个结点来完成移除。但头结点没有前一个结点。所以这里用了一个链表非常经典的思路:虚拟头结点
我们来分析下步骤:
step 1:定义一个结点,next指针指向头结点,这个结点就是新的头结点
step 2:从新的头结点开始向后遍历,要注意每次要判断删除的应该是后一个结点的值(如果判断现在遍历指针所指向结点的值,就找不到前一个结点了,这可不是双链表),还要注意遍历停止条件应该是 cur->next == NULL,这时说明已经在链表尾结点了。
step 3:判断该不该删除,不该删除就继续往后走,该删除的话需要定义一个临时结点指向待删除结点,删除步骤为:遍历结点next指针指向待删除结点的后一个结点,然后释放待删除结点。
这里有个要注意的点:判断该不该删除应该是if-else语句而不是if语句,如果只是if语句的话删除完结点就会往后走了,但是你完全不知道你删除完结点的下一个结点是不是也是要删除的结点!举个例子:{6,7,7},删除7的话你如果删除完就 cur = cur->next,那第二个7你就删不掉。
上核心代码:其实我个人不喜欢贸然free结点,无非就是多点内存的事,如果在复杂的题中代码没写好就随便释放结点的话可能会造成链表断裂,应对机试还是要增加容错比较好。
while(cur->next != NULL){
if (cur->next->val == val){
ListNode *tmp = cur->next;
cur->next = cur->next->next;
free(tmp);
}
else{
cur = cur->next;
}
}
3、设计链表:很好的一道题,使我的脑壳旋转。
这道题基本没什么好说的,需要注意的就是C语言这道题必须用虚拟头指针,参数都是一级指针,头结点确定后没办法改指向。还有就是注意一些坑点比如只有一个头结点的情况,这时候操作链表可能会报错。
4、翻转链表:这也能用双指针法啊,我只能想到头插法。
双指针法:基本思路:两指针一前一后向后遍历,每次向后走都改变结点next指针指向
step 1:定义双指针,一个 cur 指向头结点,一个 pre 指向NULL,这个很重要,因为等会头结点翻转后会变成尾结点,尾结点的next指针就指向NULL
step 2:开始遍历,定义临时结点 tmp 保存一下现在要翻转的结点的后一个结点,也就是cur->next 不然你翻转完就找不到了
step 3:开始翻转,让当前结点的指针指向前一个结点,也就是cur->next = pre ,然后让pre = cur ,cur =tmp,相当于 cur 和 pre 全部后移了
重复step 2 和step 3直至遍历完成,注意这次遍历的终止条件是 cur==NULL,为什么呢,因为你如果你像前面那样用 cur->next == NULL来停止循环,那么在到达尾结点的时候就退出循环了,尾结点的next依然指向NULL,这不符合题意。所以,设定终止条件的标准就是:看题目是否要对尾结点进行操作!
ListNode* temp; // 保存cur的下一个节点
ListNode* cur = head;
ListNode* pre = NULL;
while(cur) {
temp = cur->next; // 保存一下 cur的下一个节点,因为接下来要改变cur->next
cur->next = pre; // 翻转操作
// 更新pre 和 cur指针
pre = cur;
cur = temp;
}
5、两两交换链表中的结点:好绕啊,但是虚拟头结点真神吧
还是得虚拟头结点,要不然每次针对头结点(没有前一个指针指向头结点),还要单独处理。
设头结点0:0->1->2->3->4 target:0->2->1->3->4
step 1 : 0=bef 1=cur ,原本是0指向1,1指向2
step 2 : 0->2 1->2 2->3->4 ,即0和1都指向2
step 3 : 0->2->3->4 1->3->4, 即1指向3
step 4 : 0->2->1->3->4,即2指向1
step 5 : bef=1 cur = 3 迭代,为什么bef=1,因为:bef不变位置,0就没动过,cur和cur->next变
关于这类题目,把思路写在代码开头是非常好的,分步骤看看有没有问题,考试总不能画图啊
struct ListNode* swapPairs(struct ListNode* head) {
typedef struct ListNode ListNode;
ListNode *shead = (ListNode *)malloc(sizeof(ListNode));
shead->next = head;
ListNode *bef = shead;
ListNode *cur = head; //step 1
while(bef && cur && cur->next){
bef->next = cur->next; //step 2
cur->next = bef->next->next; //step 3
bef->next->next = cur; //step 4
bef = cur;
cur = cur->next; //step 5
}
return shead->next;
}
6、删除链表的倒数第N个结点:经典,太经典了,09年408统考的代码题让你找倒数第N个
因为做过,所以还是马上想到双指针法:一个先走N步,一个再开始走,两个一起向后走,先走的到表尾了,后走的就是倒数第N个
但这里要做一个改动:因为你要删除结点,你必须得留在倒数第N+1个才能对第N个进行删除,所以先走的要走N+1步才能一起走。
对了,还是需要虚拟头结点,不然你不好删除头结点,写到这里感觉链表的虚拟头结点真的感觉哪道题都能用吧
步骤如下:
step 1:定义fast指针和slow指针,初始值为虚拟头结点
step 2:fast首先走n + 1步
step 3:fast和slow同时移动,直到fast指向末尾
step 4:删除slow指向的下一个节点
理论成立,实践开始
ListNode *cur = shead;
ListNode *bef = shead; //step 1
for(int i=0;i<n;i++)
cur = cur->next; //step 2
while(cur){
if(cur->next == NULL){ //step 4
ListNode *tmp = bef->next;
bef->next = tmp->next;
break;
}
else{ //step 3
bef = bef->next;
cur = cur->next;
}
}
7、链表相交:好好好,又是408原型题,12年的那道找出两个字符串所指链表的共同起始后缀
思路也是双指针,这次怎么控制指针呢?
首先我们要注意,两个链表长度可能不一样,但是相交的部分一定是一样的,我们怎么判断相交?答案是两个指针需要指向同一结点,思路就来了:让长链表的那个指针先走一部分,这一部分就是两个链表的差值,再让长短链表的指针同时向后走,啥时候两指针指向同一结点了,就表明找到了
步骤如下:
step 1:先分别求出两个链表的长度,进而得出差值gap
step 2:长链表指针后移gap个结点
step 3:两个链表指针同时后移,找到了返回结点,找不到返回NULL
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
typedef struct ListNode ListNode;
int lenA=0,lenB=0;
ListNode *p,*q;
p = headA;
q = headB;
while(p){
lenA++;
p=p->next;
}
while(q){
lenB++;
q=q->next;
} //step 1
if(lenA > lenB){
p = headA;
q = headB;
}
else{
p = headB;
q = headA;
}
int gap = abs(lenA-lenB);
for(int i=0;i<gap;i++) //step 2
p = p->next;
while(p){ //step 3
if(p==q) return p;
else{
p = p->next;
q = q->next;
}
}
return NULL;
}
8、环形链表II:你搁这考我数学呢?有环这个地方408学过,怎么找入口还真不知道,看完推导头都大了,考试不可能有时间给你推的。
直接记结论:
1、如何判断有环?(可能是面试考点哦)
可以使用快慢指针法,分别定义 fast 和 slow 指针,从头结点出发,fast指针每次移动两个节点,slow指针每次移动一个节点,如果 fast 和 slow指针在途中相遇 ,说明这个链表有环。
2、怎么找环的入口?
从头结点出发一个指针,从相遇节点 也出发一个指针,这两个指针每次只走一个节点, 那么当这两个指针相遇的时候就是 环形入口的节点。
考试背结论就行了,推导去看原文。
ListNode *detectCycle(ListNode *head) {
ListNode *fast = head, *slow = head;
while (fast && fast->next) {
// 这里判断两个指针是否相等,所以移位操作放在前面
slow = slow->next;
fast = fast->next->next;
if (slow == fast) { // 相交,开始找环形入口:分别从头部和从交点出发,找到相遇的点就是环形入口
ListNode *f = fast, *h = head;
while (f != h) f = f->next, h = h->next;
return h;
}
}
return NULL;
}
9、总结:虚拟头结点yyds,链表非常考研逻辑性,删除翻转之类的链表操作一不留神就会做错,而且双指针法同样适用于链表,但一定要注意指针的各种条件。
机试的话这部分我感觉是必考的,可能还会结合一些其他类型的题目,太高深的数据结构不适合考机试,手撕那些什么图算法可太要命了