单链表简单功能的代码实现

        上一章我们对顺序表的一些简单功能做了代码实现,但对于顺序存储的一些缺点,这一章我们实现一种简单的链式结构的数据结构单链表。

文章目录

  • 前言
  • 单链表打印
  • 创建新的结点
  • 单链表尾插
  • 单链表头插
  • 单链表尾删
  • 单链表头删
  • 单链表查找指定数据
  • 单链表删除指定位置数据
  • 单链表删除指定位置后面数据
  • 单链表指定位置前面插入数据
  • 单链表指定位置后面插入数据
  • 单链表销毁
  • 头文件代码
  • 源文件代码

前言

顺序表的缺点:

  1. 顺序表空间不足要扩容,如果原地扩容则只需要在顺序表后面开辟空间,而如果后面的空间不够则需要在内存中另找一块更大的地方将已有的数据全部拷贝过去
  2. 为了避免每次扩容太少导致频繁的扩容,我们一般都是把顺序表扩容到原来的二倍,而如果我们只需要增加几个数据则容易造成内存的浪费
  3. 顺序表是连续存储的,所以我们每次插入数据都要把插入位置后面所有的数据都往后挪动,最坏情况的时间复杂度是O(N)

(顺序表也有随机访问的优点)

        链式结构就可以解决顺序表这种缺点,我们今天介绍的一种简单的链式结构是单链表(不带哨兵位的头节点),单链表在存储中也有一些缺点,所以在后面我们还会介绍双向链表等。

(本篇内容参考B站比特科技学习)

typedef struct sListNode {
    dataType num;
    struct sListNode* next;
}sListNode;

         第一张图是我们现象出来的逻辑结构,第二张图是实际的物理结构,一个链表存数据的同时指向下一个数据的位置,所以我们用结构体来实现,在结构体里一部分存数据另一部分有一个结构体指针用来存下一个数据的地址

单链表打印 

void sListPrint(sListNode* phead) {
	sListNode* cur = phead;
	while (cur != NULL) {
		printf("%d->", cur->num);
		cur = cur->next;
	}
	printf("NULL\n");
}

        因为用到指针,所以我们传进来都要断言一下是不是为空下面就不再赘述了,这里没有断言是因为链表可能为空链表。定义一个临时变量cur存链表头节点的地址,如果为空我们判断一下直接打印NULL即可。如果不为空,就进入循环打印,再把cur指向下一个位置,如果为空跳出循环,不为空重复操作即可。

创建新的结点

sListNode* buySListNode(dataType x) {
	sListNode* newnode = (sListNode*)malloc(sizeof(sListNode));
	if (newnode == NULL){
		printf("malloc fail\n");
		exit(-1);
	}
	else{
		newnode->num = x;
		newnode->next = NULL;
	}
	return newnode;
}

        由于每次插入数据都需要开辟一个新的结点,所以我们直接封装一个函数。进来后我们直接malloc一个结点用newnode指针接收,养成好习惯我们这里判断一下,开辟成功后我们把数据赋值进来,并让指针指向为空,返回该结点的地址。 

单链表尾插

void sListPushBack(sListNode** pphead, dataType x) {
	assert(pphead);
	sListNode* newnode = buySListNode(x);
	if (*pphead == NULL) {
		*pphead = newnode;
	}
	else {            
		sListNode* tail = *pphead;
		while (tail->next != NULL){
			tail = tail->next;
		}
		tail->next = newnode;
	}
}

         这里面需要传进来二级指针是为了解决链表为空链表的情况。

        一方面传进来空指针的话我们对其解引用就造成了非法操作,另一方面向空链表插入新的结点我们肯定要改变链表头结点的地址,如果单纯传进来这个地址那么里面改变了出去这个临时变量也会被销毁,而能做到改变地址的方法就是把这个地址的地址传进来,所以就用到了二级指针

        明白了这一点后剩下的事情就好办了,先开辟一个新结点,如果为空链表,那么我们直接把对二级指针解引用把链表地址改为新结点的地址newnode。如果不为空,那就定义一个临时变量tail存起始位置的地址,判断这个结点中存储的下一个结点是否为空,为空就说明这个是最后一个结点,如果不是那我们就让它变为下一个结点,这样循环直到找到最后一个结点的位置,最后让最后一个结点指向newnode即可。 

        (其实不止二级指针这一种方法,在这里我们只介绍二级指针这种方法)

单链表头插

void sListPushFront(sListNode** pphead, dataType x) {
	assert(pphead);
	sListNode* newnode = buySListNode(x);
	newnode->next = *pphead;
	*pphead = newnode;
}
        头插相较于尾插简单很多,因为需要改变头节点的地址,所以自然也要传进来二级指针,首先开辟一个新结点,这时只需要把这个新结点指向原来的头节点,再把头节点的位置改为这个newnode的位置即可。要注意的是不能先改头节点的位置,那样会使新头节点找不到原来头节点的位置。

单链表尾删

void sListPopBack(sListNode** pphead) {
	assert(*pphead&&pphead);
	if ((*pphead)->next == NULL) {
		free(*pphead);
		*pphead = NULL;
	}
	else {
		sListNode* tail = *pphead;
		while (tail->next->next) {
			tail = tail->next;
		}
		free(tail->next);
		tail->next = NULL;
	}
}

        尾删不仅需要判断传进来的pphead是否不为空,主要的作用是判断传进来的不能是个空链表,所以解引用看一下是不是空链表。这里用if,else分开的原因是当链表中只有一个结点跟有多个结点的情况是不一样的,因为链表有个缺点是它不能实现随机访问,而且一个结点不能访问到它上一个结点的位置。所以我们需要从头开始访问直到找到最后一个,为了解决无法访问上一个结点的问题除了这种方法这里我们也可以定义一个prev临时变量存头节点位置,跟着tail往后走,找到了让tail回到prev就行。但是这两种面对只有一个结点的情况都没办法解决,因为如果只有一个结点的话prev是什么呢,它压根就不存在上一个结点,对一个空指针解引用自然会出错,上面这种方法同样也面对这个问题。所以我们把一个结点的情况单拿出来,如果它指向空,直接free掉再把指向它的指针置为空就行。多个结点就按上面那种思路,找到最后一个结点的位置把它free掉再置为空即可。

单链表头删

void sListPopFront(sListNode** pphead) {
	assert(*pphead && pphead);
	sListNode* next = (*pphead)->next;
	free(*pphead);
	*pphead = next;
}

        头删就不需要考虑那么多了,我们只需要进来判断不是空,然后创建一个指针记住下一个结点的位置,把这个头结点free掉,再把临时指针复制过来就行,即使只有一个结点我们是把NULL 赋值过来的,所以不需要特殊处理。这里需要注意的是先需要把下一个结点的位置存起来,避免先free掉这个头节点从而找不到下一个结点的位置。

单链表查找指定数据

sListNode* sListFind(sListNode* phead, dataType x) {
	assert(phead);
	sListNode* cur = phead;
	while (cur) {
		if (cur->num == x) return cur;
		else cur = cur->next;
	}
	return NULL;
}

        查找数据我们返回的是该数据的地址,因为只是查找所以我们不用传二级指针。根据链表的特性不支持随机访问,所以我们还是首先定义一个临时变量存头节点位置从头访问,如果不为空就进入循环判断是不是找到该数据,不是就指向下一个结点,如果一开始就是空链表或者到最后一个结点也没找到对应的数据,我们直接返回一个空指针即可。

单链表删除指定位置数据

void sListDeleteCurrent(sListNode** pphead, sListNode** ppos) {   //传址删除
	assert(pphead && ppos);
	if (*pphead == *ppos) {
		*pphead = (*ppos)->next;
		free(*ppos);
	}
	else {
		sListNode* prev = *pphead;
		while (prev->next != *ppos) {
			prev = prev->next;
		}
		prev->next = (*ppos)->next;
		free(*ppos);
		*ppos = NULL;
	}
}

         这里头节点的位置可能会改变所以肯定还是要传二级指针,而后面的结点pos是传址删除还是传值删除那个课程学习和我看到的几个博主都是传值,但我觉得如果只是传进来值的话free的话跟出来这个函数临时变量自己销毁好像区别不大,而外面的那个pos依然记得这个地址,而这块空间已经free掉了,如果手误再使用这个指针就造成了越界访问。

        删除数据同样要考虑如果一种情况就是删除的结点前面也有结点,那么我们需要把前一个结点指向后一个结点,所以这个也需要分两种情况。如果前面没有结点我们只需要直接删即可,先让头结点指向下一个结点的位置,再free掉就行了。如果前面有结点,我们就需要创建一个临时的prev指向头结点位置往后走,没有找到就继续走,找到后跳出循环,这个时候把prev指向ppos所在结点的下一个结点的位置,把该结点删除就行。

void sListDeleteCurrent0(sListNode** pphead, sListNode* pos) {   //传值删除
	assert(pphead && pos);
	if (*pphead == pos) {
		*pphead = pos->next;
		free(pos);
	}
	else {
		sListNode* prev = *pphead;
		while (prev->next != pos) {
			prev = prev->next;
		}
		prev->next = (pos)->next;
		free(pos);
		pos = NULL;
	}
}

单链表删除指定位置后面数据

void sListDeleteAfter(sListNode* pos) {
	assert(pos && pos->next);
	pos->next = pos->next->next;
	free(pos->next);
}

        我们会发现删除指定位置的数据还要考虑特殊情况,需要得到前一个位置的地址,所以有种简单的方法不考虑只有一种结点的情况,我们只删除指定位置后面的数据,进来assert判断不能只有一个结点,这个时候只需要把pos下一个结点存的next赋值过来再把pos指向的那个结点删除就行了。

单链表指定位置前面插入数据

void sListInsertBefore(sListNode** pphead, sListNode* pos, dataType x) {
	assert(pphead && pos);
	sListNode* newnode = buySListNode(x);
	if (*pphead == pos) {
		newnode->next = *pphead;
		*pphead = newnode;
	}
	else {
		sListNode* prev = *pphead;
		while (prev->num != x) {
			prev = prev->next;
		}
		prev->next = newnode;
		newnode->next = pos;
	}
}

        在指定位置插入数据同样也有特殊情况,因为链表中结点是不知道前一个结点的位置的。如果正好是在头位置插入的话我们只需要让这个新结点指向本来的头结点的位置,再把这个新结点的位置赋值给头位置即可。如果不是第一个,那么一样的道理我们也需要创建一个临时变量prev跟着走,直到遇到指定位置,让prev指向这个新结点,再让这个新结点指向pos即可。

单链表指定位置后面插入数据

void sListInsertAfter(sListNode* pos, dataType x) {
	assert(pos);
	sListNode* newnode = buySListNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}

        跟指定位置后面删除数据同样道理,我们可以写一个指定位置后面插入的函数。判断不为空链表,直接让它指向本来pos指向的结点再让pos指向它即可。 

单链表销毁

void sListDestroy(sListNode** pphead) {
	assert(pphead);
	sListNode* cur = *pphead;
	while (cur) {
		sListNode* next = cur->next;
		free(cur);
		cur = next;
	}
	*pphead = NULL;
}

         单链表的销毁需要把头结点的位置也置为空,所以同样需要二级指针。因为没法随机访问,我们只能一个结点一个结点删。创建cur记住头结点地址,不为空就进行后续操作,把cur这时指向的结点free掉,再指向下一个结点,直到cur为空了说明后面的结点全部free完了,这时把头结点的指针free了即可。

头文件代码

#pragma once

#define _CRT_NO_SECRUE_WARNINGS

#include <stdio.h>
#include <assert.h>
#include <stdlib.h>

typedef int dataType;

typedef struct sListNode {
	dataType num;
	struct sListNode* next;
}sListNode;

//单链表打印
void sListPrint(sListNode* phead);

//单链表销毁
void sListDestroy(sListNode** phead);

//创建新的结点
sListNode* buySListNode(dataType x);

//单链表尾插
void sListPushBack(sListNode** pphead, dataType x);

//单链表头插
void sListPushFront(sListNode** pphead, dataType x);

//单链表尾删
void sListPopBack(sListNode** pphead);

//单链表头删
void sListPopFront(sListNode** pphead);

//单链表查找指定数据
sListNode* sListFind(sListNode* pphead, dataType x);

//单链表删除指定数据
void sListDeleteCurrent(sListNode** pphead, sListNode** pos);//传址
void sListDeleteCurrent0(sListNode** pphead, sListNode* pos);//传值

//单链表删除指定位置后面数据
void sListDeleteAfter(sListNode* pos);

//单链表指定位置前面插入数据
void sListInsertBefore(sListNode** pphead, sListNode* pos, dataType x);

//单链表指定位置后面插入数据
void sListInsertAfter(sListNode* pos, dataType x);

源文件代码

#include "singleLinkedList.h"

void sListDestroy(sListNode** pphead) {
	assert(pphead);
	sListNode* cur = *pphead;
	while (cur) {
		sListNode* next = cur->next;
		free(cur);
		cur = next;
	}
	*pphead = NULL;
}

void sListPrint(sListNode* phead) {
	sListNode* cur = phead;
	while (cur != NULL) {
		printf("%d->", cur->num);
		cur = cur->next;
	}
	printf("NULL\n");
}

sListNode* buySListNode(dataType x) {
	sListNode* newnode = (sListNode*)malloc(sizeof(sListNode));
	if (newnode == NULL){
		printf("malloc fail\n");
		exit(-1);
	}
	else{
		newnode->num = x;
		newnode->next = NULL;
	}
	return newnode;
}

void sListPushBack(sListNode** pphead, dataType x) {
	assert(pphead);
	sListNode* newnode = buySListNode(x);
	if (*pphead == NULL) {
		*pphead = newnode;
	}
	else {            
		sListNode* tail = *pphead;
		while (tail->next != NULL){
			tail = tail->next;
		}
		tail->next = newnode;
	}
}

void sListPushFront(sListNode** pphead, dataType x) {
	assert(pphead);
	sListNode* newnode = buySListNode(x);
	newnode->next = *pphead;
	*pphead = newnode;
}

void sListPopBack(sListNode** pphead) {
	assert(*pphead&&pphead);
	if ((*pphead)->next == NULL) {
		free(*pphead);
		*pphead = NULL;
	}
	else {
		sListNode* tail = *pphead;
		while (tail->next->next) {
			tail = tail->next;
		}
		free(tail->next);
		tail->next = NULL;
	}
}

void sListPopFront(sListNode** pphead) {
	assert(*pphead && pphead);
	sListNode* next = (*pphead)->next;
	free(*pphead);
	*pphead = next;
}

sListNode* sListFind(sListNode* phead, dataType x) {
	assert(phead);
	sListNode* cur = phead;
	while (cur) {
		if (cur->num == x) return cur;
		else cur = cur->next;
	}
	return NULL;
}

void sListDeleteCurrent(sListNode** pphead, sListNode** ppos) {   //传址删除
	assert(pphead && ppos);
	if (*pphead == *ppos) {
		*pphead = (*ppos)->next;
		free(*ppos);
	}
	else {
		sListNode* prev = *pphead;
		while (prev->next != *ppos) {
			prev = prev->next;
		}
		prev->next = (*ppos)->next;
		free(*ppos);
		*ppos = NULL;
	}
}

void sListDeleteCurrent0(sListNode** pphead, sListNode* pos) {   //传值删除
	assert(pphead && pos);
	if (*pphead == pos) {
		*pphead = pos->next;
		free(pos);
	}
	else {
		sListNode* prev = *pphead;
		while (prev->next != pos) {
			prev = prev->next;
		}
		prev->next = (pos)->next;
		free(pos);
		pos = NULL;
	}
}

void sListDeleteAfter(sListNode* pos) {
	assert(pos && pos->next);
	pos->next = pos->next->next;
	free(pos->next);
}

void sListInsertBefore(sListNode** pphead, sListNode* pos, dataType x) {
	assert(pphead && pos);
	sListNode* newnode = buySListNode(x);
	if (*pphead == pos) {
		newnode->next = *pphead;
		*pphead = newnode;
	}
	else {
		sListNode* prev = *pphead;
		while (prev->num != x) {
			prev = prev->next;
		}
		prev->next = newnode;
		newnode->next = pos;
	}
}

void sListInsertAfter(sListNode* pos, dataType x) {
	assert(pos);
	sListNode* newnode = buySListNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值