数据结构——单链表

发布于:2024-12-22 ⋅ 阅读:(12) ⋅ 点赞:(0)

上一篇中,我们已经了解过了顺序表了,但是,顺序表有哪些不便的地方?

一.顺序表的缺陷问题:

1. 中间/头部的插入删除,时间复杂度为O(N)

头插N个数据时间复杂度:O(N^2)

需要挪动数据,效率低下

解释:因为头插要将原本所有的数都向后移动一位
尾插N个数据时间复杂度:O(N)
所以尽量避免去使用头插。

2. 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。

3. 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。

对此,我们为了解决上述问题,链表就大大的解决了这些困扰。

正文开始:

链表

介绍

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

可以先简单理解成下图:

物理结构
实实在在数据在内存中的变化
下面的是逻辑结构,为了方便理解,形象化画出来的‘

原理:上一个节点要存下一个节点的地址

 

我们可以看出它的地址相同的部分知道,它们是一个又一个地连在一起。

 

后面画图更简便就成了下图: 

构造: 

typedef int SLTDataType;   为了后面一眼就能看到它的类型的意思

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

 解读:struct SListNode* next;通过指针链接起来。

 

 首先,我们先区分下面可能出现的问题吧:

指针类型是什么?
结构体指针
结构体肯定不能嵌套结构体
相当于自己里面一个我自己,这就造成无穷套娃了
但是可以结构体里面嵌套一个结构体指针,只是这个类型是结构体
是结构体
结构体没有执行的概念,函数才讲执行
因为编译的寻找规则:从上面找,
struct SListNode* next
不能:SListNode* next,虽然你在下面自定义了,但编译器只能往上找啊,找不到,就显示错误了。

初始化问题

对比:我们在顺序表时,为什么要进行初始化呢?

因为在顺序表时,我们是实实在在地开辟了一个空间(已经开辟好了的),来供后续使用,因此需要来进行初始化这块空间。

但是在链表中呢?

在链表中,我们是当要使用了,才会链接成新的结点,此时新结点的空间才会开辟使用,这时候就不用额外初始化了。

打印部分: 

我们知道顺序表是通过数组历遍打印出来的。

如:


void SLPrint(SL* ps)
{
	assert(ps);
	for (int i = 0; i < ps->sz; i++)
	{
		printf("%d ", ps->a[i]);
	}
	printf("\n");
}

链表中打印与顺序表不同:

void SLTPrint(SLTNode* phead)
{
	SLTNode* cur = phead;
	//while (cur->next != NULL)
	//while(cur != NULL)         这几种都是一样的意思
	while (cur)
	{
		printf("%d->", cur->data);
		cur = cur->next;
		这里可不能使用cur++;
	}
	printf("NULL\n");
}

易错:
链表打印当中(如左图),想要从1->2,可不能直接cur++。
因为链表的每一个节点都是单独malloc出来的,你只有一次mallooc很多个(如10个),能保证它的空间是连续的才可,而链表不能保证它连续,
有人又说我们让它连续不就好了吗?
你想想这样的话它不就变成了数组了吗?顺序表

因此!

要cur指向2做法:就cur=cur->next
解释:cur是一个结构体指针,加->这个符号就是结构体的内容,next是结构体成员
next是下一个节点的地址,赋值后就去到了2的地址位置,依次去到最后位置
注意:
这里while循环是不能:while(cur->next !=NULL)
这里到了最后一个数据时,为空了,最后一个数据就不能打印了
所以应该是while(cur!=NULL),也可以while(cur),因为0就是假,非0就是真
空就是0.

同时,我们会发现,为什么,链表的打印部分不用断言,顺序表那边需要断言。这与我们之前学到认为指针,基本都需要断言的认知是错误的:

下面给出解释:

答案:
链表:这个指针是指向第一个存有数据的节点
顺序表,指针指向一个结构体,而数据不是存在结构体上面的
如果有数据,则它存在的是这样一个空间上面(下图)

链表(下图) 

对于链表为空,那么phead就为空,但,对于顺序表为空,那么这个指针(指向结构体)不能为空,哪怕这个顺序表一个数据都没有,那么这个结构体也是有的,只是你这个a为不为空也不重要,它到底有没有数据,取决于size是不是0,如0就没有数据,所以有没有空间都不是最重要的 

因此!!!我们可以得出:!!所以不要看到指针就写断言!!!!要按照具体需求来判断。

 创建新结点

因为,下面写道的尾插,尾删,头插,头删,中间位置插,删都用到新结点的内容,为了让后面的代码更简便,我们在这里创建一个独立的函数,后面直接调用就可以了。

SLTNode* BuySLTNode(SLTDataType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		return NULL;
	}

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

	return newnode;
}

 图解:将x赋值到newnode中,再将newnode的下一个置空

 

尾插部分:

void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
	SLTNode* newnode = BuySLTNode(x);

	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	else
	{
		// 找尾
		SLTNode* tail = *pphead;
		while (tail->next != NULL)
		{
			tail = tail->next;
		}

		tail->next = newnode;
	}
}

它的过程分析如下:

首先把你要尾插的值放到新的节点;

接着为了后面方便直接找到头节点,所以不使用它本身来找尾。因此重新弄了个tail的指针,开始位置也再*pphead那里

然后,开始正式找尾,以tail的下一个为准,若下一个为空指针,则说明已经找到尾。就跳出循环,把新节点和尾巴连接在一起。若不为空,就把tail跳到tail的下一个,依次判断。

 

 注意易错的点:

链表的连接一定是:上一个节点要存下一个节点的地址
尾插的细节
本质:原尾结点中要存储新的尾结点的地址
while(nur !=NULL)
{
  tail=tail->next;
}
tail=newnode    错误的

这样就链接不上去了

(图解析看上面)
正确是tail->next !=NULL
tail->next=newnode

尾插的时候要改变的是结构体,要改变结构体就得改变结构体的成员,结构体的成员也是一个指针而已,但是有了结构体的指针已经能够改变它的成员了

另外,我们为什么传的是二级指针呢?

 我们先了解:

形参的改变不影响实参
非常实用的理解方式:
改变的是int,使用的就是int的指针
改变的是int*,使用的就是int*的地址,int**指针
可以这样理解:
不改变实参就直接传,改变就传地址

而我们现在的,就是要改变结构体指针里面的值,所以要双指针。

现在我们来分析分析:

我们可以看出,形参的改变不影响实参,出去了之后就销毁了。就无法回到mian函数

再看我们 如果这样?

 

通过使用它的地址,解引用保存了,就可以不被因为销毁,而不能达到目的。 

 

有了以上的认识后,我们可以解释本次的问题了:

 

 头插部分:

传二指针的问题,跟上面一样。

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

现在来分析它的过程:

先把原本的头赋值给新头的next。

接着再将plist指向newnode

 

尾删部分 

void SLTPopBack(SLTNode** pphead)
{
	// 暴力检查
	assert(*pphead);

	// 温柔的检查
	//if (*pphead == NULL)
	//	return;

	// 1、只有一个节点
	// 2、多个节点
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		// 找尾:第一种方法
		//SLTNode* prev = NULL;
		//SLTNode* tail = *pphead;
		//while (tail->next != NULL)
		//{
		//	prev = tail;
		//	tail = tail->next;
		//}

		//free(tail);
		//tail = NULL;

		//prev->next = NULL;
        第二种方法:

		SLTNode* tail = *pphead;
		while (tail->next->next != NULL)
		{
			tail = tail->next;
		}

		free(tail->next);
		tail->next = NULL;
	}	
}

它的过程分析:

 先创建一个prev(为NULL),tail(初始在头位置)

接着就是找尾,标志为:tail的下一个是否为0;若是则尾,不是就继续向右移动

最后找到了之后,将tail(即尾巴释放)

除此之外,我们还要释放prev的下一个为空。

为什么呢?有人会问,它不是tail已经为空了吗?

但是,你想一想,你都已经释放掉tail了,你还弄空,还有用吗?这不就相当于下面这种情况了吗?

 这就是为什么我们在开始之前,要再定义一个prevx新指针。

释放prev的下一个为空。就成了下面的图解释的那样了。

 

第二种方法:图解

 

 

头删部分 

void SLTPopFront(SLTNode** pphead)
{
	// 暴力检查
	assert(*pphead);

	// 温柔的检查
	//if (*pphead == NULL)
	//	return;

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

这部分的原理有了上面的铺垫,也是比较简单的(相对上面的 )就不具体分析了,给下图,应该能够明白的!

 

 查找部分:

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

		cur = cur->next;
	}

	return NULL;
}

这一部分没有什么好解释的。

插入任意中间的位置部分

其中又分为两种情况:

1.pos之前插入

void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pos);
	assert(pphead);

	if (pos == *pphead)
	{
		SLTPushFront(pphead, x);
	}
	else
	{
		// 找到pos的前一个位置
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}

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

 它的过程分析:

先在原本头位置设定一个prev指针

接着找到pos的前一个位置

 然后找到后,就当前prev位置的下一个指针指向newnode

newnode指向pos位置,就完成了中间插。 

2.在pos之后插入

void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
	assert(pos);
	SLTNode* newnode = BuySLTNode(x);
	newnode->next = pos->next;    | 这两步可不能调乱
	pos->next = newnode;          |
}

为什么不能调乱呢?请看下面

 

如果调乱了,这第一步就不是把2-3的箭头去掉了吗,那后面的地址(next)就找不到了,连不到一起了 。

由代码量就可以看出来,这种方法更加简单

 

删除任意中间的位置部分 

分为两部分

1.pos位置前删除

void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead);
	assert(pos);
	//assert(*pphead);

	if (*pphead == pos)
	{
		SLTPopFront(pphead);
	}
	else
	{
		// 找到pos的前一个位置
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}

		prev->next = pos->next;
		free(pos);
		//pos = NULL;
	}
}

它的分析过程:

 

 

 

2.pos位置后删除

void SLTEraseAfter(SLTNode* pos)
{
	assert(pos);
	assert(pos->next);

	//SLTNode* del = pos->next;
	//pos->next = pos->next->next;
	//free(del);
	//del = NULL;

	SLTNode* del = pos->next;
	pos->next = del->next;
	free(del);
	del = NULL;
}

它的分析过程图: 

最后一个销毁部分

方法一:
void SLTDestory(SLTNode** pphead)
{
  assert(pphead);
  SLTNode* cur=*pphead;
  while(cur)
  {
    SLTNode* temp=cur->next;
    free(cur);
    cur=temp;
 
  }
    *pphead=NULL;
}
方法二:在调用时使用返回方式。
void SLTDestory(SLTNode* phead)
{
  
  SLTNode* cur=phead;
  while(cur)
  {
    SLTNode* temp=cur->next;
    free(cur);
    cur=temp;
 
  }



 

它的过程分析: 

 

 

易错:这里可不能

 

 

至此,所有的就已经结束了。

附上所有的代码:

 SLT.C

#define _CRT_SECURE_NO_WARNINGS 1
#include "SLT.h"

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

SListNode* BuySLTNode(SLTDatatype x)
{
	SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		return;
	}
	newnode->data = x;
	newnode->next = NULL;
	return newnode;
}
void SLTPushBack(SListNode** pphead,SLTDatatype x)
{
    assert(pphead);
	SListNode* newnode = BuySLTNode(x);
	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	else
	{
		//找尾
		SListNode* tail = *pphead;
		while (tail->next != NULL)
		{
			tail = tail->next;
		}
		tail->next = newnode;

	}
}

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

//尾删
void SLTPopBack(SListNode** pphead)
{
    assert(pphead);
	assert(*pphead);
	//只有一个节点
	// 有多个节点
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	//找尾
	else
	{
		SListNode* tail = *pphead;
		SListNode* prev = NULL;

		while (tail->next != NULL)
		{
			prev = tail;
			tail = tail->next;
		}
		free(tail);
		tail = NULL;
		prev->next = NULL;

	}
}

//头删
void SLTPopFront(SListNode** pphead)
{
    assert(pphead);
	SListNode* frist = *pphead;
	*pphead = frist->next;
	free(frist);
	frist = NULL;
	
}

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

//内插
//在pos之前插
void SLTInsert(SListNode** pphead, SListNode* pos, SLTDatatype x)
{
    assert(pos);
	assert(pphead);
	if (pos == *pphead)
	{
		SLTPushFront(pphead, x);
	}
	else
	{
		//找到pos之前的数字
		SListNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		//创建一个新的空间
		SListNode* newnode = BuySLTNode(x);
		prev->next = newnode;
		newnode->next = pos;

	}
}

//pos位置删除
void SLTErase(SListNode** pphead, SListNode* pos)
{
	assert(pos);
	assert(pphead);
	if (*pphead == pos)
	{
		SLTPopFront(pphead);
	}
	else
	{
		//找到pos之前的位置
		SListNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		prev->next = pos->next;
		free(pos);
		pos = NULL;
	}
	
}
//pos后面插入
void SLTInsertAfter( SListNode* pos, SLTDatatype x)
{
    assert(pos);
	SListNode* newnode = BuySLTNode(x);
	newnode->next = pos->next;
	pos->next = newnode;

}

//pos之后删除
void SLTEarseAfter(SListNode* pos)
{
    assert(pos);
	assert(pos->next);
	SListNode* del = pos->next;
	pos->next = del->next;
	free(del);
	del = NULL;
	

}
void SLTDestory(SLTNode** pphead)
{
  assert(pphead);
  SLTNode* cur=*pphead;
  while(cur)
  {
    SLTNode* temp=cur->next;
    free(cur);
    cur=temp;
 
  }
    *pphead=NULL;
}

头文件SLT.h 

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

typedef int SLTDatatype;

typedef struct SListNode
{
	SLTDatatype data;
	struct SListNode* next;
}SListNode;


void SLTPrint(SListNode* phead);
//ͷ
void SLTPushBack(SListNode** phead,  SLTDatatype x);
void SLTPushFront(SListNode** pphead, SLTDatatype x);
void SLTPopBack(SListNode** pphead);
void SLTPopFront(SListNode** pphead);
SListNode* SLTFind(SListNode* phead, SLTDatatype x);
void SLTInsert(SListNode** pphead, SListNode* pos, SLTDatatype x);
void SLTErase(SListNode** pphead, SListNode* pos);
void SLTInsertAfter( SListNode* pos, SLTDatatype x);
void SLTEarseAfter(SListNode* pos);

test.c:这一部分只是用做测试,没太大作用。

#define _CRT_SECURE_NO_WARNINGS 1
#include "SLT.h"
Test1()
{
	SListNode* s=NULL;
	SLTPushBack(&s, 1);
	SLTPrint(s);
	SLTPushBack(&s, 2);
	SLTPrint(s);
	SLTPushBack(&s, 3);
	SLTPrint(s);
	SLTPushBack(&s, 4);
	SLTPrint(s);

}
Test2()
{
	SListNode* s = NULL;
	SLTPushBack(&s, 1);
	SLTPushBack(&s, 2);
	SLTPushBack(&s, 3);
	SLTPushBack(&s, 4);
	SLTPrint(s);
	SLTPushFront(&s, 5);
	SLTPushFront(&s, 6);
	SLTPushFront(&s, 7);
	SLTPushFront(&s, 8);
	SLTPrint(s);
	SLTPopBack(&s);
	SLTPrint(s);
	SLTPopBack(&s);
	SLTPrint(s);
	SListNode* ret = SLTFind(s, 4);
	SLTInsert(&s, ret, 9);
	SLTPrint(s);

}
Test3()
{
	SListNode* s = NULL;
	SLTPushBack(&s, 1);
	SLTPushBack(&s, 2);
	SLTPushBack(&s, 3);
	SLTPushBack(&s, 4);
	SLTPrint(s);

	SListNode* ret = SLTFind(s, 4);
	SLTInsert(&s, ret, 9);
	SLTPrint(s);

}

Test4()
{
	SListNode* s = NULL;
	SLTPushBack(&s, 1);
	SLTPushBack(&s, 2);
	SLTPushBack(&s, 3);
	SLTPushBack(&s, 4);
	SLTPrint(s);

	SListNode* ret = SLTFind(s, 4);
	//SLTErase(&s, ret);

	SLTInsertAfter(ret, 6);
	SLTPrint(s);
}
int main()
{
	Test4();

	return 0;
}

今次鸡汤:

对未来的真正慷慨,是把一切都献给现在,

你和我!一起奋斗吧!会越来越好的!