数据结构——单链表

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

一.顺序表的缺陷问题:

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

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

需要挪动数据,效率低下

解释:因为头插要将原本所有的数都向后移动一位

尾插N个数据时间复杂度:O(N)

所以尽量避免去使用头插。

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

  2. 增容一般是呈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就没有数据,所以有没有空间都不是最重要的

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

创建新结点

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

cs 复制代码
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的下一个置空

尾插部分:

cs 复制代码
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函数

再看我们 如果这样?

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

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

头插部分:

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

cs 复制代码
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
	SLTNode* newnode = BuySLTNode(x);
	newnode->next = *pphead;
	*pphead = newnode;
}

现在来分析它的过程:

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

接着再将plist指向newnode

尾删部分

cs 复制代码
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的下一个为空。就成了下面的图解释的那样了。

第二种方法:图解

头删部分

cs 复制代码
void SLTPopFront(SLTNode** pphead)
{
	// 暴力检查
	assert(*pphead);

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

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

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

查找部分:

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

		cur = cur->next;
	}

	return NULL;
}

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

插入任意中间的位置部分

其中又分为两种情况:

1.pos之前插入

cs 复制代码
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之后插入

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

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

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

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

删除任意中间的位置部分

分为两部分

1.pos位置前删除

cs 复制代码
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位置后删除

cs 复制代码
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;
}

它的分析过程图:

最后一个销毁部分

cs 复制代码
方法一:
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

cs 复制代码
#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

cs 复制代码
#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:这一部分只是用做测试,没太大作用。

cs 复制代码
#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;
}

今次鸡汤:

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

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

相关推荐
工一木子2 分钟前
【Leecode】Leecode刷题之路第82天之删除排序链表中的重复元素II
java·数据结构·算法·leetcode·链表
Eshin_Ye10 分钟前
transformer学习笔记-自注意力机制(2)
笔记·学习·transformer·示例代码
Amor风信子22 分钟前
华为OD机试真题---机房布局
java·开发语言·数据结构·算法·华为od
musir129 分钟前
数据结构基本概念、关系,算法
数据结构·算法
垂杨有暮鸦⊙_⊙34 分钟前
高等动力学期末复习试题(按章节分)
笔记·学习·高等动力学
不如语冰35 分钟前
pytorch学习笔记汇总
人工智能·pytorch·笔记·python·深度学习·神经网络·学习
scdifsn1 小时前
动手学深度学习11.1. 优化和深度学习-笔记&练习(PyTorch)
pytorch·笔记·深度学习·深度学习优化
麻瓜老宋1 小时前
C语言之波兰表达式的交互运行
c语言·开发语言·数据结构·算法
杨浦老苏1 小时前
开源的个人笔记系统TriliumNext
笔记·docker·开源·群晖
SchrodingerSDOG1 小时前
算法刷题Day18: BM41 输出二叉树的右视图
数据结构·python·算法