剑指Offer(数据结构与算法面试题精讲)C++版——day8
- 题目一:链表中环的入口节点
- 题目二:两个链表的第1个重合节点
- 题目三:反转链表
- 附录:源码gitee仓库
题目一:链表中环的入口节点
这道题的有如下三个关键点:
(1)判断链表中是否存在环;
(2)找出环的长度;
(3)快慢指针遍历链表得到相遇节点,该相遇节点即为环入口。
首先对于环的存在性判断,可以使用快慢指针,一开始让front指针和end指针都指向第一个节点,接下来慢指针每次向前移动1个节点,快节点每次向前移动2个节点,这样快指针每次都会比慢指针多走1个节点,如果链表中存在环,那么存在一种情况,快指针比慢指针多走k*n,其中k表示一个正整数,n为环的长度。
接下来便是找出环的长度了,在第一次快慢指针相遇之后,让快慢指针按照之前的方式接着向前走,最后快慢指针会第二次相遇,此时便有快指针比慢指针多走的长度就是链表中环长。
最后,调整依次快慢指针的开始位置,快慢指针都设置成第一个节点,接着快指针向前走环长,然后快慢指针一起向前走,下一次相遇说明便找到了入口节点了。最终得到如下代码:
# include <iostream>
# include <algorithm>
using namespace std;
struct linkNode {
int data;
linkNode * next;
linkNode(int val):data(val),next(nullptr) {}
};
typedef linkNode * linkList;
linkList createLinkList(int arr[],int len) {//使用空头链表
linkList head=new linkNode(0),q=head;
for(int i=0; i<len; ++i) {
linkNode * p=new linkNode(arr[i]);
q->next=p;
q=p;
}
q->next=nullptr;
return head;
}
void mockCircle(linkList head) {
linkList p=head;
linkNode * q=nullptr,*last=nullptr;
while(p!=nullptr) {
if(p->data==3) {
q=p;
}
if(p->next==nullptr) {
last=p;
}
p=p->next;
}
last->next=q;
}
linkNode* findCircleEnter(linkList head) {
linkNode *front=head->next,*end=head->next;
int count=1;
while(front!=nullptr&&end!=nullptr&&front!=end) {
front=front->next;
if(end->next) {
end=end->next->next;
} else {
end!=nullptr;
}
}
if(end==nullptr) {
return nullptr;
} else {
front=front->next;
end=end->next->next;
while(front!=end) {
front=front->next;
end=end->next->next;
count++;
}
cout<<"输出环长:"<<count<<endl;
front=head->next;
end=head->next;
while(count--) {
end=end->next;
}
while(front!=end) {
front=front->next;
end=end->next;
}
return end;
}
}
int main() {
int arr[]= {1,2,3,4,5,6};
linkList head= createLinkList(arr,6);
mockCircle(head);
cout<<"输出入口节点的值:"<<findCircleEnter(head)->data<<endl;
return 0;
}
补充说明,实际代码应该没有这里长,通常算法只需要写findCircleEnter
函数,但是为了方便去模拟这个查找过程,这里根据图例构造了带环模拟链表(带有空头链表)。这里的时间复杂度也比较好,站在front的角度,一开始为了得到环的长度,只需要遍历一次链表,然后找入口节点,总和来看最多遍历两次链表,因此总的时间复杂度为O(n)。
题目二:两个链表的第1个重合节点
依稀记得这是考研408中的一道真题,相较于前面的题目而言,这道题的难度稍显简单。这道题的解题关键在于统一起点,然后让两个链表一起向后遍历。具体的操作步骤是,首先分别对两个链表的长度进行一次统计,这样便能清晰知晓两个链表长度的差异。接着,使用两个指针,让它们各自指向两个链表的第一个节点,此时,根据之前统计得到的链表长度,让长度较长的链表的指针先走gap个节点。例如,若长链表的长度为m,短链表的长度为n,那么gap的值就是两者的差值,即n-m。通过这样的方式,使得两个链表的指针处于相对统一的起始位置,以便后续的遍历操作。基于上述思路,于是便可以得到如下的代码来实现相应的功能。
# include <iostream>
# include <algorithm>
using namespace std;
struct linkNode {
int data;
linkNode * next;
linkNode(int val):data(val),next(nullptr) {}
};
typedef linkNode * linkList;
linkList createLinkList(int arr[],int len) {//使用空头链表
linkList head=new linkNode(0),q=head;
for(int i=0; i<len; ++i) {
linkNode * p=new linkNode(arr[i]);
q->next=p;
q=p;
}
q->next=nullptr;
return head;
}
void mockCircle(linkList head1,linkList head2) {
linkList p1=head1->next,p2=head2->next;
linkNode * q=nullptr;
while(p1!=nullptr) {
if(p1->data==4) {
q=p1;
}
p1=p1->next;
}
while(p2->next!=nullptr) {
p2=p2->next;
}
p2->next=q;
}
linkNode* findCommon(linkList head1,linkList head2) {
int count1=0,count2=0,gap=0;
linkNode * p1=head1, *p2=head2,*p=head1->next,*q=head2->next;
while(p1->next) {//统计head1链表长度
p1=p1->next;
count1++;
}
while(p2->next) {//统计head2链表长度
p2=p2->next;
count2++;
}
while(count1>count2) {//拉齐链表起点
p=p->next;
count1--;
}
while(count2>count1) {
q=q->next;
count2--;
}
while(p!=q){
p=p->next;
q=q->next;
}
return q;
}
int main() {
int arr1[]= {1,2,3,4,5,6};
int arr2[]= {7,8};
linkList head1= createLinkList(arr1,6);
linkList head2= createLinkList(arr2,2);
mockCircle(head1,head2);
cout<<"输出公共节点对应的值:"<<findCommon(head1,head2)->data<<endl;
return 0;
}
同样的,实际编码的时候只需要写findCommon
即可。
题目三:反转链表
在数据结构与算法的学习和考察中,链表相关的题目一直是重点内容,而本题基本上算得上是链表系列的必考题。因为链表反转操作在实际的编程场景,如操作系统底层数据处理、图形渲染中的节点顺序调整等方面经常会被使用到。对于这道题,其解题的一个非常关键的思路是,将指针pre
初始化为null
。这样做的目的在于为后续链表节点指针方向的改变提供一个起始参照。在每次循环过程中,巧妙地改变指针的指向,从而逐步实现链表的反转。每一次指针方向的调整,都是对链表原有结构的一次重塑,通过这种循环操作,最终能够将整个链表的顺序反转过来。最终得到的代码如下:
# include <iostream>
# include <algorithm>
using namespace std;
struct linkNode {
int data;
linkNode * next;
linkNode(int val):data(val),next(nullptr) {}
};
typedef linkNode * linkList;
linkList createLinkList(int arr[],int len) {//使用空头链表
linkList head=new linkNode(0),q=head;
for(int i=0; i<len; ++i) {
linkNode * p=new linkNode(arr[i]);
q->next=p;
q=p;
}
q->next=nullptr;
return head;
}
void printLinkList(linkList head) {
linkList p=head->next;
while(p!=nullptr) {
cout<<p->data<<" ";
p=p->next;
}
}
void reverseList(linkList head) {
linkNode * pre=nullptr,*cur=head->next,*next=nullptr,*last=nullptr;
while(cur) {
if(!cur->next) {//因为存在空头,所以在真实元素反转后,需要让头指向最后一个节点
last=cur;
}
next=cur->next;
cur->next=pre;
pre=cur;
cur=next;
}
head->next=last;
}
int main() {
int arr1[]= {1,2,3,4,5};
linkList head= createLinkList(arr1,5);
reverseList(head);
cout<<"输出反转之后的链表:";
printLinkList(head);
return 0;
}
附录:源码gitee仓库
考虑到有些算法需要模拟数据,如果全部放进来代码显得过长,不利于聚焦在算法的核心思路上。于是把所有的代码整理到了开源仓库上,如果想要看详细模拟数据,可在开源仓库自取:【凌空暗羽/剑指offer-C++版手写代码】。
我是【Jerry说前后端】,本系列精心挑选的算法题目全部基于经典的《剑指 Offer(数据结构与算法面试题精讲)》。在如今竞争激烈的技术求职环境下,算法能力已成为前端开发岗位笔试考核的关键要点。通过深入钻研这一系列算法题,大家能够系统地积累算法知识和解题经验。每一道题目的分析与解答过程,都像是一把钥匙,为大家打开一扇通往高效编程思维的大门,帮助大家逐步提升自己在数据结构运用、算法设计与优化等方面的能力。
无论是即将踏入职场的应届毕业生,还是想要进一步提升自己技术水平的在职开发者,掌握扎实的算法知识都是提升竞争力的有力武器。希望大家能跟随我的步伐,在这个系列中不断学习、不断进步,为即将到来的前端笔试做好充分准备,顺利拿下心仪的工作机会!快来订阅吧,让我们一起开启这段算法学习之旅!