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

news2024/12/25 23:53:18

文章目录

  • 3.单链表
    • 3.1 概念与结构
      • 3.1.1 结点
      • 3.1.2 链表的性质
      • 3.1.3 链表的打印
    • 3.2 实现单链表
    • 3.3 链表的分类
    • 3.4 单链表算法题
      • 3.4.1 移除链表元素
      • 3.4.2 反转链表
      • 3.4.3 链表的中间结点
      • 3.4.4 合并两个有序链表
      • 3.4.5 链表分割
      • 3.4.6 链表的回文结构
      • 3.4.7 相交链表
      • 3.4.8 环形链表I
      • 3.4.9 环形链表II
      • 3.4.10 随机链表的复制


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
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1930916.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Mysql解忧杂货铺

欢迎来到一夜看尽长安花 博客&#xff0c;您的点赞和收藏是我持续发文的动力 对于文章中出现的任何错误请大家批评指出&#xff0c;一定及时修改。有任何想要讨论的问题可联系我&#xff1a;3329759426qq.com 。发布文章的风格因专栏而异&#xff0c;均自成体系&#xff0c;不足…

面向对象七大原则

学习目标 了解面向对象七大原则基本概念。 在之后实践应用中&#xff0c;要给予七大原则去设计程序。 为什么有七大原则 七大原则总体要实现的目标是&#xff1a; 高内聚、低耦合。 使程序模块的可重复性、移植性增强。 高内聚低耦合 从类角度来看&#xff0c;高内聚低…

LTSPICE仿真电路:(二十)TVS管简单仿真

1.目的 目的很简单&#xff0c;就是为了更加了解TVS管&#xff0c;以及更加能记住他的特性&#xff0c;加深印象&#xff0c;原本是只打算仿真TVS管&#xff0c;后面做着做着&#xff0c;搞二极管伏安特性曲线的时候&#xff0c;发现稳压二极管和TVS管比较接近&#xff0c;索性…

解决 Vscode不支持c++11的语法

问题&#xff1a; 解决方案&#xff1a; 1、按 CtrlShiftP 调出命令面板&#xff0c;输入 C/C: Edit Configurations (UI) 并选择它。这将打开 C/C 配置界面 2、打开 c_cpp_properties.json 文件 3、编辑 c_cpp_properties.json 4、保存 c_cpp_properties.json 文件。 关闭并…

【HarmonyOS】关于鸿蒙消息推送的心得体会 (一)

【HarmonyOS】关于鸿蒙消息推送的心得体会&#xff08;一&#xff09; 前言 这几天调研了鸿蒙消息推送的实现方式&#xff0c;形成了开发设计方案&#xff0c;颇有体会&#xff0c;与各位分享。 虽然没做之前觉得很简单的小功能&#xff0c;貌似只需要和华为服务器通信&…

Java-寻找二叉树两结点最近公共祖先

目录 题目描述&#xff1a; 注意事项&#xff1a; 示例&#xff1a; 示例 1&#xff1a; 示例 2&#xff1a; 示例 3&#xff1a; 解题思路&#xff1a; 解题代码&#xff1a; 题目描述&#xff1a; 给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。 百度百科…

刷题日志——模拟专题(python实现)

模拟往往不需要设计太多的算法&#xff0c;而是要按照题目的要求尽可能用代码表示出题目的旨意。 以下是蓝桥杯官网模拟专题的选题&#xff0c;大多数比较基础&#xff0c;但是十分适合新手入门&#xff1a; 一. 可链接在线OJ题 饮料换购图像模糊螺旋矩阵冰雹数回文日期长草最…

R语言实现神经网络ANN

# 常用激活函数 # 自定义Sigmoid函数 sigmod <- function(x){return(1/(1exp(-x))) } # 绘制Sigmoid曲线 x <- seq(-10,10,length.out 100) plot(x,sigmod(x),type l,col blue,lwd 2,xlab NA,ylab NA,main Sigmoid函数曲线)# 自定义Tanh函数 tanh <- function(…

MYSQL——数据库基础和操作

1.创建数据库 CREATE DATABASE [IF NOT EXISTS] db_name [create_specification [, create_specification] …] create_specification: [DEFAULT] CHARACTER SET charset_name [DEFAULT] COLLATE collation_name 说明&#xff1a; 1.大写的表示关键字 2. []是可选项 3. CHARACT…

读人工智能全传15意向立场

1. 物理立场 1.1. 可以解释一个实体行为 1.2. 在物理立场中&#xff0c;我们使用自然法则(物理、化学等)来预测系统的行为结果 1.3. 虽然物理立场在解释这种行为的时候非常有效&#xff0c;但无法应用于理解或者预测人类行为 1.3.1. …

RocketMQ源码学习笔记:消费者启动流程

这是本人学习的总结&#xff0c;主要学习资料如下 马士兵教育rocketMq官方文档 目录 1、前置知识1.1、pull和push型消费者1.2、消息CommitLog到ConsumeQueue1.3、自动创建的重试主题1.4、广播型消费和集群型消费中offset的存储位置 2、消费中的启动流程2.1、Preview2.2、校验&…

主流大数据调度工具DolphinScheduler之数据ETL流程

今天给大家分享主流大数据调度工具DolphinScheduler&#xff0c;以及数据的ETL流程。 一&#xff1a;调度工具DS 主流大数据调度工具DolphinScheduler&#xff0c; 其定位&#xff1a;解决数据处理流程中错综复杂的依赖关系 任务支持类型&#xff1a;支持传统的shell任务&a…

MBR40150FCT-ASEMI无人机专用MBR40150FCT

编辑&#xff1a;ll MBR40150FCT-ASEMI无人机专用MBR40150FCT 型号&#xff1a;MBR40150FCT 品牌&#xff1a;ASEMI 封装&#xff1a;TO-220F 批号&#xff1a;最新 最大平均正向电流&#xff08;IF&#xff09;&#xff1a;40A 最大循环峰值反向电压&#xff08;VRRM&a…

shell脚本——编程规范与变量

目录 一、shell脚本 1、shell脚本概述 2、shell脚本的应用场景 3、shell脚本的作用——命令解释器 二、Shell 脚本编程规范 1、用户登录Shell 2、shell脚本的构成 3、执行shell脚本 三、重定向与管道操作 1、重定向 1.1、交互式硬件设备 1.2、重定向操作 2、重定向…

php相关

php相关 ​ 借鉴了小迪安全以及各位大佬的博客&#xff0c;如果一切顺利&#xff0c;会不定期更新。 如果感觉不妥&#xff0c;可以私信删除。 默认有php基础。 文章目录 php相关1. php 缺陷函数1. 与2. MD53. intval()4. preg_match() 2. php特性1. php字符串解析特性2. 杂…

数据结构-C语言-排序(3)

代码位置&#xff1a;test-c-2024: 对C语言习题代码的练习 (gitee.com) 一、前言&#xff1a; 1.1-排序定义&#xff1a; 排序就是将一组杂乱无章的数据按照一定的规律&#xff08;升序或降序&#xff09;组织起来。(注&#xff1a;我们这里的排序采用的都为升序) 1.2-排序分…

从汇编层看64位程序运行——栈保护

大纲 栈保护延伸阅读参考资料 在《从汇编层看64位程序运行——ROP攻击以控制程序执行流程》中&#xff0c;我们看到可以通过“微操”栈空间控制程序执行流程。现实中&#xff0c;黑客一般会利用栈溢出改写Next RIP地址&#xff0c;这就会修改连续的栈空间。而编译器针对这种场景…

集合媒体管理、分类、搜索于一体的开源利器:Stash

Stash&#xff1a;强大的媒体管理工具&#xff0c;让您的影音生活井井有条- 精选真开源&#xff0c;释放新价值。 概览 Stash是一个专为个人媒体管理而设计的开源工具&#xff0c;基于 Go 编写&#xff0c;支持自部署。它以用户友好的界面和强大的功能&#xff0c;满足了现代用…

16_网络IPC2-寻址

进程标识 字节序 采用大小模式对数据进行存放的主要区别在于在存放的字节顺序&#xff0c;大端方式将高位存放在低地址&#xff0c;小端方式将高位存放在高地址。 采用大端方式进行数据存放符合人类的正常思维&#xff0c;而采用小端方式进行数据存放利于计算机处理。到目前…