[数据结构]C语言实现顺序表和链表及经典题目

[数据结构]顺序表和链表

1.线性表

线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使 用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串… 线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的, 线性表在物理上存储时,通常以数组和链式结构的形式存储。

image-20221229070309645

image-20221229070608899

2.顺序表

2.1概念及结构

顺序表是用一段物理地址连续的存储单元连续依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。

顺序表一般可以分为:

1. 静态顺序表:使用定长数组存储元素。

#define N 7//利用宏定义控制顺序表的大小

typedef int SLDataType;//定义顺序表内所存储的数据类型

//静态顺序表数据结构的定义
typedef struct	SeqList
{
	SLDataType arr[N];
	int size;//记录存储有效数据个数
}SL;

静态顺序表由于自身不方便修改自身存储数据的内存空间的大小,因此在实际使用中并不常见。

2.动态顺序表:使用动态开辟的数组存储。

typedef int SLDataType;//定义顺序表内所存储的数据类型

//动态顺序表数据结构的定义
typedef struct	SeqList
{
	SLDataType* arr;//利用指针管理动态内存空间
	int size;//记录存储有效数据个数
	int capacity;//空间容量大小
}SL;

2.2接口实现

数据结构的使用就是对数据类型定义和数据操作实现,因此我们不仅要知道数据类型的定义,也要知道数据操作函数的定义。

静态顺序表只适用于确定知道需要存多少数据的场景。静态顺序表的定长数组导致N定大了,空间开多了浪费,开少了不够用。所以现实中基本都是使用动态顺序表,根据需要动态的分配空间 大小,所以下面我们实现动态顺序表。

//初始化
void SLInit(SL* ps)
{
	assert(ps);//检查参数传入是否错误

	ps->arr = NULL;
	ps->size = ps->capacity = 0;
}

void SLDestroy(SL* ps)//销毁
{
    assert(ps);//检查参数传入是否错误
    
	if (ps->arr)
	{
		free(ps->arr);
		ps->arr = NULL;
		ps->size = ps->capacity = 0;
	}
}
void SLCheckCapacity(SL* ps)//检查空间是否为空或为满
{
	assert(ps);

	if (ps->size == ps->capacity)//判断顺序表是否为空或者为满
	{
		int newCapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
		SLDataType* tmp = (SLDataType*)realloc(ps->arr, newCapacity * sizeof(SLDataType));
		if (tmp == NULL)//判断空间申请是否成功
		{
			perror("ralloc fail");
			exit(-1);
		}
		ps->arr = tmp;
		ps->capacity = newCapacity;
	}
}

void SLPushBack(SL* ps, SLDataType x)//尾插
{
	assert(ps);
	SLCheckCapacity(ps);//检查空间是否为空或为满

	ps->arr[ps->size] = x;
	ps->size++;
}

void SLPushFront(SL* ps, SLDataType x)//头插
{
	assert(ps);
	SLCheckCapacity(ps);//检查空间是否为空

	int end = ps->size-1;//size指向最后一个数据的下一个位置
	while (end >= 0)
	{
		ps->arr[end + 1] = ps->arr[end];
		end--;
	}
	ps->arr[0] = x;
	ps->size++;
}
void SLInsert(SL* ps, int pos, SLDataType x)//在pos位置插入数据
{
	assert(ps);
	assert(pos >= 0);
	assert(pos <= ps->size);//由于顺序表要求连续存储数据,因此数据插入必须在0到size位置,在size插入相当于尾插,在0插入相当于头插
	SLCheckCapacity(ps);

	int end = ps->size - 1;
	while (end >= pos)
	{
		ps->arr[end + 1] = ps->arr[end];
		end--;
	}
	ps->arr[pos] = x;
	ps->size++;
}

由于SLInsert函数可以在“任意位置”插入,因此可以用SLInsert函数来实现头插尾插函数。

void SLInsert(SL* ps, int pos, SLDataType x)//在pos位置插入数据
{
	assert(ps);
	assert(pos >= 0);
	assert(pos <= ps->size);//由于顺序表要求连续存储数据,因此数据插入必须在0到size位置,在size插入相当于尾插,在0插入相当于头插
	SLCheckCapacity(ps);

	int end = ps->size - 1;
	while (end >= pos)
	{
		ps->arr[end + 1] = ps->arr[end];
		end--;
	}
	ps->arr[pos] = x;
	ps->size++;
}

void SLPushBack(SL* ps, SLDataType x)//用SLInsert实现尾插
{
	SLInsert(ps, ps->size, x);
}

void SLPushFront(SL* ps, SLDataType x)//用SLInsert实现头插
{
	SLInsert(ps, 0, x);
}
void SLPopBack(SL* ps)//尾删
{
	assert(ps);
	assert(ps->size > 0);

	ps->size--;
}

void SLPopFront(SL* ps)//头删
{
	assert(ps);
	assert(ps->size > 0);

	int begin = 0;
	while (begin < ps->size - 1)
	{
		ps->arr[begin] = ps->arr[begin + 1];
		begin++;
	}
	ps->size--;
}
void SLErase(SL* ps, int pos)//在pos位置删除数据
{
	assert(ps);
	assert(pos >= 0);
	assert(pos < ps->size);//由于顺序表要求连续存储数据,因此数据删除必须在0到size-1位置,在size插入相当于尾删,在0插入相当于头删
	assert(ps->size > 0);//可加可不加,如果ps->size <= 0,前两条语句就会报错。

	while (pos < ps->size - 1)
	{
		ps->arr[pos] = ps->arr[pos + 1];
		pos++;
	}
	ps->size--;
}

同样的,我们可以用SLErase函数实现头删尾删。

void SLErase(SL* ps, int pos)//在pos位置删除数据
{
	assert(ps);
	assert(pos >= 0);
	assert(pos < ps->size);//由于顺序表要求连续存储数据,因此数据删除必须在0到size-1位置,在size插入相当于尾删,在0插入相当于头删
	assert(ps->size > 0);//可加可不加,如果ps->size <= 0,前两条语句就会报错。

	while (pos < ps->size - 1)
	{
		ps->arr[pos] = ps->arr[pos + 1];
		pos++;
	}
	ps->size--;
}

void SLPopBack(SL* ps)//用SLErase函数尾删
{
	SLErase(ps, ps->size - 1);
}

void SLPopFront(SL* ps)//用SLErase函数头删
{
	SLErase(ps, 0);
}

插入数据时要注意空间是否够用,删除数据时要注意是否会造成越界。

对于越界的检查不同的编译器的检查方法是不同的,因此可能会造成同样的程序在一个编译器下没问题,另一个编译器下有问题,越界问题是一个很严重的问题,越界问题不一定会被编译器检测出来,但不代表没有问题,因此写程序时要注意是否会造成越界。

int SLFind(SL* ps, SLDataType x)//查找数据位置
{
	assert(ps);

	for (int i = 0; i < ps->size; i++)
	{
		if (ps->arr[i] == x)
		{
			return i;//所查找的数据存在返回下标
		}
	}
	return -1;//所查找的数据不存在
}

配合SLErase函数可以实现指定数据的删除。

int pos = SLFind(&sl, 3);
	if (pos != -1)
	{
		SLErase(&sl, pos);
	}
//将顺序表中某一个数据全部删除
int SLFind(SL * ps, SLDataType x, int begin)//查找数据位置
	{
		assert(ps);

		for (int i = begin; i < ps->size; i++)
		{
			if (ps->arr[i] == x)
			{
				return i;//所查找的数据存在返回下标
			}
		}
		return -1;//所查找的数据不存在
	}
	int pos = SLFind(&sl, 5, 0);
	while (pos != -1)
	{
		SLErase(&sl, pos);

		pos = SLFind(&sl, 5, pos);

	}
void SLPrint(SL* ps)//打印数据
{
	assert(ps);

	for (int i = 0; i < ps->size; i++)
	{
		printf("%d ", ps->arr[i]);
	}
	printf("\n");
}

2.3. LeetCode经典题目

2.3.1.移除元素

题目描述:

image-20221229145749897

要求时间复杂度为O(N),空间复杂度为O(1)。

题目解析:

关于此题,我们只需要用两个指针,也就是表示数组下标的变量,一个用来遍历数组,另一个用来记录数据存储的位置,

比如我们用src变量来遍历数组,dst变量来记录数据存储位置,利用src变量遍历数组时,如果数据不等于val,dst位置就保存该数据,同时dst指向下一个位置来存储下一个数据,直到利用src变量完成遍历数组,注意dst指向的是最后一个元素的下一个位置

image-20221229144133312

代码实现

int removeElement(int* nums, int numsSize, int val){
    int src = 0;
    int dst = 0;
    while(src < numsSize)
    {
        if (nums[src] != val)
        {
            nums[dst++] = nums[src];
        }
        src++;
    }
    return dst;
}
2.3.2.删除有序数组中的重复项

题目描述:

image-20221229145806206

image-20221229145658868

题目解析:

删除有序数组中的重复项是移除元素的变形题目,也是用一个双指针解决,由于数组元素是升序,因此重复元素会并列出现,

定义两个变量,一个src变量用来遍历数组,一个dst变量用来存储新的数据的存储位置的变量,利用src变量遍历数组,如果src变量指向的元素等于dst变量指向的元素,src变量继续遍历,当src变量指向的数据不同于dst变量指向的数据时,dst变量指向下一个位置然后在dst变量指向位置存储新的数据,循环直到src变量遍历完成,注意dst最后指向的是最后一个元素

image-20221229150757586

代码实现:

int removeDuplicates(int* nums, int numsSize){
    int src = 0;
    int dst = 0;
    while(src < numsSize)
    {
        if(nums[src] == nums[dst])
        {
            src++;
        }
        else
        {
            nums[++dst] = nums[src];
        }
    }
    return dst+1;
}
2.3.3.合并两个有序数组

题目描述:

image-20221229153009369

image-20221229153022535

题目解析:

由于涉及到两个数组,因此我们选择采用三指针,一个i1变量用于遍历nums1数组,一个i2变量用来遍历nums2数组,一个j变量用于指向数据存储位置,由于是将nums1和nums2两个数组的数据合并,如果nums2中有比nums1小的数据会造成数据覆盖因此,我们采取从后往前遍历的方式,如果i1和i2指向的数据谁大,谁先赋值给j位置,并且指向前一个数据,由于数据大小的原因,会产生两种情况,一是nums2数组先遍历完成:

image-20221229154208288

nums2遍历完成后,也就得到我们想要的数组了。

另一种情况是nums1数组先遍历完:

image-20221229154541218

遍历完后nums2数组里还有比j位置后面更小的数据,应当全部移到j位置及之前。

代码实现:

void merge(int* nums1, int nums1Size, int m, int* nums2, int nums2Size, int n){
    int i1 = m - 1;
    int i2 = n - 1;
    int j = m + n -1 ;
    while(i1 >= 0 && i2 >= 0)
    {
        if(nums1[i1] > nums2[i2])
        {
            nums1[j] = nums1[i1--];
        }
        else
        {
            nums1[j] = nums2[i2--];
        }
        j--;
    }
    while(i2 >= 0)
    {
        nums1[j--] = nums2[i2--];
    }
}

3.链表

顺序表有一些缺点无法避免:

1.空间不够,需要扩容。扩容,尤其是异地扩容是有一些代价。并且每次扩容后可能会有一定空间的浪费。

2.头部或者中部的数据插入,需要挪动数据,效率低下。

针对如上问题提出以下解决方案:

1.按需申请空间

2.不进行数据的挪动

而链表就满足以上两点。

3.1链表的概念及结构

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

image-20221229172419105

现实中 数据结构中

image-20221229172449729

注意:

1.从上图可看出,链式结构在逻辑上是连续的,但是在物理上不一定连续。

2.现实中的结点一般都是从堆上申请出来的。

3.从堆上申请的空间,是按照一定的策略来分配的,两次申请的空间可能连续,也可能不连续。

3.2链表的分类

实际中链表的结构非常多样,以下情况组合起来就有8种链表结构:

1.单向或者双向

image-20221229172758671

2. 带头或者不带头

image-20221229172903010

3.循环或者非循环

image-20221229172926398

虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构:

image-20221229173033276

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

3.3无头单向非循环链表的实现

实现链表前,我们需要知道一些事情,看如下两段代码:

typedef int SLTDataType;

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

SLTNode* BuySLTNode(SLTDataType x)//申请新的结点
{
	SLTNode* newnode;
    newnode->data = x;
    newnode->next = NULL;
	return newnode;
}
typedef int SLTDataType;

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

SLTNode* BuySLTNode(SLTDataType x)//申请新的结点
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}

	newnode->data = x;
	newnode->next = NULL;

	return newnode;
}

同样是申请结点,第一段代码简洁许多,但我们不会选择第一种,因为第一种方式是在栈帧里创建变量,出了栈帧就被销毁了,无法在整个程序中任意位置使用这个结点,用这个结点申请的链表也就没有意义了,第二种方式是在堆区上申请结点,堆区上的变量由程序员自己选择什么时候销毁,因此可以在整个程序中任意位置使用,这些结点组成的链表才有意义。

想象中的链表就像串串一样是串成一条线的,但是在实际存储空间中,是一个个分散的结点,通过指针来联系起立的,

在函数中访问一个链表我们往往只关心链表的第一个结点,因为链表是通过指针一个个连接起来的,只要有第一个结点,就能依次访问后面的所有结点。

链表的类型有8种,本文只实现两种:无头单向非循环链表和带头双向循环链表,因为其他类型的链表都可以由这两种变化出来。

3.3.1实现链表时常见的错误

实现无头单向非循环链表时有一些常见的误区:

3.3.1.1实现尾插时的错误:

错误1:不能正确的找到尾结点

//单向非循环链表数据结构定义
typedef int SLTDataType;

typedef struct SListNode
{
	SLTDataType data;
	struct SListNode* next;
}SLTNode;
void SLTPushBack(SLTNode* phead, SLTDataType x)//尾插
{
	SLTNode* ptail = phead;
	SLTNode* newnode = BuySLTNode(x);//申请新的结点
	while (ptail)
	{
		ptail = ptail->next;
	}
	ptail = newnode;
}

由于链表的尾插是在尾结点后链接上一个新的结点,为了找到尾结点定义一个变量ptail让ptail不断往后找,此段代码中while循环的结束条件是ptail为空,也就是说ptail遍历了整个链表后,ptail的值为NULL,然后令ptail = newnode ,此时只是让ptail这个局部指针变量指向了新的结点,并没有改变尾结点的指向,函数调用结束ptail作为局部变量被销毁了,不仅没链接上新的结点,还造成了内存泄漏。

//修改后1
void SLTPushBack(SLTNode* phead, SLTDataType x)//尾插
{
	SLTNode* ptail = phead;
	SLTNode* newnode = BuySLTNode(x);//申请新的结点
	while (ptail->next)
	{
		ptail = ptail->next;
	}
	ptail->next = newnode;
}

对上面的代码进行了一些修改后,while循环结束的条件变成了ptail->next为空的时候,一个正确的单向非循环链表尾结点指向应该是NULL,因此循环结束后ptail指向的尾结点,此时ptail作为指针变量改变尾结点的指向是可以做到的,但修改后的程序还是存在问题的,

如果phead传过来是一个空指针,也就是程序需要在一个空链表链接结点,phead为NULL,ptail = phead 也为NULL,ptail->next是在访问空指针,造成程序崩溃,为了解决这个问题,进行一些修改,有可能造成下一个错误。

错误2:误认为形参能改变实参

//修改后2
void SLTPushBack(SLTNode* phead, SLTDataType x)//尾插
{
	SLTNode* ptail = phead;
	SLTNode* newnode = BuySLTNode(x);//申请新的结点
	if(phead == NULL)
    {
        phead = newnode;
    }
    else
    {
        while (ptail->next)
	 {
		ptail = ptail->next;
	 }
	 ptail->next = newnode;
    }
}

为了解决空链表不能链接新的结点的问题,修改了如上代码,进行了选择判断,但还是无法正确的链接新的结点,因为phead变量是链表头指针的临时拷贝,phead的改变不改变实参,实参指向空,phead也指向空,函数中phead = newnode,只是改变了phead这个局部变量的指向,并没有改变实参的指向,如果想要改变实参的指向,我们要传入的是实参的指针,因为 只有实参的指针才能够改变实参的内容,也就是实参的指向。就像在函数中改变int要传入int*,改变int 要传入int*。

//修改后3
void SLTPushBack(SLTNode** pphead, SLTDataType x)//尾插
{
	SLTNode* newnode = BuySLTNode(x);
	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	else
	{
		SLTNode* ptail = *pphead;
		while (ptail->next)
		{
			ptail = ptail->next;
		}
		ptail->next = newnode;
	}
}

修改后利用二级指针解决了空链表的链接问题,即使是一个空链表,可以通过对头结点的指针的指针的解引用,访问头结点的指针的指向。

3.3.1.2实现尾删时的错误
//单向非循环链表数据结构定义
typedef int SLTDataType;

typedef struct SListNode
{
	SLTDataType data;
	struct SListNode* next;
}SLTNode;
void SLTPopBack(SLTNode* phead)
{
    SLTNode* tail = phead;
    while(tail->next->next)
    {
        tail = tail->next;
    }
    free(tail->next);
    tail->next = NULL;
}

这样实现的问题在于如果只有一个结点tail->next为NULL,tail->next->next就相当于NULL->next是一个非法访问操作。

//修改后
void SLTPopBack(SLTNode** pphead)//尾删
{
	assert(*pphead);//空链表不能继续删除

	if ((*pphead)->next == NULL)//只有一个结点 
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		SLTNode* tail = *pphead;
		while (tail->next->next)
		{
			tail = tail->next;
		}
		free(tail->next);
		tail->next = NULL;
	}
}

利用二级指针并进行条件判断解决只有一个结点的问题,这里还要注意加上断言检查语句,不同于尾插,空链表可以插入但不能删除

3.3.1.3总结

实现链表时,插入结点时要注意空指针的访问,删除结点时要注意空链表的删除和一个结点的删除。注意这些边缘条件才能减少错误。

3.3.2接口函数的实现
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <errno.h>

typedef int SLTDataType;

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

SLTNode* BuySLTNode(SLTDataType x)//申请新的结点
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}

	newnode->data = x;
	newnode->next = NULL;

	return newnode;
}


void SLTPushBack(SLTNode** pphead, SLTDataType x)//尾插
{
	SLTNode* newnode = BuySLTNode(x);
	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	else
	{
		SLTNode* ptail = *pphead;
		while (ptail->next)
		{
			ptail = ptail->next;
		}
		ptail->next = newnode;
	}
}

void SLTPopBack(SLTNode** pphead)//尾删
{
	assert(*pphead);

	if ((*pphead)->next == NULL)//只有一个结点 
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		SLTNode* tail = *pphead;
		while (tail->next->next)
		{
			tail = tail->next;
		}
		free(tail->next);
		tail->next = NULL;
	}
}


void SLTPrint(SLTNode* phead)
{
	SLTNode* cur = phead;
	while (cur != NULL)
	{
		printf("%d->", cur->data);
		cur = cur->next;
	}
	printf("NULL\n");
}

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

void SLTPopFront(SLTNode** pphead)//头删
{
	assert(*pphead);

	SLTNode* next = (*pphead)->next;
	free(*pphead);
	*pphead = next;
}

SLTNode* SLTFind(SLTNode* phead,SLTDataType x)//查找
{
	SLTNode* cur = phead;
	while (cur)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

void SLTInsertAfter(SLTNode* pos, SLTDataType x)//在pos位置后面插入结点
{
	assert(pos);

	SLTNode* newnode = BuySLTNode(x);
	SLTNode* next = pos->next;
	pos->next = newnode;
	newnode->next = next;
}

void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)//在pos位置之前插入结点
{
	//不需要断言,可以解决*pphead或pos为空的情况
	//*pphead和pos为空即为空链表插入
	//pos为空即是尾插
	if (*pphead == pos)
	{
		SLTPushFront(pphead, x);
	}
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		SLTNode* newnode = BuySLTNode(x);
		prev->next = newnode;
		newnode->next = pos;
	}
}


void SLTEraseAfter(SLTNode* pos)//删除pos位置后的结点
{
	assert(pos);

	if (pos->next == NULL)
	{
		return;
	}
	else
	{
		SLTNode* nextnode = pos->next;
		pos->next = nextnode->next;
		free(nextnode);
	}
}

void SLTErase(SLTNode** pphead, SLTNode* pos)//删除pos位置的结点
{
	assert(*pphead);
	assert(pos);

	if (*pphead == pos)
	{
		SLTPopFront(pphead);
	}
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		prev->next = pos->next;
		free(pos);
	}
}

void SLTDestory(SLTNode** pphead)//链表的释放
{
	SLTNode* cur = *pphead;
	while (cur)
	{
		SLTNode* next = cur->next;
		free(cur);
		cur = next;
	}
	*pphead = NULL;
}

**总结:**以上接口函数都可以用二级指针形参或一级指针形参(可能需要修改函数返回值类型)可以解决,但是有时修改会使使用时十分的不方便,总之需要修改头结点一般使用二级指针,不需要修改头结点一般使用一级指针。

3.4 无头单向非循环链表经典题目

3.4.1反转链表(LeetCode)

题目链接:206. 反转链表 - 力扣(Leetcode)

题目描述:

image-20221230150710815

image-20221230150718749

题目解析:

解法1:将原有链表头插到一个新的空链表实现链表的反转。

解法2:依次访问原链表的每个结点使原链表的每个结点指向上一个结点。

image-20221231153919265

代码实现

//解法1
struct ListNode* reverseList(struct ListNode* head){
    struct ListNode* cur = head;
    struct ListNode* newhead = NULL;
    struct ListNode* next = NULL;
    while(cur)
    {
        next = cur->next;
        cur->next = newhead;
        newhead = cur;
        cur = next;
    }
    return newhead;
}
//解法2
struct ListNode* reverseList(struct ListNode* head){
    if (head == NULL)//避免传入的是空链表
      return NULL;
    struct ListNode* n1,*n2,*n3;
    n1 = NULL;
    n2 = head;
    n3 = head->next;
    while (n2)
    {
        n2->next = n1;
        n1 = n2;
        n2 = n3;
        if (n3)//避免n3造成空结点访问
         n3 = n3->next;
    }
    return n1;
}
3.4.2移除链表元素(LeetCode)

题目链接:203. 移除链表元素 - 力扣(Leetcode)

题目描述:

image-20221230174942697

image-20221230174954559

题目解析:

创建一个新的链表把所有需要的结点链接上,不需要的结点就释放掉。

代码实现:

//解法1 用不带哨兵位的新链表链接
struct ListNode* removeElements(struct ListNode* head, int val){
    struct ListNode* cur = head;
    struct ListNode* newhead, *tail;
    newhead = tail =NULL;
    while(cur)
    {
        if (cur->val != val)
        {
            if (newhead == NULL)
            {
                newhead = tail = cur;
            }
            else 
            {
                tail->next = cur;
                tail = cur;
            }
            cur = cur-> next;
        }
        else
        {
            struct ListNode* next = cur->next;
            free(cur);
            cur = next;
        }
    }
    if (tail)//如果传入为空链表或没有需要结点的链表导致新链表为空,避免造成空指针的访问
        tail->next = NULL;//处理尾结点指向野指针

    return newhead;
}
//解法2
//带哨兵位的头结点的新链表
struct ListNode* removeElements(struct ListNode* head, int val){
    struct ListNode* cur = head;
    struct ListNode* guard, *tail;
    guard = tail = (struct ListNode*)malloc(sizeof(struct ListNode));
    guard->next = NULL;
    while(cur)
    {
        if (cur->val != val)
        {
            tail->next = cur;
            tail = tail->next;
            cur=cur->next;
        }
        else 
        {
            struct ListNode* next = cur->next;
            free(cur);
            cur = next;
        }
    }
    tail->next = NULL;
    struct ListNode* newnode = guard->next;
    free(guard);
    return newnode;
}
3.4.3合并两个有序链表(LeetCode)

题目链接:21. 合并两个有序链表 - 力扣(Leetcode)

题目描述:

image-20221230180826776

image-20221230180842029

题目解析:

创建一个新的空链表,把结点按顺序依次尾插到新的链表上。

代码实现:

//解法1
//不带哨兵位的单链表
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2){
    if (list1 == NULL)  //不带哨兵位要注意如果其中一个链表为空时的空指针访问
      return list2;
    if (list2 == NULL)
      return list1;
    struct ListNode* head, *tail;
    head = tail = NULL;
    while(list1 && list2)
    {
        if (list1->val < list2->val)
        {
            if (head == NULL)
            {
                head = tail = list1;
            }
            else
            {
                tail->next = list1;
                tail = tail->next;
            }
            list1 = list1->next;
        }
        else
        {
            if (head == NULL)
            {
                head = tail = list2;
            }
            else
            {
                tail->next = list2;
                tail = tail->next;
            }
            list2 = list2->next;
        }
    }
    if (list1)
      tail->next = list1;
    if (list2)
      tail->next = list2;
    return head;
}

代码实现2:

//解法2
//带哨兵位的单链表
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2){
    struct ListNode* guard = (struct ListNode*)malloc(sizeof(struct ListNode));
    guard->next = NULL;
    struct ListNode* tail = guard;
    while (list1 && list2)
    {
        if (list1->val < list2->val)//尾插简单
        {
            tail->next = list1;
            tail = tail->next;
            list1 = list1->next;
        }
        else
        {
            tail->next = list2;
            tail = tail->next;
            list2 = list2->next;
        }
    }
    if (list1)
      tail->next = list1;
    if (list2)
      tail->next = list2;
    struct ListNode* head = guard->next;
    free(guard);
    return head;
}

**总结:**用带哨兵位的空链表尾插时会简单很多,由于头结点是不为空的不用考虑空指针的访问问题。

3.4.4链表的中间结点(LeetCode)

题目链接:876. 链表的中间结点 - 力扣(Leetcode)

题目描述:

image-20221231154237833

image-20221231154251601

题目解析:

利用快慢指针,开始时快指针和慢指针同时指向链表的头结点,每次循环快指针走两个结点,慢指针走一个结点,循环停下来时,慢指针指向的就是中间结点,当结点个数为奇数时,循环结束的条件是快指针指向结点的指向为空,当结点个数为偶数时,循环结束的条件时快指针指向为空。

struct ListNode* middleNode(struct ListNode* head){
    struct ListNode* fast,*slow;
    fast = slow = head;
    while (fast && fast->next)//一定是先判断fast是否为空,否则会造成空指针的访问问题
    {
        slow = slow->next;
        fast = fast->next->next;
    }
    return slow;
}
类似题目(牛客网)

题目链接:链表中倒数第k个结点_牛客题霸_牛客网 (nowcoder.com)

题目描述:

image-20221231161305679

image-20221231161317103

题目解析:

利用快慢指针,快指针提前走k步或k-1步,然后循环快指针和慢指针同时走一步,结束时慢指针指向结点为倒数第k的结点。

代码实现:

//快指针先走k步
struct ListNode* FindKthToTail(struct ListNode* pListHead, int k ) {
    // write code here
    struct ListNode* slow,*fast;
    slow = fast = pListHead;
    while (k&&fast)
    {
        k--;
        fast = fast->next;
    }
    if (k)
    return NULL;
    while (fast)
    {
        slow = slow->next;
        fast = fast->next;
    }
    return slow;
}
3.4.5 链表分割(牛客网)

题目链接:链表分割_牛客题霸_牛客网 (nowcoder.com)

题目描述:

image-20221231163735768

image-20221231163746726

题目解析:

创建两个哨兵位,一个哨兵位链接小于x的结点,另一个哨兵位链接大于x的结点,然后将两个链表链接起来。

代码实现:

class Partition {
public:
    ListNode* partition(ListNode* pHead, int x) {
        // write code here
        struct ListNode* lesshead = (struct ListNode*)malloc(sizeof(struct ListNode));
        struct ListNode* greaterhead = (struct ListNode*)malloc(sizeof(struct ListNode));
        struct ListNode* lesstail,*greatertail;
        lesstail = lesshead;
        greatertail = greaterhead;
        lesstail->next = NULL;
        greatertail->next = NULL;
        struct ListNode* cur = pHead;
        while(cur)
        {
            if (cur->val < x)
            {
                lesstail->next = cur;
                lesstail = lesstail->next;
            }
            else
            {
                greatertail->next = cur;
                greatertail = greatertail->next;
            }
            cur = cur->next;
        }
        greatertail->next = NULL;//避免最后一个结点小于x造成链表循环
        lesstail->next = greaterhead->next;
        struct ListNode* newhead = lesshead->next;
        free(lesshead);
        free(greaterhead);
        return newhead;
    }
};
3.4.6链表的回文结构(LeetCode)

题目链接:链表的回文结构_牛客题霸_牛客网 (nowcoder.com)

题目描述:

image-20230101154942370

image-20230101154953511

题目解析:

将链表的后半部分逆置和前半部分对比,相同则为回文链表。

image-20230101155933169

代码实现:

class PalindromeList {
public:
    struct ListNode* mid(struct ListNode* head)//取链表中间结点
    {
        struct ListNode* fast,*slow;
        fast = slow = head;
        while (fast && fast->next)
        {
            slow = slow->next;
            fast = fast->next->next;
        }
        return slow;
    }
    struct ListNode* reverse(struct ListNode* head)//用头插法逆置链表
    {
        struct ListNode* newhead =NULL;
        struct ListNode* cur = head;
        while(cur)
        {
           struct ListNode*next = cur->next;
           cur->next = newhead;
           newhead = cur;
           cur = next;
        }
        return newhead;
    }
    bool chkPalindrome(ListNode* A) {
        // write code here
        struct ListNode* midnode  = mid(A);
        struct ListNode* midder = reverse(midnode);
        while(A && midder)
        {
            if (A->val != midder->val)
            {
                return false;
            }
            A = A->next;
            midder = midder->next;
        }
        return true;
    }
};
3.4.7相交链表(LeetCode)

题目链接:160. 相交链表 - 力扣(Leetcode)

题目描述:

image-20230102152446492

image-20230102152457816

题目解析:

如果两个链表相交那么两个链表的最后一个结点一定相同,首先判断两个链表是否有交点,如果有交点由于链表长度可能不能如果挨个对比时间复杂度太高,因此我们采用让长的链表先走差距步,然后两个链表同时走找到交点。

代码实现:

struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
    struct ListNode* curA = headA, *curB = headB;
    int longA = 0, longB = 0;
    
    while(curA->next)
    {
        longA++;
        curA = curA->next;
    }

    while(curB->next)
    {
        longB++;
        curB = curB->next;
    }

    if (curA != curB)//无交点
      return NULL;

    int gap = abs(longA - longB);//计算长度差值
    struct ListNode* longlist = headA, *shortlist = headB;//***假设第一个链表长***避免代码重复
    if (longA < longB)//如果假设错误进行修正
    {
        longlist = headB;
        shortlist = headA;
    }
    //长链表走差距步
    while (gap--)
    {
        longlist = longlist->next;
    }
    //找到第一个交点
    while (longlist != shortlist)
    {
        longlist = longlist->next;
        shortlist = shortlist->next;
    }

    return longlist;
}
3.4.8环形链表

题目链接:141. 环形链表 - 力扣(Leetcode)

题目描述:

image-20230102160558992

image-20230102160612820

题目解析:

快指针每次走两步,慢指针每次走一步,快指针先进入环,慢指针后进入环,此时两个指针的距离小于等于环的长度,快指针追慢指针,能追上证明有环。

为什么快指针每次走两步,慢指针走一步可以?

两个指针进入环后,两个指针的距离小于等于环的长度,快指针每次走两步,慢指针每次走一步,差距步数为一步,相当于快指针每次追慢指针一步,假设两个指针差距N步,只要追N次就一定能追上,因此快指针每次走两步,慢指针走一步可以完成。

快指针一次走3步,走4步,…n步行吗?

假设快指针走3步,两个指针进入环后,两个指针的距离小于等于环的长度,快指针每次走三步,慢指针每次走一步,差距步数为两步,相当于快指针每次追慢指针两步,假设两个指针差距N步:

image-20230102161906495

由此可以看出如果快指针一次走的步数大于2,能否追上是不确定的。

代码实现:

bool hasCycle(struct ListNode *head) {
    struct ListNode* fast = head, *slow = head;
    
    while (fast && fast->next)
    {
        slow = slow->next;
        fast = fast->next->next;

        if (slow == fast)
           return true;
    }

    return false;//没有环返回假
}
3.4.9环形链表 II

题目链接:142. 环形链表 II - 力扣(Leetcode)

题目描述:

image-20230102162645171

image-20230102162702183

题目解析:

解法1:

使用一个快指针,一个慢指针,快指针每次走两步,慢指针每次走一步,快慢指针会在环中相遇,然后,head指针从链表的头结点出发,meet指针从快慢指针的相遇结点出发,当head指针和meet指针相遇即为入口点。

证明:

image-20230102165943909

解法2:

利用快慢指针找到相遇结点,然后将相遇点的下一个结点作为一个新的头结点,并把相遇点的next置空,转化为两个链表(相遇点的下一个结点和原链表的头结点)求第一个交点的问题。

image-20230102171137966

代码实现:

//解法1
struct ListNode *detectCycle(struct ListNode *head) {
    struct ListNode* fast = head, *slow = head;

    while (fast && fast->next)
    {
        slow = slow->next;
        fast = fast->next->next;
        
        if (slow == fast)//找到相遇点
        { 
           struct ListNode* meet = fast;
           while (head != meet)
           {
               head = head->next;
               meet = meet->next;
           }
           
           return meet;
        }
    }
    
    return NULL;
}
//解法2
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) //找第一个交点的函数
{
    struct ListNode* curA = headA, *curB = headB;
    int longA = 0, longB = 0;
    
    while(curA->next)
    {
        longA++;
        curA = curA->next;
    }

    while(curB->next)
    {
        longB++;
        curB = curB->next;
    }

    if (curA != curB)//无交点
      return NULL;

    int gap = abs(longA - longB);
    struct ListNode* longlist = headA, *shortlist = headB;//假设第一个链表长
    if (longA < longB)//如果假设错误进行修正
    {
        longlist = headB;
        shortlist = headA;
    }
    //长链表走差距步
    while (gap--)
    {
        longlist = longlist->next;
    }
    //找到第一个交点
    while (longlist != shortlist)
    {
        longlist = longlist->next;
        shortlist = shortlist->next;
    }

    return longlist;
}

struct ListNode *detectCycle(struct ListNode *head) {
    struct ListNode* fast = head, *slow =head;
    while (fast && fast->next)
    {
        slow = slow->next;
        fast = fast->next->next;

        if (slow == fast)
        {
            struct ListNode* meet = fast;
            struct ListNode* otherhead = meet->next;//新的头结点
            meet->next = NULL;
            struct ListNode* node = getIntersectionNode(head, otherhead);//找第一个交点
            return node;
        }
    }

    return false;
}
3.4.10 复制带随机指针的链表

题目链接:138. 复制带随机指针的链表 - 力扣(Leetcode)

题目描述:

image-20230102200833158

image-20230102200843149

题目解析:

1.将拷贝结点依次链接到原结点后

image-20230102204738104

2.利用原结点赋值拷贝结点的random

拷贝结点的random只需要指向原结点的random的next即可。

3.将拷贝结点链接起来

代码实现:

struct Node* copyRandomList(struct Node* head) {
    struct Node* cur = head;
    while (cur)//将拷贝结点链接
    {
        struct Node* next = cur->next;
        struct Node* copy = (struct Node*)malloc(sizeof(struct Node));
        
        copy->val = cur->val;
        copy->next = next;
        copy->random = NULL;
        cur->next = copy;
        cur = next;
    }

    cur = head;
    while (cur)//给拷贝结点random赋值
    {
        struct Node* copy = cur->next;
        struct Node* next = copy->next;

        if (cur->random)
        {
            copy->random = cur->random->next;
        }

        cur = cur->next->next;
    }

    struct Node* newhead, *newtail;
    newhead = newtail = NULL;
    cur = head;
    while (cur)//将拷贝结点链接起来
    {
        struct Node* copy = cur->next;
        struct Node* next = copy->next;

        if (newhead == NULL)
        {
            newhead = newtail = copy;
        }
        else
        {
            newtail->next = copy;
            newtail = newtail->next;
        }
        cur = next;
    }
    return newhead;
}

3.5有头双向循环链表的实现

有头双向循环链表虽然结构复杂,但是使用代码实现以后会发现结构会带 来很多优势,实现反而简单了。

顺序表的为空是创建一个结构体,结构体内的指针可以指向一片空间也可以指向空,但是结构体的size为0。

无头单向非循环链表为空是指向链表头结点的结构体指针指向空。

有头双向循环链表为空是申请一个哨兵位头结点,哨兵位的头结点上一个结点是自身,下一个结点也是自身。

//数据结构的定义
typedef int LTDataType;

typedef struct ListNode
{
	struct ListNode* prev;
	struct ListNode* next;
	LTDataType data;
}LTNode;

LTNode* BuyListNode(LTDataType x)//申请新的结点
{
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
	
	if (newnode == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	
	newnode->data = x;
	newnode->prev = NULL;
	newnode->next = NULL;

	return newnode;
}


LTNode* LTInit()//初始化
{
	LTNode* phead = BuyListNode(-1);
	
	phead->next = phead;
	phead->prev = phead;

	return phead;
}

void LTPushBack(LTNode* phead,LTDataType x)//尾插
{
	assert(phead);

	LTNode* newnode = BuyListNode(x);
	LTNode* tail = phead->prev;
	
	tail->next = newnode;
	newnode->prev = tail;
	
	phead->prev = newnode;
	newnode->next = phead;
}

void LTPopBack(LTNode* phead)//尾删
{
	assert(phead);
	assert(phead->next != phead);//空链表

	LTNode* tail = phead->prev;
	LTNode* tailPrev = tail->prev;

	tailPrev->next = phead;
	phead->prev = tailPrev;

	free(tail);
}


void LTPushFront(LTNode* phead, LTDataType x)//头插
{
	assert(phead);

	LTNode* newnode = BuyListNode(x);

	顺序有关
	//newnode->next = phead->next;
	//phead->next->prev = newnode;

	//newnode->prev = phead;
	//phead->next = newnode;

	//顺序无关
	LTNode* first = phead->next;

	phead->next = newnode;
	newnode->prev = phead;
	newnode->next = first;
	first->prev = newnode;
}

void LTPopFront(LTNode* phead)//头删
{
	assert(phead);
	assert(phead->next != phead);//判空

	LTNode* first = phead->next;
	LTNode* second = first->next;

	free(first);

	phead->next = second;
	second->prev = phead;
}

void LTPrint(LTNode* phead)//打印数据
{
	assert(phead);

	LTNode* cur = phead->next;

	while (cur != phead)
	{
		printf("%d ", cur->data);
		cur = cur->next;
	}
	printf("\n");
}

LTNode* LTFind(LTNode* phead, LTDataType x)//查找数据
{
	assert(phead);
	assert(phead->next != phead);

	LTNode* cur = phead->next;
	while (cur != phead)
	{
		if (cur->data == x)
			return cur;

		cur = cur->next;
	}
	return NULL;
}
void LTInsert(LTNode* pos, LTDataType x)//在pos之前插入x 
{
	assert(pos);

	LTNode* newnode = BuyListNode(x);
	LTNode* prev = pos->prev;

	prev->next = newnode;
	newnode->prev = prev;
	newnode->next = pos;
	pos->prev = newnode;
}

void LTErase(LTNode* pos)//删除pos位置
{
	assert(pos);

	LTNode* prev = pos->prev;
	LTNode* next = pos->next;

	free(pos);

	prev->next = next;
	next->prev = prev;
}

void LTPushBack(LTNode* phead,LTDataType x)//尾插
{
	assert(phead);

	//利用LTInsert实现尾插
	LTInsert(phead, x);
}


void LTPopBack(LTNode* phead)//尾删
{
	assert(phead);
	assert(phead->next != phead);//空链表

	//利用LTErase实现尾删
	LTErase(phead->prev);
}


void LTPushFront(LTNode* phead, LTDataType x)//头插
{
	assert(phead);

	//利用LTInsert实现头插
	LTInsert(phead->next, x);
}

void LTPopFront(LTNode* phead)//头删
{
	assert(phead);
	assert(phead->next != phead);//判空

	//利用LTErase实现头删
	LTErase(phead->next);
}
bool LTEmpty(LTNode* phead)//判空
{
	assert(phead);

	return phead->next == phead;
}

size_t LTSize(LTNode* phead//查看链表存储数据个数
{
	assert(phead);

	LTNode* cur = phead->next;
	size_t size = 0;

	while (cur != phead)
	{
		size++;
		cur = cur->next;
	}

	return size;
}

void LTDestroy(LTNode* phead)//链表销毁(使用完手动置空哨兵位)
{
	assert(phead);

	LTNode* cur = phead->next;
	while (cur != phead)
	{
		LTNode* next = cur->next;

		free(cur);

		cur = next;
	}

	free(phead);
}

4.顺序表和链表的对比

(链表->带头双向循环链表)

不同点顺序表链表
存储空间上物理上一定连续逻辑上连续,但物理上不一定连续
随机访问支持O(1)不支持;O(N)
任意位置插入或删除元素可能需要搬移元素,效率低O(N)只需要修改指针指向
插入动态顺序表,空间不够时需要扩容没有容量的概念
应用场景元素高效存储+频繁访问任意位置插入和删除频繁
缓存利用率

image-20230102173046349

数据结构比如顺序表和链表都是在内存里的,当CPU读取数据时,由于内存的读写速度相比CPU过慢,因此设置了缓存和寄存器进行快速的读写,当然速度越快,存储的空间大小越小,CPU读取数据时,会从读写速度最快的寄存器读写,寄存器又是从一级缓存读取数据,如果寄存器每次只读取一个数据,那么效率太低,因此寄存器每次会根据第一个数据的地址连续的读取几个数据(多少取决于CPU),顺序表地址是连续的,因此寄存器不用频繁的从缓存中读取数据,但是链表地址一般不连续,导致寄存器频繁的读取数据,因此顺序表的缓存利用率高,链表的缓存利用率低。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

好想写博客

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值