目录
1.返回倒数第K个节点【链接】
代码实现
2.链表的回文结构【链接】
代码实现
3.相交链表【链接】
代码实现
4.判断链表中是否有环【链接】
代码实现
常见问题解析
5.寻找环的入口点【链接】
代码实现1
代码实现2
6.随机链表的复制【链接】
代码实现
7.顺序表和链表的区别
1.返回倒数第K个节点【链接】
题目描述:
实现一种算法,找出单向链表中倒数第 k 个节点。返回该节点的值。
思路:快指针先走k步,然后快指针和慢指针同时走,直到快指针走到NULL,此时慢指针的节点即为所求。
解析:
代码实现
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
int kthToLast(struct ListNode* head, int k){
struct ListNode*fast=head,*slow=head;
//快指针先走k步
while(k--)
{
fast=fast->next;
}
//快慢指针一起走
while(fast)
{
slow=slow->next;
fast=fast->next;
}
return slow->val;
}
2.链表的回文结构【链接】
题目描述:
对于一个链表,请设计一个时间复杂度为O(n),额外空间复杂度为O(1)的算法,判断其是否为回文结构。
给定一个链表的头指针A,请返回一个bool值,代表其是否为回文结构。保证链表长度小于等于900。
思路:首先找到中间节点,将中间节点后半部分倒置,再分别从头结点和尾节点向中间遍历,看对应值是否相等。
这里需要用到曾经写过的找链表的中间节点函数和反转链表函数可参照【单链表的应用】
代码实现
/*
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
};*/
class PalindromeList {
public:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode* middleNode(struct ListNode* head) {
//创建快慢指针
struct ListNode* slow = head;
struct ListNode* fast = head;//如果有两个中间节点,则返回第二个中间节点
while (fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
}
//此时slow刚好指向中间节点
return slow;
}
struct ListNode* reverseList(struct ListNode* head) {
// 重新创建一个链表,将之前的链表进行头插即可
struct ListNode* rphead = nullptr;
// 进行指针变换
struct ListNode* cur = head;
while (cur != nullptr) {
// 用于保存下一个节点地址
struct ListNode* newnode = cur->next;
// 头插
cur->next = rphead;
rphead = cur;
cur = newnode;
}
return rphead; //返回新链表的头rhead
}
bool chkPalindrome(ListNode* A) {
struct ListNode* mid = middleNode(A);
struct ListNode* rmid = reverseList(mid);
while (rmid && A) {
if (rmid->val != A->val) {
return false;
}
rmid = rmid->next;
A = A->next;
}
return true;
}
};
3.相交链表【链接】
题目描述:
给你两个单链表的头节点
headA
和headB
,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回null
。
示例1相交节点不是1,而是8,注意这里不能比值,因为两个不同节点的值可能一样,要比较节点的地址。
思路1:暴力求解,对于链表A中的每个节点,我们都遍历一次链表B看B中是否有相同节点,第一个找到的就是第一个公共节点,假设A链表有M个节点,B链表有N个节点,时间复杂度太高了为O(M*N)即O(N^2)。
思路2:先判断两个链表是否相交,可以通过判断两个链表最后一个节点地址是否相同,如果尾节点相同,说明两个链表一定相交,如果不相等,直接返回空指针。计算出两个链表的长度,让长链表先走相差的长度,然后让两个链表同时走,直到遇到相同的节点就是第一个公共节点,返回指向这个节点的指针。
解析:
代码实现
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
struct ListNode *curA=headA,*curB=headB;
int lenA=0,lenB=0;
while(curA->next)
{
curA=curA->next;
++lenA;
}
while(curB->next)
{
curB=curB->next;
++lenB; //此时B链表的长度少算一个
}
//尾节点不相等就是不相交
if(curA!=curB)
{
return NULL;
}
lenA+=1;
lenB+=1;
//长的先走差距步,再同时走,第一个相等的就是交点
//假设法
int gap=abs(lenA-lenB);//求绝对值
struct ListNode *longList=headA,*shortList=headB;//longList指向长链表,shprtList指向短链表
//如果B比A长,再修改一下指针的指向,让longList指向长链表B,shprtList指向短链表A
if(lenB>lenA)
{
longList=headB;
shortList=headA;
}
while(gap--)//走差距步
{
longList=longList->next;
}
while(longList!=shortList)
{
longList=longList->next;
shortList=shortList->next;
}
return shortList;//返回哪一个都可以
}
4.判断链表中是否有环【链接】
题目描述:
给你一个链表的头节点
head
,判断链表中是否有环。如果链表中有某个节点,可以通过连续跟踪
next
指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数pos
来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos
不作为参数进行传递 。仅仅是为了标识链表的实际情况。如果链表中存在环 ,则返回
true
。 否则,返回false
。
思路:首先,让快慢指针fast、slow指向链表的头节点head, 让快指针fast一次向后移动两个节点,慢指针一次向后移动一个节点, 判断fast和slow是否走到同一个节点上(数学上追击相遇问题),如果走到同一个节点上,就返回true。
代码实现
bool hasCycle(struct ListNode *head) {
struct ListNode*slow=head,*fast=head;
while(fast&&fast->next)//当一个链表中没有环时,fast一定会移动到链表的结尾
{
slow=slow->next;
fast=fast->next->next;
if(slow==fast)
{
return true;
}
}
return false;
}
常见问题解析
1.快慢指针为什么会相遇,它们有没有可能错过,永远追不上?
不会错过。
假设slow进环时,fast 和 slow 的距离是N,追击过程中二者距离变化如下:
N->N-1->N-2->……->3->2->1->0
每追击一次,二者之间距离缩小1,距离为0即相遇。
2.慢指针slow一次移动一个节点,快指针一次移动多个节点(3,4,5……n)可行吗?
可行。
用快指针一次移动3个节点举例
假设slow进环时,fast 和 slow 的距离是N,追击过程中二者距离变化如下:
情况1:N->N-2->N-4->……->4->2->0(N为偶数)
情况2:N->N-2->N-4->……->5->3->1->-1(N为奇数)N为-1表示错过了,距离变成C-1(C为环的长度)
- 如果C-1是偶数,新一轮追击就追上了
- 如果C-1是奇数,那么就永远追不上
如果N是奇数并且C是偶数,那么就永远追不上,那这种条件存在吗?
假设slow进环前走的距离为L,设slow进环时,fast已经在环中转了x圈
slow走的距离:L
fast走的距离:L+x*C+C-N
由fast走的距离是slow的三倍产生的数学等式:3L=L+x*C+C-N
化简得:2L=(x+1)*C-N
我们发现偶数=(x+1)*偶数-奇数不成立,反证出当N是奇数时,C也一定是奇数。
总结一下:一定能追上,N是偶数,第一轮就追上了;N是奇数,第一轮追不上,C-1是偶数,第二轮就追上了。
5.寻找环的入口点【链接】
题目描述:
给定一个链表的头节点
head
,返回链表开始入环的第一个节点。 如果链表无环,则返回null
。如果链表中有某个节点,可以通过连续跟踪
next
指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数pos
来表示链表尾连接到链表中的位置(索引从 0 开始)。如果pos
是-1
,则在该链表中没有环。注意:pos
不作为参数进行传递,仅仅是为了标识链表的实际情况。不允许修改 链表。
思路1:用两个指针head、meet分别指向链表的头节点和快慢指针相遇的节点,同时移动两个指针,当两个指针指向同一个节点时,该节点就是环的入口点 。
解析:
假设环的长度为C,到达相遇点时,慢指针slow走过的距离为L+N(一圈之内肯定会被追上),快指针fast走过的距离为L+x*C+N (假设快指针走了x圈,N一定大于等于1),由fast走的距离是slow的2倍产生的数学等式:2*(L+N)=L+x*C+N 化简得:L=x*C-N
也就是说head到入口点的距离等于meet指针转x圈减去N的距离,head走到入口点,meet也刚好走到入口点,所以两个指针一起遍历,最终会同时到达入口点。
代码实现1
struct ListNode *detectCycle(struct ListNode *head) {
struct ListNode *slow=head,*fast=head;
while(fast&&fast->next)
{
slow=slow->next;
fast=fast->next->next;
if(slow==fast)
{
struct ListNode *meet=slow;
while(meet!=head)
{
meet=meet->next;
head=head->next;
}
return meet;
}
}
return NULL;
}
思路2:让快慢指针相遇节点与下一个节点断开,然后将问题转化为两个链表的相交,环的入口点其实就是两个链表的相交点。
解析:
代码实现2
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
struct ListNode *curA=headA,*curB=headB;
int lenA=0,lenB=0;
while(curA->next)
{
curA=curA->next;
++lenA;
}
while(curB->next)
{
curB=curB->next;
++lenB; //此时B链表的长度少算一个
}
//尾节点不相等就是不相交
if(curA!=curB)
{
return NULL;
}
lenA+=1;
lenB+=1;
//长的先走差距步,再同时走,第一个相等的就是交点
//假设法
int gap=abs(lenA-lenB);//求绝对值
struct ListNode *longList=headA,*shortList=headB;//longList指向长链表,shprtList指向短链表
//如果B比A长,再修改一下指针的指向,让longList指向长链表B,shprtList指向短链表A
if(lenB>lenA)
{
longList=headB;
shortList=headA;
}
while(gap--)//走差距步
{
longList=longList->next;
}
while(longList!=shortList)
{
longList=longList->next;
shortList=shortList->next;
}
return shortList;//返回哪一个都可以
}
struct ListNode *detectCycle(struct ListNode *head) {
struct ListNode *slow=head,*fast=head;
while(fast&&fast->next)
{
slow=slow->next;
fast=fast->next->next;
if(slow==fast)
{
struct ListNode *meet=slow;
struct ListNode *newhead=meet->next;
meet->next=NULL;
return getIntersectionNode(head,newhead);
}
}
return NULL;
}
6.随机链表的复制【链接】
题目描述:
给你一个长度为
n
的链表,每个节点包含一个额外增加的随机指针random
,该指针可以指向链表中的任何节点或空节点。构造这个链表的 深拷贝。 深拷贝应该正好由
n
个 全新 节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的next
指针和random
指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点 。
深拷贝就是拷贝一个值和指针指向都跟当前链表一模一样的链表。
思路: 拷贝节点到原节点的后面,再寻找途径处理random指针,处理完后对复制的节点拿下来尾插成新链表,返回新链表的头。
解析:
代码实现
/**
* Definition for a Node.
* struct Node {
* int val;
* struct Node *next;
* struct Node *random;
* };
*/
//拷贝节点插入在原节点的后面
struct Node* copyRandomList(struct Node* head) {
struct Node*cur=head;
while(cur)
{
struct Node*copy=(struct Node*)malloc(sizeof(struct Node));
copy->val=cur->val;
copy->next=cur->next;//注意这里顺序不能颠倒
cur->next=copy;
cur=copy->next;
}
//控制random
cur=head;
while(cur)
{
struct Node*copy=cur->next;
if(cur->random==NULL)
{
copy->random=NULL;
}
else
{
copy->random=cur->random->next;
}
cur=copy->next;
}
//把拷贝节点取下来尾插成为新链表,然后恢复原链表
struct ListNode*copyhead=NULL,*copytail=NULL;
cur=head;
while(cur)
{
struct ListNode*copy=cur->next;
struct ListNode*next=copy->next;
if(copytail==NULL)
{
copyhead=copytail=copy;
}
else
{
copytail->next=copy;
copytail=copytail->next;
}
cur->next=next;//恢复原链表,有没有这句代码都行!
cur=next;
}
return copyhead;
}
7.顺序表和链表的区别
不同点 | 顺序表 | 链表 |
存储空间上 | 底层存储空间连续 | 不连续 |
任意位置插入删除元素 | 需要搬运大量的元素,效率低,时间复杂度为O(N) | 不需要搬运大量的元素,只需要修改指针指向,效率高,时间复杂度为O(1) |
随机访问 | 数组中存储的元素可以直接通过下标访问,不需要遍历数组,时间复杂度最低可以达到 O(1) | 从头指针开始往后找,直至找到目标元素,访问目标元素的时间复杂度为 O(n) |
扩容 | 容量不足,则需要进行扩容(原地扩容和异地扩容),扩容有消耗,还可能存在空间浪费的问题 | 由于链表独特的组成结构,按需申请,不需要考虑容量是否足够,即不需要考虑扩容 |
应用场景 | 大多用在元素高效存储或随机访问频繁的场景 | 大多用在任意位置插入和删除操作频繁的场景 |
缓存利用率 | 高 | 低 |
为什么CPU不直接访问内存?
当程序或数据存储在内存上时,CPU不能直接访问它们,因为内存的访问速度较慢。为了执行程序或访问数据,如果内存的数据比较小,通常就会加载到寄存器;如果数据比较大,它们首先需要被一级一级加载到缓存中,这样CPU才能快速访问它们。它们可以从缓存中快速读取,而不是从较慢的内存中读取。尽可能多的让CPU访问缓存,这大大减少了CPU直接访问内存的次数,从而大大提高了计算机的性能。
CPU是怎么读取缓存的?
缓存由快速且昂贵的存储器组成,其容量较小但速度较快。缓存中存储的数据是内存中即将被CPU访问的一小部分。当一个计算机程序需要访问内存中的数据时,它通常会首先查找缓存。如果有,则发生了缓存命中,CPU就直接从缓存中读取,这样可以大大提高数据的读取速度。如果没有,就会发生缓存未命中,程序需要从主存或其他更慢的存储设备中获取数据到缓存中。
如果我们要访问顺序表和链表里面的一部分数据例如1,2,3,4,5,6,7,怎么访问呢?
CPU不会直接访问内存,而是加载到缓存中去访问。
由于这些数据量大,这些数据不会被加载到寄存器中,会被加载到缓存中。
访问这段数据,CPU首先到缓存中查看第一个元素的地址在不在缓存里,如果在,发生缓存命中,直接访问;如果不在,缓存不命中,数据会先从内存加载到缓存,再去访问。
其实在加载的过程中,对于CPU来说,它是不会一个字节一个字节的加载的,因为这非常没有效率,一般来说都是要一块一块的加载的(局部性原理),它会认为当前位置的数据被访问了,与之相邻的内存区域也有很大概率被访问。一般加载多少字节到缓存中跟CPU的字长有关,CPU从内存中捞数据上来的最小数据单位是64Bytes也就是16个32位的整型。
顺序表中由于第一个数据不命中,会从首地址开始的一长段数据都加载到缓存中去,再访问第2个位置的数据,在缓存中,就发生缓存命中。如果数据特别长,第一个不命中,后面连续一长段都命中,再发生不命中,就再加载数据到缓存中去,后面一长段都命中(如下图所示),以此类推。这就叫做缓存利用率高。
但是在链表中,节点与节点之间在内存中的位置不一定连续,第一次访问时,发生缓存不命中,会把第一个节点以及后面一连串的地址都加载到缓存中去,有可能会访问到后面的节点,最坏的情况就是后面的节点一个都访问不到,这就叫做缓存利用率低。同时,可能会因为加载进来的没用的数据把缓存空间中其它有用的数据挤走而导致缓存污染。