本章重点
1.链表
顺序表的问题及思考
顺序表的优点:
- 顺序表中的元素在内存中是连续存储的,因此可以通过索引直接访问任意位置的元素。
- 顺序表尾插尾删操作实现简单。
问题:
- 中间/头部的插入删除,时间复杂度为O(N)
- 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
- 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到 200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。
思考:如何解决以上问题呢?下面给出了链表的结构来看看。
链表的概念
概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 。
特点: 链表由一系列节点(链表中每一个元素称为节点)组成,节点在运行时动态生成 (malloc),每个节点包括两个部分:
- 一个是存储数据元素的数据域:存放各种实际的数据
- 另一个是存储下一个节点地址的指针域:存放下一节点的首地址
链表的概念结构
2.链表的结合实现
(1)、动态申请一个结点:SLTNode* BuySListNode(SLTDataType x)
注意:我们这里只申请了结点,并没有进行连接,后续通过头插或者尾插进行连接。
// 动态申请一个结点
SLTNode* BuySListNode(SLTDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode*));
if (newnode == NULL)
{
perror("malloc");
exit(-1);
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
(2)、单链表尾插:void SListPushBack(SLTNode** pplist, SLTDataType x)
单链表如何尾插入?只需将尾插的结点的地址给到前一个结点的空白位置(也就是前结点->next)
问题:我们上面的代码中,tail
指针的位置不对,遍历寻找尾节点的循环没有正确地将新节点连接到尾节点的 next
上,通过遍历寻找尾结点,当 tail
为空时,由于上一个结点的 next
也为空,此时链接会造成对空指针的解引用操作,tail = newnode
,虽然 tail
被修改为 newnode 的值,但是上一个结点的 next
的值没有被修改为 newnode
的值,而不会影响链表本身。正确的做法是,要将新节点连接到前一个节点的 next
上,然后更新尾节点指针,让其指向空地址处。
如果我们刚开始一个结点也没有,我们就需要对链表为空作单独处理
上面的代码有什么问题吗?我们先来看一下交换两个指针变量需要怎么交换呢?
void Swap1(int *p1, int *p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
void Swap2(int** pp1, int** pp2)
{
int* tmp = *pp1;
*pp1 = *pp2;
*pp2 = tmp;
}
int main()
{
int a = 0, b = 1;
Swap1(&a, &b);//数据交换需要传入地址
int* p1 = &a, * p2 = &b;
Swap1(p1, p2);
//修改版本
Swap2(&p1, &p2);
return 0;
}
交换两个指针变量需要将指针变量的地址,也就是二级指针传入到函数参数,通过二级指针去找到指针变量从而去交换它们。传入指针变量只是在函数内部交换了,并没有交换原数据。所以我们上面写的代码并没有将plist指针修改,只在函数内部修改了pplist,出了函数pplis这个局部遍历就被释放了,plist仍然指向空地址处。
// 单链表尾插
void SListPushBack(SLTNode** pplist, SLTDataType x)
{
assert(pplist);
SLTNode* newnode = BuySListNode(x);
if (*pplist == NULL)
{
//改变的结构体指针,要传入二级指针
*pplist = newnode;//传入地址才能修改原数据
}
else
{
SLTNode* tail = *pplist;
while (tail->next != NULL)
{
tail = tail->next;
}
//改变结构体内容,用结构体的指着即可
tail->next = newnode;
//这里不用写newnode->next = NULL;该BuySListNode函数已经处理了
}
}
总结:要改变函数传入的参数,就需要传入要改变这个参数的地址。
- int ----- 传入int*
- int* ------- 传入int**
(3)、单链表的头插:void SListPushFront(SLTNode** pplist, SLTDataType x)
单链表的头插我们首先需要将头结点所指向的结点给到要插入结点的next位置(防止后面的结点丢失),然后再将要插入的结点的地址给到头结点。
// 单链表的头插
void SListPushFront(SLTNode** pplist, SLTDataType x)
{
assert(pplist);
SLTNode* newnode = BuySListNode(x);
newnode->next = *pplist;
*pplist = newnode;
}
(4)、单链表的尾删:void SListPopBack(SLTNode** pplist)
下面这样写有问题吗嘛?
有问题,我们发现将 tail
置为空后,但是3结点位置的 next
并没有置为空,那么就会出现野指针的问题。解决这个问题的关键就是将3结点位置的 next
置为空.
方法一:创建新结点,让这个结点的位置的 next
等于 tail
。
// 单链表的尾删
void SListPopBack(SLTNode** pplist)
{
assert(pplist);
//1.链表为空
assert(*pplist != NULL);
//2.链表只剩下一个元素
if ((*pplist)->next == NULL)
{
free(*pplist);
*pplist = NULL;
}
//3.链表有多个元素
else
{
SLTNode* tailPrev = NULL;
SLTNode* tail = *pplist;
while (tail->next!= NULL)
{
tailPrev = tail;
tail = tail->next;
}
free(tail);
tailPrev->next = NULL;
}
}
方法二:不创建新结点,使用 tail->next->next
找到3结点位置
// 单链表的尾删
void SListPopBack(SLTNode** pplist)
{
assert(pplist);
//1.链表为空
assert(*pplist != NULL);
//2.链表只剩下一个元素
if ((*pplist)->next == NULL)
{
free(*pplist);
*pplist = NULL;
}
//3.链表有多个元素
else
{
SLTNode* tail = *pplist;
while (tail->next->next != NULL)
{
tail = tail->next;
}
free(tail->next);
tail->next = NULL;
}
}
(5)、单链表头删:void SListPopFront(SLTNode** pplist)
单链表如何进行头删呢?头删需要找到头结点的 next
,
将其 next
用一个新结点 newhead
保存起来,然后释放原来的头结点,再将 newhead
赋给 pplist
即可。
// 单链表头删
void SListPopFront(SLTNode** pplist)
{
assert(pplist);
//空
assert(*pplist);
//非空
SLTNode* newhead = (*pplist)->next;
free(*pplist);
*pplist = newhead;
}
(6)、单链表查找:SLTNode* SListFind(SLTNode* plist, SLTDataType x)
要查找某个数字的在链表中的位置,需要遍历链表,让 tail指向空的位置,这样链表中的每个数据都能被访问到,就能查找的数字的在链表中的位置。
// 单链表查找
SLTNode* SListFind(SLTNode* plist, SLTDataType x)
{
SLTNode* cur = plist;
while (cur != NULL)
{
if (cur->data == x)
return cur;
cur = cur->next;
}
//没找到数据
return NULL;
}
(7)、单链表在pos位置之前插入x:void SListInsert(SLTNode** plist, SLTNode* pos, SLTDataType x)
我们这里设置plist为二级指针,因为pos位置可能会头插,需要改变头结点指向的结点。
// 单链表在pos位置之前插入x
void SListInsert(SLTNode** plist, SLTNode* pos, SLTDataType x)
{
assert(plist);
assert(pos);
if (pos == *plist)//头插
{
pos->next = *plist;
*plist = pos;
}
else//中间插入
{
//需要找到pos位置之前的结点
SLTNode* posPrev = *plist;
while (posPrev->next != pos)
{
posPrev = posPrev->next;
}
//申请一个结点
SLTNode* newnode = BuySListNode(x);
posPrev->next = newnode;
newnode->next = pos;
}
}
单链表在pos位置之前插入x,有两种情况,一种是头插,一种是在中间插入。头插可以复用之前的函数,但是注意传入的参数,中间插入的话首先要找到pos位置前一个结点posPrev,将posPrev位置的next指向要插入的结点,再将要插入的结点的next给到pos,即可完成连接。
(8)、单链表在pos位置之后插入x:void SListInsertAfter(SLTNode* pos, SLTDataType x)
在pos位置之后插入x不会出现头插的现象,所以这里我们不会改变plist,所以这个参数也就不用掺入了
// 单链表在pos位置之后插入x
void SListInsertAfter(SLTNode* pos, SLTDataType x)
{
assert(pos);
SLTNode* newnode = BuySListNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
(9)、删除pos位置的值:void SListErase(SLTNode* pos, SLTDataType x);
单链表删除pos位置的值,有两种情况,一种是头删,一种是在中间删除。头删可以复用之前的函数,但是注意传入的参数,中间删除的话首先要找到pos位置前一个结点posPrev,将posPrev位置的next指向pos位置的next,即可完成删除。
// 删除pos位置的值
void SListErase(SLTNode** plist, SLTNode* pos)
{
assert(plist);
assert(pos);
if (pos == *plist)
{
*plist = pos->next;
}
else
{
//需要找到pos位置之前的结点
SLTNode* posPrev = *plist;
while (posPrev->next != pos)
{
posPrev = posPrev->next;
}
posPrev->next = pos->next;
free(pos);
}
}
(10)、单链表删除pos位置之后的值:void SListEraseAfter(SLTNode* pos)
这个函数有个缺点:不能删头,同时pos为尾结点,那么此时删除pos位置之后的值就无意义
// 单链表删除pos位置之后的值
void SListEraseAfter(SLTNode* pos)
{
assert(pos);
//删除pos位置之后的值就无意义
if (pos->next == NULL)
exit(-1);
SLTNode* posNext = pos->next;
pos->next = posNext->next;
free(posNext);
}
这里我们为什么不写成pos->next = pos->next->next呢?为什么还要传教一个posNext结点呢?
因为如果我们写成pos->next = pos->next->next,那么删除的那个结点我们就丢失了,无法找到,就会早成内存泄露的问题。
(11)、单链表打印:void SListPrint(SLTNode* plist);
第一步:输出第一个节点的数据域,输出完毕后,让指针保存后一个节点的地址
第二步:输出移动地址对应的节点的数据域,输出完毕后,指针继续后移
第三步:以此类推,直到节点的指针域为NULL
// 单链表打印
void SListPrint(SLTNode* plist)
{
SLTNode* cur = plist;
while (cur != NULL)
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
哈哈哈