顺序表 单链表
叙述单链表之前我想先小结一下顺序表的一些缺陷。
- 中间/头部的插入删除,时间复杂度为O(N)
- 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
- 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。
链表的概念及结构:
概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的
上图感受一下
我们可以很直观的看出,链表的关系,逻辑上的关系,都是有指针连接起来的,但是他们的物理内存却和顺序表不太相同,由于他们的空间都是在堆上动态开辟的,所以是按照一定的策略分配的,并不是连续的。
链表的种类有很多,但我们主要叙述**”无头单向非循环链表“**该链表用的最多
准备工作
在叙述之前还是老样子 我们先把函数的声明写一下,怕待会儿遗漏掉。
```cpp
//SList.h
#pragma once//原因可以去顺序表最前面找 保准文件只被执行一次
#include<string.h>
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
typedef int SLTDataType;//所以我们链表中的LTDataType都可以不止看作整形,只是整形方便叙述
//先定义链表的结构体
typedef struct SListNode
{
SLTDataType data;
struct SListNode* next;
}SLN;//我不愿意写那么长的结构体,所以下面都有SLN替代
SLN* BuySListNode(SLTDataType x);//动态申请一个节点
void SListPrint(SLN* phead);//单链表的打印
void SListPushBack(SLN** pphead, SLTDataType x);//单链表尾插
void SListPushFront(SLN** pphead, SLTDataType x);//单链表头插
void SListPopback(SLN** pphead);//单链表尾删
void SListPopfornt(SLN** pphead);//单链表头删
SLN* SListFine(SLN* phead, SLTDataType x);//单链表查找
void SListInsert(SLN** pphead, SLN* pos, SLTDataType x);//在pos位置之前插入
void SListErase(SLN**pphead,SLN* pos);//删除pos位置
void SListInsertAfter( SLN* pos, SLTDataType x);//在pos之后插入
void SListEraseAfter(SLN* pos);//删除pos位置后面的值
void SListDestory(SLN** pphead);//但单链表的销毁
注意注意
上述函数有的传入的是二级指针,有的传入的是一级指针,这其中与函数有没有可能改变头结点有关,如果函数能改变头节点,那么应该传入二级指针,这个时候传入pphead时 phead表示的就是头节点,具体为啥可以思考一下传值调用,传址调用*
//test.c 这是执行文件
#include"SList.h"
int main()
{
SLN* plist = NULL;//初始化单链表,空指针即可
return 0;
}
链表的初始化要和顺序表区别开:
顺序表要知道是结构体内数组的一部分,但是单链表每个节点都是一个结构体,所以我们初始化顺序表的时候是吧结构体中的指针赋值NULL,其他部分size(顺序表元素个数)以及capacity(顺序表的容量)赋值为0;而链表我们刚开始有一个指针就可以了,链表后面的部分,我们可以通过next指针连接
要知道链表都是动态开辟的,你需要一个链表就动态开辟一个,因此
SLN* BuySListNode(SLTDataType x)//动态申请一个节点
{
SLN* ret = (SLN*)malloc(sizeof(SLN));
if (ret == NULL)
{
printf("mallpc fail\n");
exit(-1);
}
ret->data = x;
ret->next = NULL;
return ret;
}
为了我们检测各个函数,我们先写链表的打印
void SListPrint(SLN* phead)//单链表的打印
{
while (phead)
{
printf("%d->", phead->data);
phead = phead->next;
}
printf("NULL\n");
}
除增删查找外还有链表的摧毁
void SListDestory(SLN** pphead)//但单链表的销毁
{
assert(pphead);
SLN* cur = *pphead;
while (cur)
{
SLN* next = cur->next;
free(cur);
cur = next;
}
*pphead = NULL;
}
上述三个函数都是基本操作。
稍微说一下啊,我说的函数顺序一般是可以让我们边写边测试的,否则写两百行代码运行一下报错不太好。
增
头插 尾插 在pos位置之前插入 在pos位置之后插入
头插
void SListPushFront(SLN** pphead, SLTDataType x)//单链表头插
{
assert(pphead);
SLN*cur = BuySListNode(x);//先不说其他的,把节点内存分配好
if (*pphead == NULL)
*pphead = cur;
else
{
SLN* next = *pphead;
cur->next = next;
*pphead = cur;
}
}
尾插:
void SListPushBack(SLN** pphead, SLTDataType x)//单链表尾插
{
assert(pphead);//断言一下二级指针,虽然我们传入的*pphead(指向头节点的指针)有可能是空指针
//但是指针的指针也就是指针的地址如果是空的话那就可以直接报错了
SLN* cur = BuySListNode(x);//动态开辟一个指针
SLN* ret = *pphead;
if (*pphead == NULL)
{
*pphead = cur;
}
else
{
while (ret->next)
{
ret = ret->next;
}
ret->next = cur;
}
// 错误写法,这里没有链接起来
/*SListNode* rer = *pphead;
while (ret != NULL)
{
ret = ret->next;
}
ret = newnode;*/
//注意:链表一般都是用next赋值 因为next才能找到地址,如果是上述的错误做法,tail只是一个临时变量,出栈了的话
//就会回收内存 所以我们入宫想尾插,就必须用next
}
在写下一个函数之前我想加深一下assert的理解
assert断言,断言的内容就是里面*绝对不可以发生的,绝对不可以发生的
在pos位置之前插入一个数字
void SListInsert(SLN** pphead, SLN* pos, SLTDataType x)//在pos位置之前插入
{
assert(pphead);
assert(pos);//如果这个pos位置是NULL的话,根本找不到
//分析:0结点不可能,所以我们分析一下一个节点与多个结点
//一个节点
if (*pphead == pos)
{
//就是头插,直接把头插代码提过来
SListPushFront(pphead, x);
}
else
{
SLN* cur = BuySListNode(x);
SLN* ret = *pphead;
while (ret->next != pos)
{
ret = ret->next;
}
cur->next = pos;
ret->next = cur;
}
}
在pos之后插入一个数字
void SListInsertAfter(SLN* pos, SLTDataType x)//在pos之后插入
{
assert(pos);
SLN* cur = BuySListNode(x);
cur->next = pos->next;//这两个顺序一定不能返
pos->next = cur;
}
删
头删 尾删 删除pos位置的值
头删:
void SListPopfornt(SLN** pphead)//单链表头删
{
assert(pphead);
//分析:这个也涉及到一个结点与多个结点
//一个结点
if (*pphead == NULL)
{
return;
}
else
{
SLN* ret = *pphead;
*pphead = ret->next;
free(ret);
ret = NULL;
}
}
尾删:
void SListPopback(SLN** pphead)//单链表尾删
{
assert(pphead);
//一个结点
if (*pphead == NULL)
return;
else
{
SLN* ret = *pphead;
while (ret->next->next != NULL)
{
ret = ret->next;
}
SLN* next = ret->next->next;
ret->next = NULL;
free(next);
next = NULL;
}
}
删除pos位置的值:
void SListErase(SLN** pphead, SLN* pos)//删除pos位置
{
assert(pphead);
assert(pos);
//如果pos的位置和头结点一样,那么就是头删
if (*pphead == pos)
{
SListPopforn(pphead);
}
else
{
SLN* ret = *pphead;
while (ret->next != pos)
{
ret = ret->next;
}
ret->next = ret->next->next;
free(pos);
pos = NULL;//可要可不要 习惯保持好点就加上吧!
}
}
删除pos位置后的值
void SListEraseAfter(SLN* pos)//删除pos位置后面的值
{
assert(pos);
SLN* next = pos->next;
if (next)
{
/*next = next->next;
free(pos->next);*///错误写法,next只是一个临时变量,出了栈区就被销毁了,所以没什么用,应该用pos->next
pos->next = next->next;
free(next);
next = NULL;
}
}
小结:
不管是删除还是插入与pos有关的值,我们改变的时候都应该加上pos,不能通过赋值的方式改变,典型错误就是上述函数的”删除pos后的元素以及尾*插!
查 找
SLN* SListFine(SLN* phead, SLTDataType x)//单链表查找
{
SLN* ret = phead;
while (ret)
{
if (ret->data == x)
return ret;
else
ret = ret->next;
}
return NULL;
}
给个赞!铁子!