【初阶数据结构】3.单链表


3.单链表

3.1 概念与结构

概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。

淡季时车次的车厢会相应减少,旺季时车次的车厢会额外增加几节。只需要将火车里的某节车厢去掉/加上,不会影响其他车厢,每节车厢都是独立存在的。

链表就像车厢一样。

在这里插入图片描述


3.1.1 结点

与顺序表不同的是,链表里的每节"车厢"都是独立申请下来的空间,我们称之为“结点

结点的组成主要有两个部分:当前结点要保存的数据和保存下一个结点的地址(指针变量)。

图中指针变量 plist保存的是第一个结点的地址,我们称plist此时“指向”第一个结点,如果我们希望plist“指向”第二个结点时,只需要修改plist保存的内容为0x0012FFA0

链表中每个结点都是独立申请的(即需要插入数据时才去申请一块结点的空间),我们需要通过指针变量来保存下一个结点位置才能从当前结点找到下一个结点。


3.1.2 链表的性质

  1. 链式机构在逻辑上是连续的,在物理结构上不一定连续
  2. 结点一般是从堆上申请的
  3. 从堆上申请来的空间,是按照一定策略分配出来的,每次申请的空间可能连续,可能不连续

结合前面学到的结构体知识,我们可以给出每个结点对应的结构体代码:

struct ListNode
{
    int data; //结点数据
    struct ListNode* next; //指针变量用保存下一个结点的地址
};

当我们想要保存一个整型数据时,实际是向操作系统申请了一块内存,这个内存不仅要保存整型数据,也需要保存下一个结点的地址(当下一个结点为空时保存的地址为空)。

当我们想要从第一个结点走到最后一个结点时,只需要在当前结点拿上下一个结点的地址就可以了。


3.1.3 链表的打印

给定的链表结构中,如何实现结点从头到尾的打印?

!在这里插入图片描述


3.2 实现单链表

SList.h

#pragma once
#include <stdio.h>
#include <stdlib.h>

//定义链表(结点)的结构

typedef int SLTDataType;

typedef struct SListNode {
	SLTDataType data;
	struct SListNode* next;
}SLTNode;

//链表的头
void SLTPrint(SLTNode* phead);

//增加数据
//尾插
void SLTPushBack(SLTNode* phead, SLTDataType x);
//头插
void SLTPushFront(SLTNode** pphaed, SLTDataType x);

//删除
//尾删
void SLTPopBack(SLTNode** pphead);
//头删
void SLTPopFront(SLTNode** pphead);

//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x);

//在指定位置之前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);

//在指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x);

//删除pos结点
void SLTErase(SLTNode** pphead, SLTNode* pos);

//删除pos之后的结点
void SLTEraseAfter(SLTNode* pos);

//销毁链表
void SListDestroy(SLTNode** pphead);

SList.c

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include"SList.h"
#include <assert.h>

//打印链表
void SLTPrint(SLTNode* phead) {
	SLTNode* pcur = phead;
	while (pcur) {
		printf("%d->", pcur->data);
		pcur = pcur->next;
	}
	printf("NULL\n");
}

//申请新节点
SLTNode* SLTBuyNode(SLTDataType x) {
	SLTNode* node = (SLTNode*)malloc(sizeof(SLTNode));
	if (node == NULL) {
		perror("malloc fail !");
		exit(1);
	}
	node->data = x;
	node->next = NULL;

	return node;
}

//增加数据
//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x) {
	assert(pphead);
	//申请新节点
	SLTNode* newnode = SLTBuyNode(x);
	if (*pphead == NULL) {
		*pphead = newnode;
	}
	else {
	//尾结点和新结点连接
	//找尾结点
		SLTNode* pcur = *pphead;
		while (pcur->next) {
			pcur = pcur->next;
		}
		//pcur和newnode连接
		pcur->next = newnode;
	}	
}

//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x) {
	assert(pphead);
	SLTNode* newnode = SLTBuyNode(x);
	newnode->next = *pphead;
	*pphead = newnode;
}

//删除
//尾删
void SLTPopBack(SLTNode** pphead) {
	//链表为空,不能删除
	assert(pphead && *pphead);
	//处理只有一个节点的情况:要删除的就是头节点
	if ((*pphead)->next == NULL) {//->的优先级比*高,所以要加个()
		free(*pphead);
		*pphead = NULL;
	}
	else {
		//找尾结点 prev(尾结点的前一个结点) ptail(尾结点)
		SLTNode* ptail = *pphead;
		SLTNode* prev = NULL;
		while (ptail->next) {
			prev = ptail;
			ptail = ptail->next;
		}
		prev->next = NULL;
		free(ptail);
		ptail = NULL;
	}
}

//头删
void SLTPopFront(SLTNode** pphead) {
	assert(pphead && *pphead);
	SLTNode* next = (*pphead)->next;//用一个指针存放第二个节点
	free(*pphead);
	*pphead = next;//头指向存放的地方
}

//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x) {
	assert(phead);
	SLTNode* pcur = phead;
	while (pcur) {
		if (pcur->data == x) {//当前这个结点存的是不是x
			return pcur;
		}
		pcur = pcur->next;
	}
	//没有找到
	return NULL;
}

//在指定位置之前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x) {
	assert(pphead);
	assert(pos);

	if (pos == *pphead) {
		SLTPushFront(pphead, x);//调用头插
	}
	else {
		SLTNode* newnode = SLTBuyNode(x);
		//找prev:pos的前一个节点
		SLTNode* prev = *pphead;
		while (prev->next != pos) {
			prev = prev->next;
		}
		newnode->next = pos;
		prev->next = newnode;
	}
}

//在指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x) {
	assert(pos);
	SLTNode* newnode = SLTBuyNode(x);
	newnode->next = pos->next;
	pos->next = newnode;

}

//删除pos结点
void SLTErase(SLTNode** pphead, SLTNode* pos) {
	assert(pphead && *pphead);
	assert(pos);
	if (pos == *pphead) {
		SLTPopFront(pphead);
	}
	else {
		SLTNode* prev = *pphead;
		while (prev->next != pos) {
			prev = prev->next;
		}
		prev->next = pos->next;
		free(pos);
		pos = NULL;
	}
}

//删除pos之后的结点
void SLTEraseAfter(SLTNode* pos) {
	assert(pos && pos->next);
	SLTNode* del = pos->next;
	pos->next = pos->next->next;
	free(del);
	del = NULL;
}

//销毁链表
void SListDestroy(SLTNode** pphead) {
	assert(pphead && *pphead);
	SLTNode* pcur = *pphead;
	while (pcur) {
		SLTNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	*pphead = NULL;
}

test.c

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include"SList.h"

//创建一个链表,并打印链表

void createSList() {
	//链表是由一个一个的结点组成的
	SLTNode* node1 = (SLTNode*)malloc(sizeof(SLTNode));
	node1->data = 1;

	SLTNode* node2 = (SLTNode*)malloc(sizeof(SLTNode));
	node2->data = 2;

	SLTNode* node3 = (SLTNode*)malloc(sizeof(SLTNode));
	node3->data = 3;

	SLTNode* node4 = (SLTNode*)malloc(sizeof(SLTNode));
	node4->data = 4;

	node1->next = node2;
	node2->next = node3;
	node3->next = node4;
	node4->next = NULL;

	//把第一个结点的地址作为参数传递过去
	SLTNode* plist = node1;
	SLTPrint(node1);

}

void SListTest01() {
	SLTNode* plist = NULL;
	/*SLTPushBack(plist, 1);
	SLTPrint(plist);
	SLTPushBack(plist, 2);
	SLTPushBack(plist, 3);
	SLTPrint(plist);*/
	SLTPushFront(&plist, 1);
	SLTPushFront(&plist, 2);
	SLTPushFront(&plist, 3);
	SLTPushFront(&plist, 4);
	SLTPrint(plist);//4->3->2->1->NULL

	//SLTPopBack(&plist);
	//SLTPrint(plist);//4->3->2->NULL
	//SLTPopBack(&plist);
	//SLTPrint(plist);
	//SLTPopBack(&plist);
	//SLTPrint(plist);
	//SLTPopBack(&plist);
	//SLTPrint(plist);
	//SLTPopBack(&plist);
	//SLTPrint(plist);

	//SLTPopFront(&plist);
	//SLTPrint(plist); //3->2->1->NULL
	//SLTPopFront(&plist);
	//SLTPrint(plist);
	//SLTPopFront(&plist);
	//SLTPrint(plist);
	//SLTPopFront(&plist);
	//SLTPrint(plist);

	SLTNode* find = SLTFind(plist, 2);
	/*if (find == NULL) {
		printf("未找到!\n");
	}
	else {
		printf("找到了!\n");
	}*/
	//SLTInsert(&plist, find, 11);//11->4->3->2->1->NULL
	//SLTInsertAfter(find, 11);//4->3->2->1->11->NULL
	//SLTErase(&plist, find);
	//SLTEraseAfter(find);

	SListDestroy(&plist);//销毁
	SLTPrint(plist);
}

int main() {
	//createSList();
	SListTest01();
	return 0;
}


3.3 链表的分类

链表的结构非常多样,以下情况组合起来就有8种(2 x 2 x 2)链表结构:

在这里插入图片描述

链表说明:

在这里插入图片描述

虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构:单链表和双向带头循环链表
  1. 无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
  2. 带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了,后面我们代码实现了就知道了。

3.4 单链表算法题

3.4.1 移除链表元素

点击链接做题

在这里插入图片描述

思路1:遍历链表,在原链表执行删除指定节点的操作。

思路2:创建新链表,将原链表中值不为val的结点尾插到新链表。

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
typedef struct ListNode ListNode;
struct ListNode* removeElements(struct ListNode* head, int val) {
    //创建新链表
    ListNode* newHead,*newTail;
    newHead = newTail = NULL;
    //遍历原链表
    ListNode* pcur = head;
    while(pcur){
        //找值不为val的结点,往新链表中进行尾插
        if(pcur->val != val){
            //尾插
            //链表为空
            if(newHead == NULL){
                newHead = newTail = pcur;
            }
            //链表不为空
            else{
                newTail->next = pcur;
                newTail = newTail->next;
            }
        }
        pcur = pcur->next;
    }
    if(newTail){
        newTail->next = NULL;
    }
    return newHead;
    
}

3.4.2 反转链表

点击链接做题

在这里插入图片描述

思路1:把第一个链表头插到一个新链表

思路2:创建3个指针,n1,n2,n3,在原链表上就可以修改指针的指向(不需要创建新的链表)

思路2代码:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
typedef struct ListNode ListNode;
struct ListNode* reverseList(struct ListNode* head) {
    //处理空链表
    if(head == NULL){
        return head;
    }
    //创建3个指针
    ListNode* n1, *n2, *n3;
    n1 = NULL;n2 = head;n3 = n2->next;
    while(n2){
        n2->next = n1;
        n1 = n2;
        n2 = n3;
        if(n3){
            n3 = n3->next;
        }
    }
    //此时的n1就是链表反转后新的头节点
    return n1;
}

3.4.3 链表的中间结点

点击链接做题

在这里插入图片描述

思路1:

  1. 第一次循环:求链表总长度,计算中间结点的位置
  2. 第二次循环:根据中间节点的位置走到中间节点

思路2:快慢指针

先定义快慢指针fast,slow,都位于头节点

慢指针每次往后走1步,快指针每次往后走2步。

奇数个结点时:当fast->next == NULL就没有必要往后走了,此时slow也刚好指向中间节点

偶数个结点时:当fast走到NULL的时候,此时slow也刚好指向中间节点

为什么快慢指针可以找到中间节点?

慢指针每次走一步,快指针每次走两步,当快指针走到链表的尾结点时,假设链表的长度为n

快指针走的路程是慢指针的两倍,即2*慢=快

此时慢指针走的路程就是n/2

思路代码:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
typedef struct ListNode ListNode;
struct ListNode* middleNode(struct ListNode* head) {
    ListNode* slow = head, *fast = head;
    //慢指针每次走1步,快指针每次走2步
    while(fast && fast->next){//不能写while(fast->next && fast)
        slow = slow->next;
        fast = fast->next->next;
    }
    //此时slow指向的结点刚好就是中间结点
    return slow;
}

3.4.4 合并两个有序链表

点击链接做题

在这里插入图片描述

思路:创建新链表,遍历原链表,比较大小,谁小就尾插到新链表

思路代码:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
typedef struct ListNode ListNode;
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2) {
    //处理链表为空的情况
    if(list1 == NULL){
        return list2;
    }
    if(list2 == NULL){
        return list1;
    }
    //创建新的链表
    ListNode* newHead = NULL,*newTail = NULL;
    //创建两个指针分别指向两个链表的头节点
    ListNode* l1 = list1;
    ListNode* l2 = list2;

    while(l1 && l2){
        if(l1->val < l2->val){
            //l1尾插到新链表中
            if(newHead == NULL){
                newHead = newTail = l1;
            }
            else{
                newTail->next = l1;
                newTail = newTail->next;
            }
            l1 = l1->next;
        }
        else{
            //l2尾插到新链表中
            if(newHead == NULL){
                newHead = newTail = l2;
            }
            else{
                newTail->next = l2;
                newTail = newTail->next;
            }
            l2 = l2->next;
        }
    }
    //跳出循环只有两种情况:要么l1为空,要么l2为空
    if(l1){
        newTail->next = l1;
    }
    if(l2){
        newTail->next = l2;
    }
    return newHead;
}

因为链表分为空和非空两种情况导致代码有点冗余,我们可以通过封装函数或者直接创建一个空链表来解决。

采用后者优化后:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
typedef struct ListNode ListNode;
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2) {
    //处理链表为空的情况
    if(list1 == NULL){
        return list2;
    }
    if(list2 == NULL){
        return list1;
    }
    //创建新的链表
    ListNode* newHead,*newTail;
    newHead = newTail = (ListNode*)malloc(sizeof(ListNode));
    
    //创建两个指针分别指向两个链表的头节点
    ListNode* l1 = list1;
    ListNode* l2 = list2;

    while(l1 && l2){
        if(l1->val < l2->val){
            //l1尾插到新链表中
            newTail->next = l1;
            newTail = newTail->next;
            l1 = l1->next;
        }
        else{
            //l2尾插到新链表中
            newTail->next = l2;
            newTail = newTail->next;
            l2 = l2->next;
        }
    }
    //跳出循环只有两种情况:要么l1为空,要么l2为空
    if(l1){
        newTail->next = l1;
    }
    if(l2){
        newTail->next = l2;
    }
    ListNode* ret = newHead->next;
    free(newHead);
    newHead = NULL;
    return ret;
}

3.4.5 链表分割

点击链接做题

在这里插入图片描述

例如:

排序前:1->6->2->3->5

X:X = 3

排序后:1->2->6->3->5

思路:

排序前:1->6->2->3->5

小链表:malloc->1->2

大链表:malloc->6->3->5

最后:1->2->6->3->5(把2的next指向6)

思路代码:

/*
struct ListNode {
    int val;
    struct ListNode *next;
    ListNode(int x) : val(x), next(NULL) {}
};*/
class Partition {
public:
    ListNode* partition(ListNode* pHead, int x) {
        // write code here
        //创建两个非空链表
        ListNode* lessHead, *lessTail;
        lessHead = lessTail = (ListNode*)malloc(sizeof(ListNode));

        ListNode* greaterHead, *greaterTail;
        greaterHead = greaterTail = (ListNode*)malloc(sizeof(ListNode));

        //遍历链表,找小于x的和其他节点尾插到大小链表中
        ListNode* pcur = pHead;
        while(pcur){//等价于pcur!=NULL
            if(pcur->val < x){
                //尾插到小链表
                lessTail->next = pcur;
                lessTail = lessTail->next;
            }
            else{
                //尾插到大链表
                greaterTail->next = pcur;
                greaterTail = greaterTail->next;
            }
            pcur = pcur->next;
        }
        //大小链表首尾相连
        lessTail->next = greaterHead->next;
        ListNode* ret = lessHead->next;
        free(lessHead);
        free(greaterHead);
        lessHead = greaterHead = NULL;
        return ret;
    }
};

上面这个代码提示内存超限,因为有个问题:

例如:

排序前:5->1->3->6->2

小链表:malloc->1->2

大链表:malloc->5->3->6

结果:1->2->5->3->6->2->5->3->6

(因为这里是把2的next指向5,来让两个链表连起来。但是6的next我们没有改,在排序前6的next是2,所以这里会死循环)

我们只要将大链表的尾结点的next指针置为null就可以了

/*
struct ListNode {
    int val;
    struct ListNode *next;
    ListNode(int x) : val(x), next(NULL) {}
};*/
class Partition {
public:
    ListNode* partition(ListNode* pHead, int x) {
        // write code here
        //创建两个非空链表
        ListNode* lessHead, *lessTail;
        lessHead = lessTail = (ListNode*)malloc(sizeof(ListNode));

        ListNode* greaterHead, *greaterTail;
        greaterHead = greaterTail = (ListNode*)malloc(sizeof(ListNode));

        //遍历链表,找小于x的和其他节点尾插到大小链表中
        ListNode* pcur = pHead;
        while(pcur){//等价于pcur!=NULL
            if(pcur->val < x){
                //尾插到小链表
                lessTail->next = pcur;
                lessTail = lessTail->next;
            }
            else{
                //尾插到大链表
                greaterTail->next = pcur;
                greaterTail = greaterTail->next;
            }
            pcur = pcur->next;
        }
        //将大链表的尾结点的next指针置为null
        greaterTail->next = NULL;

        //大小链表首尾相连
        lessTail->next = greaterHead->next;
        ListNode* ret = lessHead->next;
        free(lessHead);
        free(greaterHead);
        lessHead = greaterHead = NULL;
        return ret;
    }
};

3.4.6 链表的回文结构

点击链接做题

在这里插入图片描述

思路1:创建新的数组,遍历原链表,遍历原链表,将链表节点中的值放入数组中,在数组中判断是否为回文结构。

例如:

排序前:1->2->2->1

设置数组来存储链表,设置数组头指针left和数组尾指针right

判断leftright指向的数是否相等,相等就left++;right--;,直到left > right

思路代码:

/*
struct ListNode {
    int val;
    struct ListNode *next;
    ListNode(int x) : val(x), next(NULL) {}
};*/
class PalindromeList {
public:
    bool chkPalindrome(ListNode* A) {
        // write code here
        int arr[900] = {0};
        int i = 0;
        ListNode* pcur = A;
        //遍历链表,将链表中每个节点中的数值储存在数组中
        while(pcur){
            arr[i++] = pcur->val;
            pcur = pcur->next;
        }
        //i即结点的个数
        //找中间节点,判断是否为回文数字
        int left = 0;//数组头指针
        int right = i - 1;//数组尾指针
        while(left < right){
            if(arr[left] != arr[right]){
                //不是回文结构
                return false;
            }
            left++;
            right--;
        }
        //是回文结构
        return true;
    }
};

思路2:反转链表

  1. 找链表的中间节点(快慢指针)
  2. 将中间节点之后的链表进行反转
  3. 从原链表的头和反转链表比较节点的值
/*
struct ListNode {
    int val;
    struct ListNode *next;
    ListNode(int x) : val(x), next(NULL) {}
};*/
class PalindromeList {
    public:
    ListNode* findMidNode(ListNode* phead){
        ListNode* slow = phead;
        ListNode* fast = phead;
        while(fast && fast->next){
            slow = slow->next;
            fast = fast->next->next;
        }
        return slow;
    }

    ListNode* reverseList(ListNode* phead){
        ListNode* n1, *n2, *n3;
        n1 = NULL; n2 = phead, n3 = n2->next;
        while(n2){
            n2->next = n1;
            n1 = n2;
            n2 = n3;
            if(n3){
                n3 = n3->next;
            }
        }
        return n1;
    }

    bool chkPalindrome(ListNode* A) {
        // write code here
        //1.找中间节点
        ListNode* mid = findMidNode(A);
        //2.根据中间节点反转后面链表
        ListNode* right = reverseList(mid);
        //3.从原链表的头和反转链表比较节点的值
        ListNode* left = A;

        while(right){
            if(left->val != right->val){
                return false;
            }
            left = left->next;
            right = right->next;
        }
        return true;
    }
};

3.4.7 相交链表

点击链接做题

在这里插入图片描述

思路:

  1. 如何判断链表是否相交
  2. 找相交链表的起始节点
  1. 遍历两个链表,若尾结点相同,则链表一定相交。
  2. 两个链表节点个数相同:往后遍历,找到相交的位置
  3. 两个链表节点个数不同:
    1. 找两个链表的节点数差值
    2. 让长链表先走差值步
    3. 两个链表开始遍历,比较是否为同一个节点

代码:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
typedef struct ListNode ListNode;
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
    ListNode* l1 = headA;
    ListNode* l2 = headB;
    int sizeA = 0, sizeB = 0;
    //计算链表长度,保存在sizeA,sizeB里面
    while(l1){
        sizeA++;
        l1 = l1->next;
    }
    while(l2){
        sizeB++;
        l2 = l2->next;
    }
    //计算链表差值(绝对值)
    int gap = abs(sizeA - sizeB);
    //让长链表先走gap步
    ListNode* longList = headA;
    ListNode* shortList = headB;
    if(sizeA < sizeB){//说明sizeB链表更长一些,否则就不交换
        longList = headB;
        shortList = headA;
    }
    while(gap--){
        longList = longList->next;
    }
    //此时longlist指针和shortlist指针在同一起跑线
    //链表相交,链表不相交
    while(longList && shortList){
        if(longList == shortList){
            //链表相交
            return longList;
        }
        //继续往后走
        longList = longList->next;
        shortList = shortList->next;
    }
    //链表不相交
    return NULL;
}

3.4.8 环形链表I

点击链接做题

在这里插入图片描述

思路:快慢指针

代码:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
typedef struct ListNode ListNode;
bool hasCycle(struct ListNode *head) {
    //快慢指针
    ListNode* slow = head;
    ListNode* fast = head;
    while(fast && fast->next){
        slow = slow->next;
        fast = fast->next->next;
        if(slow == fast){
            return true;
        }
    }
    //两个指针始终没有相遇
    return false;
}

思考1:为什么快指针每次走两步,慢指针走一步可以相遇,有没有可能遇不上,请推理证明!

在这里插入图片描述

slow一次走1步,fast一次走2步,fast先进环,假设slow也走完入环前的距离,准备进环,此时fastslow之间的距离为N,接下来的追逐过程中,每追击一次,他们之间的距离缩小1步

因此,在带环链表中慢指针走一步,快指针走两步最终一定会相遇。

思考2:快指针一次走3步,走4步,...n步行吗?

step1:

按照上面的分析,慢指针每次走一步,快指针每次走三步,此时快慢指针的最大距离为N,接下来的追逐过程中,每追击一次,他们之间的距离缩小2步追击过程中fastslow之间的距离变化:

在这里插入图片描述

分析:

  1. 如果N是偶数,第一轮就追上了

  2. 如果N是奇数,第一轮追不上,快追上,错过了,距离变成-1,即C-1,进入新的一轮追击

    a. C-1如果是偶数,那么下一轮就追上了

    b. C-1如果是奇数,那么就追不上,进入新的一轮追击

总结一下追不上的前提条件:N是奇数,C是偶数

step2:

在这里插入图片描述

假设:

环的周长为C,头结点到slow结点的长度为Lslow走一步,fast走三步,当slow指针入环后,slowfast指针在环中开始进行追逐,假设此时fast指针已经绕环x周。

在追逐过程中,快慢指针相遇时所走的路径长度:

fast: L+xC+C-N

slow:L

由于慢指针走一步,快指针要走三步,因此得出: 3 * 慢指针路程 = 快指针路程 ,即:

3L = L + xC + C − N (化简前)

2L = (x + 1)C − N (化简后)

对上述公式继续分析:由于偶数乘以任何数都为偶数,因此2L一定为偶数,则可推导出可能得情况:

  • 情况1:偶数 = 偶数 - 偶数

  • 情况2:偶数 = 奇数 - 奇数

由step1中(1)得出的结论,如果N是偶数,则第一圈快慢指针就相遇了。

由step1中(2)得出的结论,如果N是奇数,则fast指针和slow指针在第一轮的时候套圈了,开始进行下一轮的追逐;当N是奇数,要满足以上的公式,则 (x+1)C 必须也要为奇数,即C为奇数,满足(2)a中的结论,则快慢指针会相遇

因此, step1 中的 N是奇数,C是偶数不成立 ,既然不存在该情况,则快指针一次走3步最终一定也可以相遇。

快指针一次走4、5…步最终也会相遇,其证明方式同上。


提示:

虽然已经证明了快指针不论走多少步都可以满足在带环链表中相遇,但是在编写代码的时候会有额外的步骤引入,涉及到快慢指针的算法题中通常习惯使用慢指针走一步快指针走两步的方式。


3.4.9 环形链表II

点击链接做题

在这里插入图片描述

思路:快慢指针

快慢指针,即慢指针一次走一步,快指针一次走两步,两个指针从链表起始位置开始运行,如果链表带环则一定会在环中相遇,否则快指针率先走到链表的未尾

让一个指针从链表起始位置开始遍历链表,同时让一个指针从判环时相遇点的位置开始绕环运行,两个指针都是每次均走一步,最终肯定会在入口点的位置相遇。

代码:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
typedef struct ListNode ListNode;
struct ListNode *detectCycle(struct ListNode *head) {
    //找环的相遇点
    //从头结点和相遇点开始遍历,每次都走一步
    ListNode* slow = head;
    ListNode* fast = head;
    while(fast && fast->next){
        slow = slow->next;
        fast = fast->next->next;
        if(slow == fast){
            //相遇即链表一定带环
            ListNode* pcur = head;
            while(pcur != slow){
                pcur = pcur->next;
                slow = slow->next;
            }
            return pcur;
        }
    }
    //链表不带环
    return NULL;
}

3.4.10 随机链表的复制

点击链接做题

在这里插入图片描述

思路:

浅拷贝:拷贝值

深拷贝:拷贝空间

  1. 在原链表的基础上继续复制链表
  2. random指针
  3. 复制链表和原链表断开

代码:

/**
 * Definition for a Node.
 * struct Node {
 *     int val;
 *     struct Node *next;
 *     struct Node *random;
 * };
 */
typedef struct Node Node;
Node* buyNode(int x){
    Node* newnode = (Node*)malloc(sizeof(Node));
    newnode->val = x;
    newnode->next = newnode->random = NULL;
    return newnode;
}
void AddNode(Node* phead){
    Node* pcur = phead;
    while(pcur){
        Node* Next = pcur->next;
        //创建新结点,尾插到pcur
        Node* newnode = buyNode(pcur->val);//newnode是复制结点
        pcur->next = newnode;
        newnode->next = Next;

        pcur = Next;
    }
}

struct Node* copyRandomList(struct Node* head) {
    if(head == NULL){
        return NULL;
    }
	//1.原链表上复制结点
    AddNode(head);
    //2.置random
    Node* pcur = head;
    while(pcur){
        Node* copy = pcur->next;
        if(pcur->random != NULL){
            copy->random = pcur->random->next;
        }
        pcur = copy->next;
    }
    //3.断开链表
    //在copy的链表里面设置两个指针:newHead,newTail位于拷贝链表的头
    //pcur往后挪两个到newHead->next,newTail挪到pcur->next
    pcur = head;//让pcur先回到头节点
    Node* newHead, *newTail;
    newHead = newTail = pcur->next;
    while(pcur->next->next){//如果pcur->next->next为空就说明结束了
        pcur = pcur->next->next;
        newTail->next = pcur->next;//让newTail->next指向copy链表的后一个元素
        newTail = newTail->next;//把newTail移到copy链表的后一个元素的位置
    }
    return newHead;//返回新链表newHead到newTail
}
  • 22
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值