数据结构——链表

链表(单链表)

一、链表

1.为什么会有链表

2.链表的概念

3.链表的种类

二、单链表

单链表的实现

1、单链表的初始化

一、链表

1、为什么会有链表

     在上次的文章中,我们讲到了关于顺序表的好处,比如它可以连续存储,它的尾插是很方便的,等等。但是,事物肯定不是十全十美的,顺序表也有它相应的问题:它的头部、中部的插入需要大范围的挪动数据,时间复杂度很高;增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。 3. 增容一般是呈2倍的增长,势必会有一定的空间浪费

    因此先贤们为了解决这些问题,设计出了链表这一数据结构。

2.链表的概念与结构

      链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表
中的指针链接次序实现的 。  
    这种链式的结构是在内存空间中开辟的一个一个空间即 节点(或叫结点),实际上就是如图所视,链式结构在逻辑上是连续的,但在物理上很可能并不连续,这样的结构它们又如何实现链接的呢?
   实际上,因为链表是由一个个的节点( 结构体)构成,这 一个节点被分成两个部分第一部分存放地址内存放的数据,第二部分存入了下一个节点的地址,用指针去访问下一个节点,以此实现链表的连续。
                         

3.链表的种类

实际中链表的结构非常多样,以下情况组合起来就有8种链表结构
1. 单向或者双向
2. 带头或者不带头
3. 循环或者非循环
下面我们从最简单的不带头单向不循环列表开始:

二、单链表

单链表的实现

1、单链表的遍历

             这里我们声明一个指针cur,从头结点指向的第一个结点开始,如果cur不为空就打印该节点里的值,并让cur走到下一个节点,直到走到NULL,循环结束,打印NULL。

void SLTPrint(SLNode* pehead)
{
	SLNode* cur = pehead;//让cur指向头节点,代替头节点遍历
	while (cur != NULL)
	{
		printf("%d->", cur->val);
		cur = cur->next;
	} 
	printf("NULL\n");
}
2.扩容

          与顺序表一样,在插入时链表也需要扩容新节点。这里我们malloc一块新空间,并让新空间的值=x,让新空间指向NULL。

SLNode* CreateNode(SLNDataType x)
{
	SLNode* newNode = (SLNode*)malloc(sizeof(SLNode));
	if (newNode == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	newNode->val = x;
	newNode->next = NULL;
}
3、链表的尾插

             在解决了扩容问题,我们就可以着手实现插入。在实现尾插问题时,我们得先思考一个问题:这个链表是否为空?如果这个链表为空,那么,我们尾插的节点就是头节点,这时我们就可以让**ppehead(头结点的指针)指向newnode;如果这个链表不为空,我们就要找到这个链表的尾节点,在尾节点后插入。因此,我们就需要声明一个指针tail代替**pphead(头节点)实现对链表的遍历。原本尾节点的后面就是NULL,在找到尾节点后就让我们开辟的新节点newNode代替NULL。在新节点后本来也应指向NULL,但是因为在扩容时,我们就已经让newNode->next=NULL;所以无需再指向NULL。

void SLTPushBack(SLNode** ppehead, SLNDataType x)//尾插
{
	SLNode* newNode = CreateNode(x);
	//尾插要先找尾节点
	{
		*ppehead = newNode;
	}
	else
	{
		//假设链表不为空
		SLNode* tail = *ppehead;
		while (tail->next != NULL)
		{
			tail = tail->next;
		}
		//接着扩容一个新节点,让新节点指向空
		tail->next = newNode;
	}
}
4.为什么要使用二级指针(可忽略

          在最开始我们声明头节点(pehead),但是在尾插时我们使用的是*ppehead,即头节点pehead的指针。

           相信大家都应该写过交换函数,我们在交换时,我们是调用的主函数外独立的一个函数,那么,在原函数里定义的临时变量出了该函数还有作用吗?这时我们是将主函数中我们要传递的数值传递过去吗?我们知道形参是实参的临时拷贝,所以我们要传递的是指针,即要交换的数的地址。

            所以在对SLTPushBack函数调用时,我们也要传递plist的地址(&plist),但是pehead已经是头节点的指针(一级指针),所以我们只能用二级指针来确保我们成功调用该函数,得到想要的结果;

5.头插

         在顺序表中,我们知道要实现头插需要大量的挪动数据,极大的影响效率。相较之下链表的头插就显得简单的多。

         只要插入就一定需要开辟新节点,在开辟了新节点newNode后,这里我们只需要让新节点插到头节点之前,然后让新节点成为头节点即可。

void SLTPushFront(SLNode** ppehead, SLNDataType x)
{
	SLNode* newNode = CreateNode(x);
	newNode->next = *ppehead;
	*ppehead = newNode;
}
6.尾删

           尾删我们也要考虑一个问题,是不是直接找到尾节点删除掉就可以了?如果删除了尾节点,但是尾节点的上一个节点还指向原尾节点,这样指针不就成野指针了?因此我们必须先让尾节点的前一个节点指向NULL才能放心的尾删。

           尾删这里我们有两种途径:1、双指针法,我们可以声明两个指针prev和tail。tail用来遍历数组找到尾节点,tail到尾节点时,prev是尾节点的前一个,这时我们就可以free掉尾节点,然后让prev保存的节点成为尾节点,让其指向空。

void SLTPopBack(SLNode** ppehead)
{
	assert(*ppehead);
	SLNode* prev = NULL;
	SLNode* tail = *ppehead;
	if ((*ppehead)->next == NULL)
	{
		free(*ppehead);
		*ppehead = NULL;
	}
	else//多个节点,只有一个节点时,prev就是空了;
	{
		//找尾
		while (tail->next != NULL)
		{
			prev = tail;
			tail = tail->next;
		}
		free(tail);
		prev->next = NULL;
	}
}

         2.单指针法:这里我们在循环时让tail->next->next != NULL作为循环继续的条件。当tail->next->next走到NULL时,tail->next刚好是尾节点,tail是尾节点的上一个节点,这时只要将         tail->next给free掉,这时tail成为尾节点,让tail->next=NULL即可。

void SLTPopBack(SLNode** ppehead)
{
	assert(*ppehead);
	SLNode* tail = *ppehead;
	if ((*ppehead)->next == NULL)
	{
		free(*ppehead);
		*ppehead = NULL;
	}
	else//多个结点
	{
		//找尾
		 while (tail->next->next != NULL)//第二种写法
		{
			tail = tail->next;
		}
		free(tail->next);
		tail->next = NULL;
	}
}

       但是,无论用哪种方式,我们依旧要考虑到,上述方法只适合多个节点。如果链表只有一个节点,我们就只能free头节点(free后依旧要让*ppehead指向NULL,防止野指针)。

7.头删

         链表对于头部的处理都很有意思,头删我们可以声明一个指针tmp,让tmp 保存头节点,然后*ppehead走到下一个节点,再free掉tmp,原头节点就被删掉了,*ppehead成为了新头节点。

void SLTPopFront(SLNode** ppehead)
{
	assert(*ppehead);
	SLNode* tmp = *ppehead;
	*ppehead = (*ppehead)->next;
	free(tmp);
}
8、查找

              遍历这个链表,如果某个节点的值=x,就返回这个节点,否则就继续找。

 SLNode* SLTFind(SLNode* pehead, SLNDataType x)
{
	SLNode* cur = pehead;
	while (cur)
	{
		if (cur->val == x)
		{
			return cur;
		}
		else
		{
			cur = cur->next;
		}
	}

}
9、任意插入

             任意插入函数与头插尾插没有大的区别。当pos=0时,就是头插。在其他点插入也就是遍历到pos节点,然后插入。

void SLTInsert(SLNode** ppehead, SLNode* pos, SLNDataType x)
{
	assert(*ppehead);
	assert(pos);
	SLNode * newNode = CreateNode(x);

	//分情况
	//pos=0,头插
	if (*ppehead==pos)
	{
		SLTPushFront(*ppehead, x);
	}
	else
	{
		SLNode* prev = *ppehead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		prev->next = newNode;
		newNode->next = pos;
	}
}
10.任意删除
void SLTErase(SLNode** ppehead, SLNode* pos)
{
	//分情况
	if (*ppehead == pos)
	{
		//头删
		SLTPopFront(*ppehead);
	}
	else
	{
		SLNode* prev = *ppehead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		prev->next = pos->next;
		free(pos);
		pos = NULL;
	}
}

11.test函数

这里是对单链表建立后的测试。

void test1()
{
	SLNode* plist = NULL;
	SLTPushBack(&plist, 1);
	SLTPushBack(&plist, 2);
	SLTPushBack(&plist, 3);
	SLTPushBack(&plist, 4);
	SLTPushBack(&plist, 5);

	SLTPrint(plist);

	/*SLTPushFront(&plist, 9);
	SLTPushFront(&plist, 8);
	SLTPushFront(&plist, 7);
	SLTPushFront(&plist, 6);*/

	/*SLTPrint(plist);*/
}
void test2()
{
	SLNode* plist = NULL;
	SLTPushBack(&plist, 1);
	SLTPushBack(&plist, 2);
	SLTPushBack(&plist, 3);
	SLTPushBack(&plist, 4);
	SLTPushBack(&plist, 5);

	SLTPrint(plist);

	SLTPopBack(&plist);
	SLTPrint(plist);
	SLTPopBack(&plist);
	SLTPrint(plist);
	SLTPopBack(&plist);
	SLTPrint(plist);
	SLTPopBack(&plist);
	SLTPrint(plist);
	SLTPopBack(&plist);
	SLTPrint(plist);
     

}
void test3()
{
	SLNode* plist = NULL;

	SLTPushBack(&plist, 1);
	SLTPushBack(&plist, 2);
	SLTPushBack(&plist, 3);
	SLTPushBack(&plist, 4);
	SLTPushBack(&plist, 5);
	SLTPrint(plist);

	SLTPopFront(&plist);
	SLTPrint(plist);

	SLNode * pos = SLTFind(plist, 3);
	SLTInsert(&plist, pos, 12);
	SLTPrint(plist);
}
void test4()
{
	SLNode* plist = NULL;

	SLTPushBack(&plist, 1);
	SLTPushBack(&plist, 2);
	SLTPushBack(&plist, 3);
	SLTPushBack(&plist, 4);
	SLTPushBack(&plist, 5);
	SLTPrint(plist);

	SLTPopFront(&plist);
	SLTPrint(plist);

	SLNode* pos = SLTFind(plist, 3);
	SLTErase(&plist, pos);
	SLTPrint(plist);
}
int main()
{
	test1();
	//test2();
	//test3();
	//test4();
	return 0;
}

小结:本文简单介绍了链表,并对单链表的进行了简单的实现。如有不妥欢迎指出。

  • 23
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值