[数据结构从小白到大牛]第四篇:3分钟带你吃透单链表并模拟实现

目录

1->认识链表

先认识下线性表:

再来思考一个问题:

接下来认识链表:

2->链表种类以及最常用的两种链表

 3->模拟实现单链表--重头戏,请务必掌握

3.1自定义链表结点  struct SListNode

 3.2动态申请一个链表节点,别忘记删除的时候释放哦

3.3链表打印数据

3.4链表增删改查之尾部插入数据

3.5链表增删改查之头部插入数据

3.6链表增删改查之尾部删除数据

3.7链表增删改查之头部删除数据

3.8链表增删改查之查找数据

3.9链表增删改查之在pos位置后边插入数据

3.10链表增删改查之在pos位置前边插入数据

 3.11链表增删改查之删除pos位置的数据

4->您的专属鼓励师


1->认识链表

先认识下线性表:

        线性表(linear list)是n个具有相同特性的数据元素的有限序列. 线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串...

        线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储.

        看图认识下结构

再来思考一个问题:

顺序表的问题及思考:

问题:

        (1)顺序表进行中间/头部的插入删除,时间复杂度为O(N)


        (2)增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗


        (3)增容一般是呈2倍的增长,势必会有一定的空间浪费,例如当前容量为100,满了以后增容到 200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间

思考:

        怎么解决顺序表的这些问题呢?没错,就是用我们今天讲解的链表!

接下来认识链表:

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

        看下链表的逻辑结构,我们写题和写代码的时候就这样用

         也可以这样理解

 注意:

(1)从上图可以看出,链式结构在逻辑上是连续的,但是在物理上不一定连续

(2)现实中的结点一般都是从堆上申请的

(3)从堆上申请的空间,是按照一定的策略来分配的,连续两次申请的空间可能连续,也可能不连续

2->链表种类以及最常用的两种链表

        实际开发中链表的种类繁多,大体就8类,以下3个组合即可

  • 单向   双向

提供两种图,你自己看哪个好理解就用哪个

 

  • 带头结点     不带头结点

  • 循环    非循环

虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构:

1. 无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等.另外这种结构在笔试面试中出现很多.

2. 带头双向循环链表: 结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都 是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了,后面我们代码实现了就知道了.

 3->模拟实现单链表--重头戏,请务必掌握

3.1自定义链表结点  struct SListNode

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


//用C语言模拟实现"不带头单向不循环链表,插入数据以int为例子
//定义单链表结构体
typedef struct SListNode
{
	int _data;				//数据域
	struct SListNode* _next;//指针域
}SLTNode;

 3.2动态申请一个链表节点,别忘记删除的时候释放哦

//动态申请一个单链表节点
SListNode* CreateSLTNode(int x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		printf("malloc failed申请空间失败,我要退出哦!\n");
		exit(1);
	}
	newnode->_data = x;
	newnode->_next = NULL;
	return newnode;
}

3.3链表打印数据

//链表打印数据
void SListPrint(SLTNode* plist)
{
	//assert(plist);
	if (plist == NULL)
	{
		printf("链表为空 : ");
	}
	SLTNode* cur = plist;
	while(cur != NULL)
	{
		printf("%d--->",cur->_data);
		cur = cur->_next;

	}
	printf("NULL\n");//最后一个节点指向NULL
}

3.4链表增删改查之尾部插入数据

//接下来实现链表的增删查改

//链表的尾部插入
void SListPushBack(SLTNode** pplist,int num)
{
	assert(pplist);
	SLTNode* newnode = CreateSLTNode(num);//不管哪种情况,节点要先申请出来

	//考虑刚开始插入数据一个节点都没有的情况
	if (*pplist == NULL)
	{
		*pplist = newnode;
	}
	else//已经存在节点的情况
	{
		SLTNode* tail = *pplist;
		while (tail->_next != NULL)
		{
			tail = tail->_next;
		}
		tail->_next = newnode;
	}
}

 我们写代码测试下是否正确:

//测试尾部插入
void test1()
{
	SLTNode* plist = NULL;
	SListPushBack(&plist, 1);
	SListPushBack(&plist, 2);
	SListPushBack(&plist, 3);
	SListPushBack(&plist, 4);
	SListPushBack(&plist, 5);

	SListPrint(plist);
}

 看控制台输出:结果正确

3.5链表增删改查之头部插入数据

//链表的头部插入
void SListPushFront(SLTNode** pplist,int num)
{
	//头部插入就很简单了,不需要考虑有节点或无节点的两种情况

	//先创建节点
	SLTNode* newnode = CreateSLTNode(num);

	newnode->_next = *pplist;
	*pplist = newnode;
}

 我们写代码测试下是否正确: 

//测试头部插入
void test2()
{
	SLTNode* plist = NULL;
	SListPushFront(&plist, 1);
	SListPushFront(&plist, 2);
	SListPushFront(&plist, 3);
	SListPushFront(&plist, 4);
	SListPushFront(&plist, 5);

	SListPrint(plist);
}

 看控制台输出:结果正确

3.6链表增删改查之尾部删除数据

先看我画的图理解下删除前后的细节

 下边是实现的代码

//链表的尾部删除
void SListPopBack(SLTNode** pplist)
{
	SLTNode* plist = *pplist;//获得链表起始地址
	//1.没有节点
	if (plist == NULL)
	{
		printf("目前没有节点,不能删除,程序退出!!!\n");
		exit(666);
	}
	else if (plist->_next == NULL)//2.一个节点
	{
		//只有一个节点,释放最后一个节点,将链表指针置空
		free(plist);
		plist = NULL;
		*pplist = NULL;
	}
	else//3.>=2 多个节点
	{
		//首先找到尾部以及尾部
		SLTNode* prev_tail = *pplist;
		SLTNode* tail = *pplist;
		while (tail->_next != NULL)
		{
			prev_tail = tail;
			tail = tail->_next;
		}
		//走到这里的时候,tail指向最后一个节点,prev_tail指向倒数第二个节点
		free(tail);
		prev_tail->_next = NULL;

	}
}

 我们写代码测试下是否正确: 

//测试尾部删除
void test3()
{
	SLTNode* plist = NULL;
	SListPushFront(&plist, 1);
	SListPushFront(&plist, 2);
	SListPushFront(&plist, 3);
	SListPushFront(&plist, 4);
	SListPushFront(&plist, 5);
	SListPrint(plist);

	SListPopBack(&plist);
	printf("plist->%p\n",plist);
	SListPrint(plist);
}

 看控制台输出:结果正确

3.7链表增删改查之头部删除数据

先看我画的图理解下删除前后的细节

 下边是实现的代码

//链表的头部删除
void SListPopFront(SLTNode** pplist)
{
	//链表头部删除肯定会导致链表指针plist变化
	SLTNode* plist = *pplist;//获得链表起始地址

	//1.没有节点
	if (plist == NULL)
	{
		printf("目前没有节点,不能删除,程序退出!!!\n");
		exit(666);
	}
	else//2.有节点,那就大胆删除无需考虑有几个节点
	{
		SLTNode* next = plist->_next;
		free(plist);
		*pplist = next;
	}
}

我们写代码测试下是否正确: 

//测试头部删除
void test4()
{
	SLTNode* plist = NULL;
	SListPushFront(&plist, 1);
	SListPushFront(&plist, 2);
	SListPushFront(&plist, 3);
	SListPushFront(&plist, 4);
	SListPushFront(&plist, 5);
	SListPrint(plist);

	SListPopFront(&plist);
	SListPrint(plist);

	SListPopFront(&plist);
	SListPrint(plist);

	SListPopFront(&plist);
	SListPrint(plist);

	SListPopFront(&plist);
	SListPrint(plist);

	SListPopFront(&plist);
	SListPrint(plist);

}

 看控制台输出:结果正确

3.8链表增删改查之查找数据

//链表查找数据
SLTNode* SListSearch(SLTNode* plist, int num)
{
	if (plist == NULL)
	{
		printf("目前没有节点,不能查找数据,程序退出!!!\n");
		exit(666);
	}

	SLTNode* cur = plist;
	while (cur != NULL)
	{
		if (cur->_data == num)
		{
			return cur;
		}
		cur = cur->_next;
	}
	return NULL;//到这里说明要找的数据在这个链表里不存在
}

 我们写代码测试下是否正确: 

//测试查找数据
void test5()
{
	SLTNode* plist = NULL;
	SListPushFront(&plist, 1);
	SListPushFront(&plist, 2);
	SListPushFront(&plist, 3);
	SListPushFront(&plist, 4);
	SListPushFront(&plist, 5);

	SLTNode* ptr = SListSearch(plist,3);
	printf("ptr->%d\n",ptr->_data);
}

看控制台输出:结果正确

3.9链表增删改查之在pos位置后边插入数据

//在pos位置后边插入数据
void SListInsertAfter(SLTNode* pos,int num)
{
	assert(pos);

	SLTNode* newnode = CreateSLTNode(num);

	newnode->_next = pos->_next;
	pos->_next = newnode;

}

 我们写代码测试下是否正确: 

//测试pos后插入数据
void test6()
{
	SLTNode* plist = NULL;
	SListPushFront(&plist, 1);
	SListPushFront(&plist, 2);
	SListPushFront(&plist, 3);
	SListPushFront(&plist, 4);
	SListPushFront(&plist, 5);
	SListPrint(plist);


	SLTNode* ptr = SListSearch(plist, 3);
	SListInsertAfter(ptr,666);
	SListPrint(plist);

}

看控制台输出:结果正确

3.10链表增删改查之在pos位置前边插入数据

//在pos位置前边插入数据
void SListInsertBefore(SLTNode** pplist,SLTNode* pos,int num)
{
	assert(pplist && pos);

	if (pos == *pplist)//说明要插入的位置是链表的头部
	{
		//这里就可以偷懒,用我们之前写的头部插入
		SListPushFront(pplist, num);
	}
	else
	{
		SLTNode* prev = NULL;
		SLTNode* cur = *pplist;
		while (cur != pos)
		{
			prev = cur;
			cur = cur->_next;
		}
		//到这里cur == pos,prev指向pos的前一个节点
		SLTNode* newnode = CreateSLTNode(num);
		newnode->_next = pos;
		prev->_next = newnode;
	}
}

 我们写代码测试下是否正确: 

//测试pos前插入数据
void test7()
{
	SLTNode* plist = NULL;
	SListPushFront(&plist, 1);
	SListInsertBefore(&plist,plist,10);
	SListPushFront(&plist, 2);
	SListPushFront(&plist, 4);
	SListPrint(plist);

	SLTNode* ptr = SListSearch(plist, 4);
	SListInsertBefore(&plist, ptr, 666);
	SListPrint(plist);
}

看控制台输出:结果正确

 

 3.11链表增删改查之删除pos位置的数据

// 删除pos位置的值
void SListErase(SLTNode** pplist, SLTNode* pos)
{
	assert(pplist);
	assert(pos);

	if (pos == *pplist)//头部删除
	{
		SListPopFront(pplist);
	}
	else
	{
		SLTNode* prev = *pplist;
		while (prev->_next != pos)
		{
			prev = prev->_next;
		}
		//到这里prev指向pos前一个位置
		prev->_next = pos->_next;
		free(pos);
	}
}

4->您的专属鼓励师

         数据结构和算法修行之路确实枯燥,但是我们把问题搞懂以后就发现他是那样的美妙!一遍学不会没关系吖,多看几遍,我也是学了好多遍呢,小伙伴们肯定学的又快又好!!!最后希望写的内容对小伙伴们有所帮助,我写的如果有哪里不对的地方请在评论区或者私信指出来哦!让我们一起进步吖,任何疑问包括心情不好都可以找我聊聊,我很乐意当你的倾听者吖.

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我叫珂蛋儿吖

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值