目录
前情回顾
顺序表:数据存在一个连续的物理空间中
优势:
- 物理空间连续
- 下标的随机访问
劣势:
- 空间不够,需要扩容。扩容有一定性能消耗,其次一般扩容2倍,会存在一些空间浪费。
- 头部或者中间位置的插入删除效率低下(只有尾插尾删效率比较高)
有什么改善方式呢?
那我们就需要链表结构来进行改善。
1.链表
1.1 链表的概念及结构
概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表
中的指针链接次序实现的 。
- 按需申请释放空间(链表可以将数据存放在单独空间中)
- 头部或者中间需要插入删除,不需要挪动数据
用一个指针指向第一个结点(头指针或头结点),依次往下指,遇见空指针截止
链表的是针对顺序表的劣势去设计出来的
示例一个链表的基本结构
SList.h
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int SLDataType;
struct SListNode
{
SLDataType data;
struct SListNode* next;//指向下一个结点的指针
};
给了四个结点,如何链接?
SList.h
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int SLDataType;
typedef struct SListNode
{
SLDataType data;
struct SListNode* next;//指向下一个结点的指针
}SLTNode;
Test.c
#include "SList.h"
//输入一些值,创建一个链表
void TestSList1()
{
SLTNode* n1 = malloc(sizeof(SLTNode));
assert(n1);
SLTNode* n2 = malloc(sizeof(SLTNode));
assert(n2);
SLTNode* n3 = malloc(sizeof(SLTNode));
assert(n3);
SLTNode* n4 = malloc(sizeof(SLTNode));
assert(n4);
n1->data = 1;
n2->data = 2;
n3->data = 3;
n4->data = 4;
n1->next = n2;
n2->next = n3;
n3->next = n4;
n4->next = NULL;
}
int main()
{
TestSList1();
return 0;
}
从图片上来看,逻辑结构:箭头只是形象的说法
代码写法:
n1->next = n2;
n2->next = n3;
n3->next = n4;
n4->next = NULL;
n1……n4是结构体指针,指针存的是地址,指向的是一个结构体的地址
结构体指针通过箭头访问到next
物理结构:在内存中的存储形式的体现
第一个结点存第二个结点的地址->第二个结点存第三个结点的地址->……->第四个存NULL的地址
1.2链表的实现
以下具体分析一些部分难点
1.2.1如何写打印
SList.c
#include "SList.h"
void SListPrint(SLTNode* phead)
{
SLTNode* cur = phead;
while ( cur != NULL)
{
printf("%d->", cur->data);
cur = cur->next;//取指向结构体中的next的值 赋值行为
}
printf("NULL\n");
}
next也是一个结构体中的指针
结构体的指针赋给结构体的指针
1.2.2如何写尾插
plist是一个实参,phead是一个形参
形参的改变不会影响实参
传地址解引用过去才能改变它
指针传参例子:
void f1(int a)
{
a = 1;
}
int main()
{
// TestSList2();
int x = 0;
f1(x);
printf("%d\n", x);//0
return 0;
}
void f1(int* p)
{
*p = 1;
}
int main()
{
// TestSList2();
int x = 0;
f1(&x);
printf("%d\n", x);//1
return 0;
}
注意:使用地址时光传值是不会发生变化的
void f2(int* p)
{
int* px = (int*)malloc(sizeof(int));
assert(px);
p = px;
}
int main()
{
// TestSList2();
int* ptr = NULL;
f2(ptr);
printf("%p\n", ptr);//00000000
return 0;
}
void f2(int** pp)
{
int* px = (int*)malloc(sizeof(int));
assert(px);
*pp = px;
}
int main()
{
// TestSList2();
int* ptr = NULL;
f2(&ptr);
printf("%p\n", ptr);//007B4CE0
return 0;
}
说明:要改变int,传int的地址,解引用改变
要改变int*,传int*的地址,解引用改变(指针的地址就是二级指针)
正确写法:
SList.c
void SListPushBack(SLTNode** pphead, SLTDataType x)
{
//要让上一个结点存新插入这个结点的地址才是尾插
//新结点要存NULL
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
assert(newnode);
newnode->data = x;
newnode->next = NULL;
if (*pphead == NULL)
{
*pphead = newnode;
}
else
{
//找尾结点(尾结点的特征是NULL)
SLTNode* tail = *pphead;
while (tail->next != NULL)
{
tail = tail->next;
}
tail->next = newnode;//改变的是结构体,使tail里面的next指针指向newnode
}
}
注意:改变plist的要使用二级指针
1.2.3头插、头删
链表只有一个结点的时候也可以删,删成空
但接着再删就有问题了。解决这种问题要使用空指针检查
温柔if或者暴力assert
小tips:断言在debug版本下才有用,release上断言会被忽略
1.2.4尾删
void SListPopBack(SLTNode** pphead)
{
assert(*pphead);
//1.只有一个结点
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
//2.有多个结点
else
{
//法1
/*SLTNode* tailPrev = NULL;
SLTNode* tail = *pphead;
while (tail->next != NULL)
{
tailPrev = tail;
tail = tail->next;
}
free(tail);
tailPrev->next = NULL;*/
//法2
SLTNode* tail = *pphead;
while (tail->next->next != NULL)
{
tail = tail->next;
}
free(tail->next);
tail->next = NULL;
}
}
一定不能为空的时候,要进行断言检查啊(检查的都是指针)
- 1.所有用二级指针的地方
- 2.具体情况具体分析
1.2.5在pos之后插入、删除
这里让pos指向newnode相当于pos重新指向一个新位置,与原链表之间断开,断开之后找不到d2
然后下一行代码相当于new弄得自己指向自己,找不到d2
注意顺序!
正确写法:
void SListInsertAfter(SLTNode* pos, SLTDataType x)
{
assert(pos);
SLTNode* newnode = BuySListNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
不在意顺序的写法:
void SListInsertAfter(SLTNode* pos, SLTDataType x)
{
assert(pos);
SLTNode* newnode = BuySListNode(x);
SLTNode* next = pos->next;
pos->next = newnode;
newnode->next = next;
}
1.3完整代码
SList.h
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int SLTDataType;
typedef struct SListNode
{
SLTDataType data;
struct SListNode* next;//指向下一个结点的指针
}SLTNode;
SLTNode* BuySListNode(SLTDataType x);
//打印
void SListPrint(SLTNode* phead);
//尾插
void SListPushBack(SLTNode** pphead, SLTDataType x);
//头插
void SListPushFront(SLTNode** pphead, SLTDataType x);
//尾删
void SListPopBack(SLTNode** pphead);
//头删
void SListPopFront(SLTNode** pphead);
//查找(附带修改
SLTNode* SListFind(SLTNode* phead, SLTDataType x);
// 在pos位置之前插入
void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
// 删除pos位置的值
void SListErase(SLTNode** pphead, SLTNode* pos);
//在pos之后插入
void SListInsertAfter(SLTNode* pos, SLTDataType x);
//单链表删除pos位置之后的值
void SListEraseAfter(SLTNode* pos);
SList.c
#include "SList.h"
void SListPrint(SLTNode* phead)
{
SLTNode* cur = phead;
while ( cur != NULL)
{
printf("%d->", cur->data);
cur = cur->next;//取指向结构体中的next的值 赋值行为
}
printf("NULL\n");
}
SLTNode* BuySListNode(SLTDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
assert(newnode);
newnode->data = x;
newnode->next = NULL;
return newnode;
}
void SListPushBack(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
//要让上一个结点存新插入这个结点的地址才是尾插
//新结点要存NULL
SLTNode* newnode = BuySListNode(x);
if (*pphead == NULL)
{
*pphead = newnode;
}
else
{
//找尾结点(尾结点的特征是NULL)
SLTNode* tail = *pphead;
while (tail->next != NULL)
{
tail = tail->next;
}
tail->next = newnode;//改变的是结构体,使tail里面的next指针指向newnode
}
}
void SListPushFront(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
SLTNode* newnode = BuySListNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
void SListPopBack(SLTNode** pphead)
{
assert(pphead);
assert(*pphead);
//1.只有一个结点
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
//2.有多个结点
else
{
//法1
/*SLTNode* tailPrev = NULL;
SLTNode* tail = *pphead;
while (tail->next != NULL)
{
tailPrev = tail;
tail = tail->next;
}
free(tail);
tailPrev->next = NULL;*/
//法2
SLTNode* tail = *pphead;
while (tail->next->next != NULL)
{
tail = tail->next;
}
free(tail->next);
tail->next = NULL;
}
}
void SListPopFront(SLTNode** pphead)
{
assert(pphead);
assert(*pphead != NULL);
/*if (*pphead == NULL)
return;*/
//先把下一个结点用next保存起来再使用free,然后再指向下一个结点
SLTNode* next = (*pphead)->next;
free(*pphead);
*pphead = next;
}
SLTNode* SListFind(SLTNode* phead, SLTDataType x)
{
SLTNode* cur = phead;
while (cur)
{
if (cur->data == x)
return cur;
cur = cur->next;
}
return NULL;
}
void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
assert(pos);
assert(pphead);
// 头插
if (pos == *pphead)
{
SListPushFront(pphead, x);
}
//不是头插,需要找到pos位置的前一个位置(不擅长)
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)//此处prev一定不为空
{
prev = prev->next;
}
SLTNode* newnode = BuySListNode(x);
prev->next = newnode;
newnode->next = pos;
}
}
void SListErase(SLTNode** pphead, SLTNode* pos)
{
//删除数据,还得知道前一个的位置
assert(pphead);
assert(pos);//不用考虑链表为空到状态
//头删
if (*pphead == pos)
{
SListPopFront(pphead);
}
else
{
//pos找到前一个
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
//pos = NULL;//几乎没有效果
//pos是形参,置空是为了防止野指针问题
}
}
void SListInsertAfter(SLTNode* pos, SLTDataType x)
{
assert(pos);
//SLTNode* newnode = BuySListNode(x);
//newnode->next = pos->next;
//pos->next = newnode;
SLTNode* newnode = BuySListNode(x);
SLTNode* next = pos->next;
pos->next = newnode;
newnode->next = next;
}
void SListEraseAfter(SLTNode* pos)
{
assert(pos);
if (pos->next == NULL)
return;
// pos->next = pos->next->next;//存在结点没有释放掉的问题
SLTNode* del = pos->next;
//pos->next = pos->next->next;
pos->next = del->next;
free(del);
del = NULL;//可写可无,但是是个好习惯
}
Test.c
#include "SList.h"
//输入一些值,创建一个链表
void TestSList1()
{
SLTNode* n1 = malloc(sizeof(SLTNode));
assert(n1);
SLTNode* n2 = malloc(sizeof(SLTNode));
assert(n2);
SLTNode* n3 = malloc(sizeof(SLTNode));
assert(n3);
SLTNode* n4 = malloc(sizeof(SLTNode));
assert(n4);
n1->data = 1;
n2->data = 2;
n3->data = 3;
n4->data = 4;
n1->next = n2;
n2->next = n3;
n3->next = n4;
n4->next = NULL;
SLTNode* plist = n1;
SListPrint(plist);
SListPushBack(&plist, 5);
SListPushBack(&plist, 6);
SListPushBack(&plist, 7);
SListPushBack(&plist, 8);
SListPrint(plist);
}
void TestSList2()
{
SLTNode* plist = NULL;
SListPushBack(&plist, 1);
SListPushBack(&plist, 2);
SListPushBack(&plist, 3);
SListPushBack(&plist, 4);
SListPushBack(&plist, 5);
SListPushBack(&plist, 6);
SListPushBack(&plist, 7);
SListPrint(plist);
SListPushFront(&plist, 0);
SListPrint(plist);
}
void f1(int* p)
{
*p = 1;
}
//void f2(int* p)
//{
// int* px = (int*)malloc(sizeof(int));
// assert(px);
// p = px;
//}
void f2(int** pp)
{
int* px = (int*)malloc(sizeof(int));
assert(px);
*pp = px;
}
void TestSList3()
{
SLTNode* plist = NULL;
SListPushFront(&plist, 0);
SListPushFront(&plist, 1);
SListPushFront(&plist, 2);
SListPushFront(&plist, 3);
SListPushFront(&plist, 4);
SListPrint(plist);
SListPopFront(&plist);
SListPopFront(&plist);
SListPopFront(&plist);
SListPopFront(&plist);
SListPopFront(&plist);
//SListPopFront(&plist);
SListPrint(plist);
}
void TestSList4()
{
SLTNode* plist = NULL;
SListPushBack(&plist, 1);
SListPushBack(&plist, 2);
SListPushBack(&plist, 3);
SListPushBack(&plist, 4);
SListPrint(plist);
SListPopBack(&plist);
SListPrint(plist);
SListPopBack(&plist);
SListPrint(plist);
SListPopBack(&plist);
SListPrint(plist);
SListPopBack(&plist);
SListPrint(plist);
//SListPopBack(&plist);
//SListPrint(plist);
}
void TestSList5()
{
SLTNode* plist = NULL;
SListPushBack(&plist, 1);
SListPushBack(&plist, 2);
SListPushBack(&plist, 3);
SListPushBack(&plist, 4);
SListPrint(plist);
SLTNode* ret = SListFind(plist, 3);
if (ret)
{
printf("找到了\n");
ret->data = 3;
}
SListPrint(plist);
SLTNode* pos = SListFind(plist, 3);
if (pos)
{
SListErase(&plist, pos);
}
SListPrint(plist);
}
int main()
{
TestSList5();
//int* ptr = NULL;
//f2(&ptr);
//printf("%p\n", ptr);//007B4CE0
return 0;
}
这里也可以给单链表添加一个菜单,自行添加即可,这里就不多加赘述。