目录
1.链表
1.1概念及结构
链表,别名链式存储结构或单链表,用于存储逻辑关系为 "一对一" 的数据。与顺序表不同,链表不限制数据的物理存储状态,换句话说,使用链表存储的数据元素,其物理存储位置是随机的。
上一篇文章介绍了顺序表,然而顺序表是存在许多缺陷的,比如:每一次插入和删除数据,都会造成大量元素移动,导致时间的效率低下。为了改进顺序存储结构的缺点,引入了链式存储结构,即链表。但因为链式存储结构的存储单元不连续,所以需要通过指针来访问它的后续元素。
链表中的每个节点是一个结构体,其有两个成员,一个成员存储数据,被称为数据域,另一个成员存储指向下一个节点的指针,被称为指针域。如下图。
n个节点构成一个链表,即为线性表的链式存储结构。其逻辑结构我们认为是这样的,如下:
但是链表在内存中的实际存储情况却并不是如此,即其物理结构并不这样。由于链表的节点是需要的时候再在动态内存中开辟,所以节点在内存中的位置并不是连续的,而是随机的,如下:
1.2链表的实现
链表分为有哨兵位和无哨兵位的区别,如下图,p1指向有哨兵位的链表的哨兵节点,p2指向无哨兵位的链表的首节点。哨兵位不存储数据,只是起到一个过渡作用,但是实际应用中无哨兵位的链表使用比较多,所以本文主要介绍无哨兵位的链表:
1.2.1接口
一个完整的链表功能无疑是强大的,可以进行插入、删除、创建节点、查询、清空链表等等。在这里和顺序表的区别是,链表内除了打印这个接口,其他接口涉及到链表的都要传二级指针,原因下面会做出解释。
1.2.2创建链表
首先要创建一个链表,与顺序表同理,typedef int SLTDataType; 的原因是:为了方便更改链表中存储的数据类型。比如原本链表的数据域是存储int类型的数据,但是此时想要改成double类型的数据,只需要把 typedef int SLTDataType; 改成 typedef double SLTDataType; 就可以了。
同时,对于指针域。指针指向的类型是 struct SListNode ,如上面链表结构的图片,不难看出,指针域存储的是下一个节点的位置,而每一个节点的类型就是 struct SListNode,所以是这样。此外,指针域不能写成 SLTNode *next; 会导致出错。
typedef int SLTDataType;
typedef struct SListNode
{
SLTDataType data; //数据域
struct SListNode* next; //指针域
}SLTNode;
1.2.3创建新的节点
如下,开辟一个节点的空间,成功后,该节点数据与存储要添加的数据,指针域为空。返回指向新节点的指针,否则无法找到新节点。
SLTNode* SListBuySLTNode(SLTDataType x)
{
SLTNode* temp = (SLTNode*)malloc(sizeof(SLTNode));
if (temp == NULL)
{
printf("malloc error!");
exit(-1);
}
//数据域存储添加的数据,指针域为空
temp->data = x;
temp->next = NULL;
return temp;
}
1.2.4头插
由于是无哨兵位的节点,所以不需要初始化。直接进行插入、删除等操作即可。为什么要传二级指针,下面有解释。
void SListPushFront(SLTNode** phead, SLTDataType x)
{
assert(phead);
SLTNode* newnode= SListBuySLTNode(x);
newnode->next = *phead;
*phead = newnode;
}
如上图,假设原本链表的头节点叫A节点,新插入的头节点叫New节点。头插操作要先创建一个新节点,这个New节点用newnode指针接收,然后New节点的指针域要指向A节点,最后再把p指针指向New节点。在这里,p指针被修改了,要在函数内部修改传入的指针,就得传二级指针。
如下,如果传的一级指针,由于形参是对实参的一份临时拷贝,所以,如果把p指针当作实参传给SListPushFront函数,那么函数内部的指针变量phead,其指向的地址和p指针是一样的。但是如果修改phead,那么只是修改了形参里面的phead,使得形参不再指向A节点,但是函数调用结束之后,实参p指针依然不变。
但是,如果传的参数是二级指针,即传入 &p 。那么就可以对p指针进行修改。此时phead代表的是实参p的地址,phead指针指向的是实参p,即*phead就是p,所以在函数里面把新节点的指针域指向*phead。如果要修改p,那么 *phead=……; 就可以达到目的。
这就好比于普通的传参,假设有一个函数 test(int x,int y); 然后有 变量 int a=6, b=10; 调用函数test(a,b); 那么在这个函数运行之后,无论函数内部的x、y如何改变,变量a、b依然不变。但是如果是 test1(int *x,int*y); 调用函数test1(&a,&b); 此时在函数内部改变 *a、*b,那么a、b就会被改变。
把普通传参看成 test(int *x,int *y); int * a,* b; 调用函数test(a,b); 是无法改变a、b的,只有test1(int **x,**y); 调用函数 test1(&a,&b); 在函数内部更改*x 、*y,才会改变函数外部的a、b。
1.2.5尾插
尾插有两种情况,第一钟是链表无数据的情况,此时*phead指针指向NULL(即上图中的p指针指向NULL)。那么直接把*phead改成新生成的节点的地址即可。原理同上。
第二种情况是链表有数据的情况下,直接把新节点插入到最后一个节点后面,即把当前最后一个节点的指针域指向新节点。所以先找到最后一个节点的地址,然后把它的指针域改成新生成的节点地址即可。
void SListPushBack(SLTNode** phead, SLTDataType x)
{
//断言
assert(phead);
SLTNode* newnode = SListBuySLTNode(x);
SLTNode* temp = *phead;
//没有数据的情况下
if (*phead == NULL)
{
*phead = newnode;
}
//有数据的情况下
else
{
while (temp->next != NULL)
{
temp = temp->next;
}
temp->next = newnode;
}
}
1.2.6头删
头删也分为两种情况:第一种,链表为空,这种情况直接用assert()避免。
第二种,链表不为空。使原本指向第一个节点的指针,指向第二个节点即可。因为要改变指针,所以函数要传二级指针,和头插类似原理,如下图。当然直接*phead=(*phead)->next;也可以。
void SListPopFront(SLTNode** phead)
{
assert(*phead != NULL);
SLTNode* temp = *phead;
*phead = temp->next;
free(temp);
temp = NULL;
}
1.2.7尾删
尾删有三种情况:第一种,没有节点,也是用assert()来判断。
第二种,只有一个节点,此时直接free掉*phead指向的空间即可,在这里用的temp和*phead指向的地址相同,所以直接free(temp);也是同样的效果,然后置*phead=NULL;
第三种,链表中有多个节点。此时要删掉最后一个节点,那么应该提前找到倒数第二个节点,然后释放尾节点,并把倒数第二个节点的指针域指向NULL。重点就在于找到倒数第二个节点,只需要设置一个prev指针,使得这个指针比temp指针慢一个节点的位置即可,思想如下图。循环结束之后,prev就指向了倒数第2个节点。
看完第三种情况,就对为什么尾删要单独考虑只有一个节点的情况,而头删不需要考虑这种情况有所了解了。尾删要设置一个prev指针,当只有一个节点的情况下,prev指针 按照第三种情况的算法 无法指向任何一个节点,所以将其单独考虑。而头删则不用这样,如果只有一个节点,那么该节点指针域就是NULL,按照头删的算法,*phead指向NULL,然后第一个节点被释放,故不需要额外考虑。
void SListPopBack(SLTNode** phead)
{
//断言
assert(*phead != NULL);
SLTNode* temp = *phead;
//只有一个数据的情况下,free*phead且*phead要置成空
if (temp->next == NULL)
{
free(temp);
*phead = NULL;
}
//多个数据下,新的是最后一个数据的指针域要为空
else
{
SLTNode* prev = NULL;
while (temp->next != NULL)
{
prev = temp;
temp = temp->next;
}
free(temp);
prev->next = NULL;
}
}
1.2.8查询
相较于头删和尾删,查询数据的算法就非常容易了。首先考虑链表为空的情况,assert()判断。其次,如果不为空,遍历链表,找到对应数据则返回该指向节点的指针,遍历完了还是没有返回对应指针的话,那就没有找到,返回NULL。
//查询数据
SLTNode* SListFind(SLTNode* phead, SLTDataType x)
{
//断言
assert(phead != NULL);
SLTNode* temp = phead;
while(temp != NULL)
{
if (temp->data == x)
return temp;
else
temp = temp->next;
}
return NULL;
}
1.2.9在pos指针之前插入数据
这里在pos指针之前插入,而不是第几个数据之前插入或者在某个特定数据之前插入的原因是,查询数据这个功能返回值是该数据的指针,在调用这个函数之前,先查询目的节点的指针,然后直接调用这个函数。
这个接口函数要考虑到,查询接口如果没有找到数据,会返回NULL,所以一开始assert()直接判断,如果pos为假(即pos==NULL)就会报错。
如果pos是指向头节点,就相当于头插。
如果pos是指向其他节点,那么先找到该节点的前面一个节点prev,如下算法,while循环结束之后,prev->next == pos。此时要先让新节点的指针域指向pos,然后把prev节点的指针域指向新节点,如下图第一种插入方法。(当然有了这种插入算法,头插和尾插其实都可以被替代,因为它同时也具有这两种功能。)
这里就有一个有意思的小问题,为什么一定要先让新节点指针域指向pos的位置呢,不可以先让prev的指针域指向新节点嘛? 在这里,是可以的,因为函数第2个参数就是pos。但是,如果是在别的情况下,传入的参数不是pos,而是pos节点的数据,那么将prev->next指向新节点之后,我们就无法找到pos指向的节点了,链表在这里就断了,如下第二种插入方法。(当然,第二种插入方法,可以在prev->next指向新节点之前,设计一个新指针,指向prev->next,即指向pos,就相当于把pos位置存下来了,只是这样太麻烦,不如直接第一种插入方法)
//在pos位置之前插入数据
void SListInsert(SLTNode** phead, SLTNode* pos, SLTDataType x)
{
//断言,括号里的条件为假的时候报错,phead==NULL即假
assert(phead && pos);
SLTNode* newnode = SListBuySLTNode(x);
//pos是头节点的情况
if (*phead == pos)
{
newnode->next = *phead;
*phead = newnode;
}
else
{
SLTNode* prev = *phead;
while (prev->next != pos)
{
prev = prev->next;
}
newnode->next = pos;
prev->next=newnode;
}
}
1.2.10删除pos位置的数据
这里要分为三种情况,空链表,通过断言判断。
如果pos指向头节点,那么头节点被删除,链表为空,直接*phead指向pos->next(NULL),然后free(pos);即可。
除了以上两种情况,就得找到pos节点的前一个节点,用prev指向该节点,然后把该节点的指针域指向pos节点的后一个节点,即prev->next=pos->next。然后free(pos);
//在中间删除数据
void SListErase(SLTNode** phead, SLTNode* pos)
{
//空链表,断言
assert(*phead);
//删除头节点
if (pos == *phead)
{
*phead =pos->next;
}
else
{
SLTNode* prev = *phead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
}
free(pos);
}
2.链表的应用
2.1两条有序链表的归并
两条链表的归并。和顺序表的归并异曲同工,具体可见上一篇博客。
void merge(SLTNode* a, SLTNode* b, SLTNode** ret)
{
SLTNode* temp1 = a;
SLTNode* temp2 = b;
while (temp1 != NULL && temp2 != NULL)
{
if (temp1->data < temp2->data)
{
SListPushBack(ret, temp1->data);
temp1 = temp1->next;
}
else
{
SListPushBack(ret, temp2->data);
temp2 = temp2->next;
}
}
if (temp1)//temp2归并完了
{
while (temp1)
{
SListPushBack(ret, temp1->data);
temp1 = temp1->next;
}
}
else//temp1归并完了
{
while (temp2)
{
SListPushBack(ret, temp2->data);
temp2 = temp2->next;
}
}
return;
}
2.2 正负分离
输入N个数字,存在链表中,将这个链表中的正负元素分开,且保持相对位置不变。
这题做法也不难,创建一个有N个数据的数组,然后两次遍历链表,第一次找出小于0的,然后拷贝到新数组,第二次找出大于0的,拷贝到新数组。然后把新数组的数据拷贝到链表中即可。
//正负分开,负数在前面,正数在后面,相对位置不变
void test2(SLTNode** a)
{
//输入数据
int x = 0;
for (int i = 0;i < N;i++)
{
scanf("%d", &x);
SListPushBack(a, x);
}
//操作,arr存储数据
int count = 0;
SLTDataType arr[N] = { 0 };
SLTNode* temp = *a;
while (temp)
{
if (temp->data < 0)
arr[count++] = temp->data;
temp = temp->next;
}
temp = *a;
while (temp)
{
if (temp->data > 0)
arr[count++] = temp->data;
temp = temp->next;
}
//重新赋值
count = 0;
temp = *a;
while (temp)
{
temp->data = arr[count++];
temp = temp->next;
}
return;
}
3.源代码
头文件Slist.h
#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#define N 10
typedef int SLTDataType;
typedef struct SListNode
{
SLTDataType data;
struct SListNode* next;
}SLTNode;
//打印
void SListPrint(SLTNode* phead);
//创建新的单元
SLTNode* SListBuySLTNode(SLTDataType x);
//头插
void SListPushFront(SLTNode** phead,SLTDataType x);
//尾插
void SListPushBack(SLTNode** phead, SLTDataType x);
//头删
void SListPopFront(SLTNode** phead);
//尾删
void SListPopBack(SLTNode** phead);
//查询数据
SLTNode* SListFind(SLTNode* phead, SLTDataType x);
//在pos位置之前插入数据
void SListInsert(SLTNode** phead, SLTNode* pos, SLTDataType x);
//在中间删除数据
void SListErase(SLTNode** phead, SLTNode* pos);
//清空链表
void SListDestory(SLTNode** phead);
//归并
void merge(SLTNode* a, SLTNode* b, SLTNode** ret);
//正负分离
void test2(SLTNode** a);
接口文件SList.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"SList.h"
void SListPrint(SLTNode* phead)
{
//断言,无数据直接报错
assert(phead != NULL);
SLTNode* temp = phead;
while (temp != NULL)
{
printf("%d->", (*temp).data);
temp = (*temp).next;
}
printf("\n");
}
SLTNode* SListBuySLTNode(SLTDataType x)
{
SLTNode* temp = (SLTNode*)malloc(sizeof(SLTNode));
if (temp == NULL)
{
printf("malloc error!");
exit(-1);
}
//数据域存储添加的数据,指针域为空
temp->data = x;
temp->next = NULL;
return temp;
}
void SListPushFront(SLTNode** phead, SLTDataType x)
{
assert(phead);
SLTNode* newnode= SListBuySLTNode(x);
newnode->next = *phead;
*phead = newnode;
}
void SListPushBack(SLTNode** phead, SLTDataType x)
{
//断言
assert(phead);
SLTNode* newnode = SListBuySLTNode(x);
SLTNode* temp = *phead;
//没有数据的情况下
if (*phead == NULL)
{
*phead = newnode;
}
//有数据的情况下
else
{
while (temp->next != NULL)
{
temp = temp->next;
}
temp->next = newnode;
}
}
void SListPopFront(SLTNode** phead)
{
assert(*phead != NULL);
SLTNode* temp = *phead;
*phead = temp->next;
free(temp);
temp = NULL;
}
void SListPopBack(SLTNode** phead)
{
//断言
assert(*phead != NULL);
SLTNode* temp = *phead;
//只有一个数据的情况下,free*phead且*phead要置成空
if (temp->next == NULL)
{
free(temp);
*phead = NULL;
}
//多个数据下,新的是最后一个数据的指针域要为空
else
{
SLTNode* prev = NULL;
while (temp->next != NULL)
{
prev = temp;
temp = temp->next;
}
free(temp);
prev->next = NULL;
}
}
//查询数据
SLTNode* SListFind(SLTNode* phead, SLTDataType x)
{
//断言
assert(phead != NULL);
SLTNode* temp = phead;
while(temp != NULL)
{
if (temp->data == x)
return temp;
else
temp = temp->next;
}
return NULL;
}
//在pos位置之前插入数据
void SListInsert(SLTNode** phead, SLTNode* pos, SLTDataType x)
{
//断言,括号里的条件为假的时候报错,phead==NULL即假
assert(phead);
SLTNode* newnode = SListBuySLTNode(x);
//pos是头节点的情况
if (*phead == pos)
{
newnode->next = *phead;
*phead = newnode;
}
else
{
SLTNode* prev = *phead;
while (prev->next != pos)
{
prev = prev->next;
}
newnode->next = pos;
prev->next=newnode;
}
}
//在中间删除数据
void SListErase(SLTNode** phead, SLTNode* pos)
{
//空链表,断言
assert(*phead);
//删除头节点
if (pos == *phead)
{
*phead =pos->next;
}
else
{
SLTNode* prev = *phead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
}
free(pos);
}
void SListDestory(SLTNode** phead)
{
//断言,空链表不需要销毁
assert(*phead);
SLTNode* temp = *phead;
SLTNode* cur = *phead;
while (temp!=NULL)
{
cur = temp->next;
free(temp);
temp = cur;
}
*phead = NULL;
}
//归并
void merge(SLTNode* a, SLTNode* b, SLTNode** ret)
{
SLTNode* temp1 = a;
SLTNode* temp2 = b;
while (temp1 != NULL && temp2 != NULL)
{
if (temp1->data < temp2->data)
{
SListPushBack(ret, temp1->data);
temp1 = temp1->next;
}
else
{
SListPushBack(ret, temp2->data);
temp2 = temp2->next;
}
}
if (temp1)//temp2归并完了
{
while (temp1)
{
SListPushBack(ret, temp1->data);
temp1 = temp1->next;
}
}
else//temp1归并完了
{
while (temp2)
{
SListPushBack(ret, temp2->data);
temp2 = temp2->next;
}
}
return;
}
//正负分开,负数在前面,正数在后面,相对位置不变
void test2(SLTNode** a)
{
//输入数据
int x = 0;
for (int i = 0;i < N;i++)
{
scanf("%d", &x);
SListPushBack(a, x);
}
//操作,arr存储数据
int count = 0;
SLTDataType arr[N] = { 0 };
SLTNode* temp = *a;
while (temp)
{
if (temp->data < 0)
arr[count++] = temp->data;
temp = temp->next;
}
temp = *a;
while (temp)
{
if (temp->data > 0)
arr[count++] = temp->data;
temp = temp->next;
}
//重新赋值
count = 0;
temp = *a;
while (temp)
{
temp->data = arr[count++];
temp = temp->next;
}
return;
}
测试文件test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"SList.h"
void Test1()
{
SLTNode* a=NULL;//一开始没有初始化为NULL,导致pushback里面最后一种情况,传过去的a是野指针,随意指,不是结构体的
SListPushBack(&a, 5);
SListPushBack(&a, 8);
SListPushBack(&a, 456);
//SListPopBack(&a);
SListPushFront(&a, 5);
SListPushFront(&a, 8);
SListPushFront(&a, 456);
SListPopFront(&a);
SListPrint(a);
}
void Test2()
{
SLTNode* a = NULL;
SListPushBack(&a, 5);
SListPushBack(&a, 8);
SListPushBack(&a, 456);
//SListPopBack(&a);
SListPushFront(&a, 5);
SListPushFront(&a, 8);
SListPushFront(&a, 456);
SListPopFront(&a);
SLTNode* b=SListFind(a, 8);
//SListInsert(&a, b, 20);
SListErase(&a,b);
SListPrint(a);
SListDestory(&a);
//SListPrint(a);
}
int main()
{
//Test1();
//Test2();
//(1)归并
//初始化la
SLTNode* la = NULL;
SListPushBack(&la, 1);
SListPushBack(&la, 3);
SListPushBack(&la, 6);
SListPushBack(&la, 9);
SListPushBack(&la, 10);
SListPrint(la);
//初始化lb
SLTNode* lb = NULL;
SListPushBack(&lb, 2);
SListPushBack(&lb, 3);
SListPushBack(&lb, 4);
SListPushBack(&lb, 5);
SListPushBack(&lb, 6);
SListPushBack(&lb, 8);
SListPushBack(&lb, 14);
SListPrint(lb);
SLTNode* ret = NULL;
merge(la, lb, &ret);
printf("归并后的结果是:");
SListPrint(ret);
printf("\n");
////(2)正负
//SLTNode* p1 = NULL;
//test2(&p1);
//printf("正负分开后的结果:");
//SListPrint(p1);
return 0;
}
如果本文对你有所帮助,可以三连支持!!!