数据结构之单链表的增删查改等操作画图详解

Posted 小赵小赵福星高照~

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构之单链表的增删查改等操作画图详解相关的知识,希望对你有一定的参考价值。

单链表


为什么有了顺序表,还需要有链表这样的数据结构呢?

顺序表存在的问题:

  • 中间或者头部插入删除,需要挪动数据时间复杂度为O(N)
  • 增容需要申请新空间,拷贝数据,释放旧空间,会有不小的消耗
  • 当线性表长度变化较大时,难以确定存储空间的容量,造成空间的浪费

有没有更好的方式解决上面的问题呢?链表可以解决,下面给出了链表的结构来看看


链表的概念及其结构

概念

链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 。

结构

链表的每个节点有两个域,一个是数据域,一个是指针域,plist是头指针,指向第一个节点,data存储每个节点的数据,next是一个指针,它指向下一个节点

注意:

  • 链式结构在逻辑上是连续的,但物理上不一定是连续的
  • 现实中的节点一般都是在堆上申请出来的
  • 从堆上申请的空间,是按照一定的策略分配的,两次申请的空间可能是连续的,也可能是不连续的

链表的实现

链表的分类有很多,我们这里重点先讲解单链表的实现,也就是上面讲述的结构的实现

和顺序表一样,我们先创建三个文件:

SList.h对相关头文件的包含,以及实现单链表的结构和函数的声明

SList.c对实现单链表增删查改等操作的函数进行定义

test.c文件进行单链表相关函数和单链表功能的测试

我们在实现单链表时,首先需要定义单链表的结构:

//单链表的结构
typedef int SLTDataType;
typedef struct SListNode
{
  	SLTDataType data;//结点的数据域  
    struct SListNode* next;//结点的指针域
}SLTNode;

在单链表结构中,我们定义了一个指针指向下一个结点,定义data存储结点的数据,我们为了以后想修改data的数据类型时方便一些,我们弄一个typedef定义,这样定义的话,我们想要改变单链表的数据类型时,就会很轻松,直接修改typede这里的数据类型即可,但我们不使用typedef的话,所有涉及到数据类型的地方都需要修改,会很麻烦,所以我们这样定义。

定义好结构之后,我们得在测试文件中先创造一个结构体指针指向我们的第一个结点,也就是头指针

void test()
{
    SLTNode *plist=NULL;
}

那么有一个问题,我们在函数传参时是传值呢还是传头指针的地址呢?答案是传地址。因为形参的改变并不会影响实参,而我们在一些单链表的增删查改操作中是需要改变头指针的,所以我们将头指针的地址传过去。当然也有一些是不需要传地址的,总之,当我们需要改变头指针时就传地址,不需要改变时就传值

我们开始写单链表的操作实现,我们想一下我们单链表实现增删查改前还需要什么操作呢?我们需要有存储结点的空间吧?开辟了空间我们还需要销毁吧?在测试时我们需要打印数据吧?还有什么呢?好像没有了,这些就够我们玩了。

下面我们将讲解下面的单链表的操作:

//打印链表
void PrintSList(SLTNode* phead);
//开辟一个新结点
SLTNode* BuySListNode(SLTDataType x);
//头插
void SListPushFront(SLTNode** pphead, SLTDataType x);
//尾插
void SListPushBack(SLTNode** pphead, SLTDataType x);
//头删
void SListPopFront(SLTNode** pphead);
//尾删
void SListPopBack(SLTNode** pphead);
//找到一个结点
SLTNode* SListFind(SLTNode* phead, SLTDataType x);

//在pos位置前插入一个结点,配合找到一个结点使用
void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);

//在pos位置后插入一个结点,配合找到一个结点使用
void SListInsertAfter(SLTNode** pphead, SLTNode* pos, SLTDataType x);

//在pos位置删除一个结点,配合找到一个结点使用
void SListErase(SLTNode** pphead, SLTNode* pos);

//在pos位置后删除一个结点,配合找到一个结点使用
void SListEraseAfter(SLTNode* pos);

//返回链表大小
int SizeSList(SLTNode* phead);

//判断链表是否为空
bool EmptySList(SLTNode* phead);

//销毁链表
void DestorySList(SLTNode** pphead);

下面我们来看开辟一个结点空间的函数实现:


开辟一个新结点

SLTNode* BuySlistNode(SLTDataType x)
{
	SLTDode* newnode = (SLTDode*)malloc(sizeof(SLTDode));
    if(newnode==NULL)
    {
        perror("malloc");
        return NULL;
    }
    newnode->data=x;
    newnode->next=NULL;
    
    return newnode;
}

malloc一个新结点,并判断是否开辟成功,然后将data置成x,next置NULL,然后将新节点返回。

有开辟一个结点,那就有释放:

下面我们来看链表的销毁


链表的销毁

void DestorySList(SLTNode **pphead)
{
    assert(phead);
    assert(*phead);
    SLTNode* cur=*phead;
    while(cur)
    {
        SLTNode* next=cur->next;//需要报存cur的下一个结点,因为释放后,cur结点里面的值会都变为随机值,就找不到下一个结点了
        free(cur);
        cur=next;
    }
    *phead=NULL;
}

这里我们传了头指针的地址,因为我们这里需要将头指针置为NULL(否则就为野指针了),其实这里也可以传一级指针,既然在函数里面无法置NULL头指针,那么我们就在调用这个函数后,将外面的头指针置为NULL就好了,这两种写法都可以,读者自行选择。

在循环里面我们需要保存cur的下一个结点,因为释放后,cur结点里面的值会都变为随机值,就找不到下一个结点了,看下面的调试截图

接下来我们看打印链表的实现


打印链表

void PrintSList(SLTDode* phead)
{
    SLTDode* cur = phead;
    while(cur)
    {
        printf("%d->",cur->data);
        cur=cur->next;
    }
    printf("NULL\\n");
}

打印链表不需要改变头指针,所以传值就可以,利用一个cur,然后循环迭代打印即可

接下来我们正式进入我们的单链表的增删查改操作


单链表的尾插

//尾插
void SListPushBack(SLTNode** pphead, SLTDataType x)
{
    assert(pphead);
    //没有结点时
    if(*pphead==NULL)
    {
        SLTNode* newnode = BuySListNode(x);
        newnode->next=*pphead;
        *pphead=newnode;
    }
    else//多个结点时
    {
        //找尾
        SLTNode* tail = *phead;
        while(tail->next!=NULL)
        {
            tail=tail->next;
        }
        //tail此时是尾
        SLTNode* newnode = BuySListNode(x);
        tail->next=newnode;
    }
}

在尾插时,我们考虑多个结点的情况不能满足在没有结点时的情况,故我们需要考虑没有结点时

  • 尾插操作图解

在没有结点时:

有结点时:

在test.c文件中进行测试,如图,我们现在尾插进了5个元素:

我们的代码没有问题。

下面我们看单链表的头插


单链表的头插

//头插
void SListPushFront(SLTNode** pphead, SLTDataType x)
{
    assert(pphead);
    struct SLTNode* newnode = BuySListNode(x);
    newnode->next = *pphead;//1
    *pphead = newnode;//2
}

在头插时,我们考虑多个结点的情况能满足在没有结点时的情况

  • 头插的操作图解

先把将头指针赋给newnode的next,再将newnode赋给头指针(注意先后顺序)

我们在测试文件中进行测试代码:

此时我们就头插进去了一个结点。

接下来我们再看头删


单链表的头删

//头删
void SListPopFront(SLTNode** pphead)
{
    assert(pphead);
    assert(*pphead);//头删时,链表不能为空
    SLTNode* newhead = (*pphead)->next;//1
    free(*pphead);//2
    *pphead = newhead;//3
}

头删时,链表不能为空,我们在前面断言一下,我们在删除结点时,一定用临时变量保存将要删除的结点的下一个结点,不然我们释放删除后,就找不到它的下一个结点了。并且我们这个代码在只有一个结点的情况下也适用。

  • 头删的操作图解

我们在测试文件中进行测试代码:

我们的代码是没问题的。

头删不需要考虑只有一个结点的情况,多个结点时的处理情况也可以处理一个结点时的情况,而尾删就必须要考虑只有一个结点的情况了,下面我们来看尾删:


单链表的尾删

//尾删
void SListPopBack(SLTNode** pphead)
{
    assert(pphead);
    assert(*pphead);
    //只有一个结点时
    if(*pphead->next==NULL)
    {
        free(*pphead);
        *pphead==NULL;
    }
    else
    {
        //先找尾的前一个元素
        SLTNode* prev = NULL;
        SLTNode* tail = *pphead;
        while(tail->next!=NULL)
        {
            prev = tail;
            tail = tail->next;
        }
        prev->next = NULL;
        free(tail);
        tail = NULL;
    }
}

单链表的尾删我们需要考虑只有一个结点时和多个结点时两种情况,多个结点时,我们需要先找尾的前一个元素,需要将它的next置NULL

只有一个结点时我们只需要直接free然后置空即可。

  • 尾删的操作图解

多个结点时:

我们来看看测试结果:

接下来我们再看找到一个结点的接口:


找到单链表中的一个结点

//找到一个结点
SLTNode* SListFind(SLTNode* phead, SLTDataType x)
{
    assert(phead);//链表不能为NULL
    SLTNode* cur=phead;
    while(cur)
    {
        if(cur->data==x)
        {
            return cur;//找到了
        }
        cur=cur->next;
    }
    return NULL;//找不到
}

在传参时,这里我们可以传值,因为不会改变头指针,这个函数很简单,就遍历链表,找到了返回结点,循环结束时说明没找到就返回NULL。


在pos位置后插入结点

通常关于在某个位置进行插入删除操作都配合上面的函数找到一个结点使用

//在pos位置后插入一个结点,配合找到一个结点使用
void SListInsertAfter(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
    assert(pphead);
    assert(pos);
    SLTNode* newnode = BuySListNode(x);
    newnode->next=pos->next;
    pos->next=newnode;
}
  • pos位置后插入结点操作图解

将新节点的next指向pos的next,再让pos的next指向新节点。

测试文件中进行测试发现没什么问题:

考虑一下我们为什么不在pos位置前插入一个新节点?

在pos位置前插入一个新结点是可以实现的,但是我们在插入新结点前,需要找到pos位置前的那个结点才能够完成操作,这样代码的时间复杂度就成了O(N),而我们在pos位置后插一个结点,时间复杂度仅为O(1),下面是在pos位置前插入一个结点:


在pos位置前插入一个结点

void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pphead);
	assert(pos);
	if (*pphead == pos)
	{
		//则头插
		SListPushFront(pphead, x);
	}
	else
	{
		SLTNode* newnode = BuySListNode(x);
		//找到pos前一个结点
		SLTNode* pre = *pphead;
		while (pre->next != pos)
		{
			pre = pre->next;
		}
		newnode->next = pos;
		pre->next = newnode;
	}
}

如果链表中只有一个元素时,这时在pos前面插入结点,就相当于头插,我们可以直接复用头插函数,多个元素时,我们需要先找到pos的前一个结点,然后才能够进行插入操作。

  • pos位置前插入结点操作图解

测试文件中进行测试发现没什么问题:

下面我们来看在pos位置删除一个结点


pos位置后删除结点

//在pos位置后删除一个结点,配合找到一个结点使用
void SListEraseAfter(SLTNode* pos)
{
    assert(pos);
    assert(pos->next);//删除pos位置后的结点,所以pos后面的结点不能为NULL
    SLTNode* next=pos->next;
    pos->next=next->next;
    free(next);
   	next=NULL;
}
  • pos位置后删除结点操作图解

我们先要将pos的next保存下来,为什么呢?因为如果直接pos->next=pos->next->next,我们释放pos->next时,会找不到它,因为pos->next已经改了

测试文件中进行测试发现没什么问题:

同样的,我们为什么不在pos位置删除结点呢?

在pos位置删除一个结点是可以实现的,但是我们在删除结点前,需要找到pos位置前的那个结点才能够完成操作,这样代码的时间复杂度就成了O(N),而我们在pos位置后删除结点,时间复杂度仅为O(1),下面是在pos位置删除一个结点:


pos位置删除结点

//在pos位置删除一个结点,配合找到一个结点使用
void SListErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead);
	assert(*pphead);
	assert(pos);
	if (pos == *pphead)
	{
		//头删
		SListPopFront(pphead);
	}
	else
	{
		//找到删除结点的前一个元素
		SLTNode* pre = *pphead;
		while (pre->next != pos)
		{
			pre = pre->next;
		}
		pre->next = pos->next;
		free(pos);
		pos = NULL;
	}
}

当链表只有一个结点时,此时的操作相当于头删,故我们复用头删的函数,链表有多个元素时,我们先要找到删除结点的前一个结点,才能完成操作。

  • pos位置删除结点操作图解

测试文件中进行测试发现没什么问题:

下面我们看两个简单的操作:


返回链表大小

//返回链表大小
int SizeSList(SLTNode* phead)
{
    SLTNode* cur=phead;
    int size=0;
    while(cur)
    {
        size++;
        cur=cur->next;
    }
    return size;
}

判断链表是否为空

//判断链表是否为空
bool EmptySList(SLTNode* phead)
{
    return phead==NULL;
}

phead==NULL表示头指针为空,为真则返回true,为假则返回false。

以上就是博主讲解的单链表的基本操作,下面附上源代码供大家参考:


源代码

SList.h(单链表结构及其函数的声明)

#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
typedef int SLTDataType;
//链表结点结构
typedef struct SListNode
{
	SLTDataType data;
	struct SListNode* next;
}SLTNode;

//打印链表
void PrintSList(SLTNode* phead);
//开辟一个新结点
SLTNode* BuySListNode(SLTDataType x);
//头插
void SListPushFront(SLTNode** pphead, SLTDataType x);
//尾插
void SListPushBack(SLTNode** pphead, SLTDataType x);
//头删
void SListPopFront(SLTNode** pphead);
//尾删
void SListPopBack(SLTNode** pphead);
//找到一个结点
SLTNode* SListFind(SLTNode* phead, SLTDataType x);

//在pos位置前插入一个结点,配合找到一个结点使用
void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);

//在pos位置后插入一个结点,配合找到一个结点使用
void SListInsertAfter(SLTNode** pphead, SLTNode* pos, SLTDataType x);

//在pos位置删除一个结点,配合找到一个结点使用
void SListErase(SLTNode** pphead, SLTNode* pos);

//在pos位置后删除一个结点,配合找到一个结点使用
void SListEraseAfter(SLTNode* pos);

//返回链表大小
int SizeSList(SLTNode* phead);

//判断链表是否为空
bool EmptySList(SLTNode* phead);

//销毁链表
void DestorySList(SLTNode** pphead);

SList.c(单链表函数的实现)

#include"SList.h"

void PrintSList(SLTNode* phead)
{
	//assert(phead);
	SLTNode* cur = phead;
	while (cur)
	{
		printf("%d->", cur->data);
		cur = cur->next;
	}
	printf("NULL\\n");
}

//开辟一个新节点
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;
}

//尾插
void SListPushBack(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);
	if (*pphead == NULL)//没有结点时
	{
		SLTNode* newnode=BuySListNode(x);
		*pphead = newnode;
	}
	else
	{
		//找尾
		SLTNode* tail = *pphead;
		while (tail->next)
		{
			tail = tail->next;
		}
		SLTNode* newnode = BuySListNode(x);
		tail->next = newnode;
	}
单链表的增删查改等基本操作C++实现

单链表的链式存储总结

数据结构学习笔记(数据结构概念顺序表的增删查改等)详细整理

c++单链表构造函数运算符重载析构函数增删查改等

数据结构单链表的增删查改,附代码+笔记gitee自取

单链表~增删查改(附代码)~简单实现