线性表——单链表

  在前面的顺序表中我们提到了顺序表的优缺点,其中最大的不足是插入和删除元素需要移动大量的数据,这就对时间有一定的消耗。为了解决这个不足,本章将介绍一个成员——链表,接下来就开始本章的介绍。

1. 链式存储结构

  链式存储结构 特点: 用一组任意的存储单元存储线性表的数据元素,这组存储单元可以是连续的,也可以是不连续的。
  在链式结构中,除了要存储数据外,还需要存储下一个数据元素的地址,这样才能找到下一个元素,不然就是一盘散沙。我们把存储数据元素信息的域称为 数据域 ,存储后继元素地址的域称为 指针域 。这两个域在一起组成链表的一个元素,称之为 结点

  头指针是指向头结点的指针(若没有头结点就直接指向第一个结点),只用来存储地址。
  头结点单链表 第一个结点前附设的一个结点(有时候也称为“ 哨兵 ”),目的是方便对链表的操作,它一样有数据域和指针域,而数据域可以不存储任何信息,当然也可以存储线性表长度或其他信息。
  单链表像火车车厢一样,车厢(结点)之间相互连接,有头就要有尾,最后一个结点不用指向其他元素,在指针域中放入NULL 即可,也相当于指向 NULL,以此收尾。

  头结点通常不是必须的,具体根据实际需求使用,而头指针是必须的,并且不为空。还一种情况,当头指针为空时,说明整个链表为空。

图解:

链式存储

2. 单链表的实现(无头结点)

  

2.1 结点的定义

typedef int LinkDataType;

typedef struct LinkNode		//单链表
{
	int val;	//数据域的信息
	struct LinkNode* next;	//指针域的信息
}LNode;

2.2 单链表的查找

  链表查找不需要对修改数据,所以直接传参,就不用再取地址(使用二级指针)。

LNode* LinkFind(LNode* phead, LinkDataType x)	//查找
{
	/*    注意返回值类型为LNode*    */

	LNode* cur = phead;
	while (cur)
	{
		if (cur->val == x)
		{
			return cur;	//找到了返回结点地址
		}
		else
		{
			cur = cur->next;
		}
	}
	return NULL;	//找不到返回空
}

2.3 单链表的插入

  单链表的插入需要创建新的结点,我们可以对此操作进行封装。

2.3.1 新结点的创建

LNode* CreateNode(LinkDataType x)
{
	LNode* newnode = (LNode*)malloc(sizeof(LNode));	//为新结点开辟空间
	if (newnode == NULL)	//检查是否开辟成功
	{
		perror("malloc fail!");
		return -1;
	}
	newnode->val = x;		//数据域存入x
	newnode->next = NULL;	//指针域暂时指向NULL
}

2.3.2 头部插入(头插)

  这里我们创建的是一个 LNode*类型的指针:LNode* plist = NULL;所以我们就要使用二级指针来对一级指针进行操作。若外部直接创建的是LNode类型的变量,就说明创建的是一个带有头结点的链表。

就像在外部定义了 int a = 10 ;由于形参是实参的一份临时拷贝,形参的改变并不会影响实参,所以不会直接传递 int 类型,而会传递 int* ;因此,遇见 int* a = NULL;就传递 int**

void LinkPushFront(LNode** pphead, LinkDataType x)	//头插
{
	assert(pphead);		//检查pphead是否为空

	LNode* newnode = CreateNode(x);	//创建新结点
	newnode->next = *pphead;		//新结点的指针域存放原链表第一个结点的地址
	*pphead = newnode;			//*pphead再指向第一个结点
}

图解:
头插


2.3.3 尾部插入(尾插)

  尾插就需要判断链表是否为空

void LinkPushBack(LNode** pphead, LinkDataType x)	//尾插
{
	assert(pphead);

	LNode* newnode = CreateNode(x);	//新结点

	if (*pphead == NULL)
	{
		*pphead = newnode;		//如果链表为空就直接指向新结点
	}
	else
	{
		LNode* tail = *pphead;
		while (tail->next != NULL)
		{
			tail = tail->next;	//找尾(最终会在最后一个结点跳出循环)
		}
		tail->next = newnode;	//原链表最后一个结点指向新结点
	}
}

图解:
尾插


2.3.4 任意位置(前)插入

  这部分再在任意位置之 插入,需要结合链表查找的返回值进行。另外,关于pos,有些地方使用的int类型,这里根据需要自行变换。

void LinkInsert(LNode** pphead, LNode* pos, LinkDataType x)	//任意位置前插入
{
	//检查1:
	assert(pphead);
	assert(*pphead);	//检查链表是否为空
	assert(pos);	//pos是链表的一个有效结点(pos不为空)

	//检查2:
	assert((!pos && !(*pphead)) || (pos && *pphead));	//pos 和 *pphead 保持一致(都为空或都不为空)
	
	/*  检查1,检查2 根据需要任选其一即可  */
	
	if (*pphead == pos)		//pos为第一个结点时
	{
		LinkPushFront(pphead, x);	//头插
	}
	else
	{
		LNode* prev = *pphead;
		while (prev->next != pos)	//寻找 pos 的位置
		{
			prev = prev->next;
		}
		//插入:
		LNode* newnode = CreateNode(x);
		prev->next = newnode;
		newnode->next = pos;
	}
}

图解:
前插


2.3.5 任意位置(后)插入

  任意位置后插入就不用考虑头插的情况,也不用将链表传过来。

void LinkInsertAfter(LNode* pos, LinkDataType x)	//任意位置后插入
{
	assert(pos);
	LNode* newnode = CreateNode(x);

	newnode->next = pos->next;	//新结点指向原链表指向的位置
	pos->next = newnode;	//原链表指向新结点的位置
}

图解:
后插


2.4 单链表的删除

2.4.1 头部删除(头删)

void LinkPopFront(LNode** pphead)	//头删
{
	assert(pphead);
	assert(*pphead);
	
	//方法1:
	LNode* tail = (*pphead)->next;	//新建指针指向第二个结点
	free(*pphead);	//释放第一个结点空间
	*pphead = tail;	//头指针指向原链表第二个结点

	//方法2:
	//LNode* tail = *pphead;	//新建指针指向第一个结点
	//*pphead = (*pphead)->next;	//头指针指向第二个结点
	//free(tail);		//释放第一个结点空间
}

2.4.2 尾部删除(尾删)

void LinkPopBack(LNode** pphead)	//尾删
{
	assert(pphead);
	assert(*pphead);

	//方法1:
	//1.一个结点
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	//2.多个结点
	else
	{
		LNode* tail = *pphead;
		LNode* prev = NULL;
		while (tail->next != NULL)
		{
			prev = tail;
			tail = tail->next;
		}
		/* 跳出循环 prev 停在倒数第二个结点,tail 停在最后一个结点 */
		
		free(tail);		//删除结点
		tail = NULL;

		prev->next = NULL;
	}

	//方法2:
	// /* 先删除,再判断链表的情况 */
	//LNode* tail = *pphead;
	//LNode* prev = NULL;
	//while (tail->next != NULL)
	//{
	//	prev = tail;
	//	tail = tail->next;
	//}
	//free(tail);
	//tail = NULL;

	//if (prev == NULL)
	//{
	//	*pphead = NULL;
	//}
	//else
	//{
	//	prev->next = NULL;
	//}

	//方法3:
	//(尽量使用以上两种)
	//LNode* tail = *pphead;
	//while (tail->next->next != NULL)
	//{
	//	tail = tail->next;
	//}
	/* tail在倒数第二个结点跳出循环 */
	//free(tail->next);
	//tail->next = NULL;

}

2.4.3 任意位置(前)删除

  需结合查找功能使用。

void LinkErase(LNode** pphead, LNode* pos)	//任意位置前删除
{
	assert(pphead);
	assert(*pphead);
	assert(pos);

	if (*pphead == pos)
	{
		LinkPopFront(pphead);	//头删
	}
	else
	{
		LNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		prev->next = pos->next;	// pos 前的结点直接指向 pos 后的结点
		free(pos);
		pos = NULL;
	}
}

2.4.4 任意位置(后)删除

  需结合查找功能使用。

void LinkEraseAfter(LNode* pos)		//任意位置后删除
{
	assert(pos);
	assert(pos->next);	//检查pos是否为最后一个结点

	//方法1:
	LNode* tail = pos->next->next;
	free(pos->next);
	pos->next = tail;

	//方法2:
	//LNode* tail = pos->next;
	//pos->next = pos->next->next;

	//free(tail);
	//tail = NULL;
}

2.5 单链表的打印

void LinkPrint(LNode* phead)
{
	LNode* cur = phead;
	while (cur != NULL)
	{
		printf("%d->", cur->val);
		cur = cur->next;
	}
	printf("NULL\n");
}

2.6 单链表的销毁

void LinkDestory(LNode** pphead)	//销毁
{
	assert(pphead);

	LNode* cur = *pphead;
	while (cur)
	{
		LNode* Next = cur->next;
		free(cur);
		cur = Next;
	}
	*pphead = NULL;
}

3. 功能综合

  以下综合会比较长,可以另外通过接口实现会更加清楚,可自行更改。

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

typedef int LinkDataType;

typedef struct LinkNode		//单链表
{
	int val;	//数据域的信息
	struct LinkNode* next;	//指针域的信息
}LNode;

LNode* LinkFind(LNode* phead, LinkDataType x)	//查找
{
	/*    注意返回值类型为LNode*    */

	LNode* cur = phead;
	while (cur)
	{
		if (cur->val == x)
		{
			return cur;	//找到了返回结点地址
		}
		else
		{
			cur = cur->next;
		}
	}
	return NULL;	//找不到返回空
}

LNode* CreateNode(LinkDataType x)
{
	LNode* newnode = (LNode*)malloc(sizeof(LNode));	//为新结点开辟空间
	if (newnode == NULL)	//检查是否开辟成功
	{
		perror("malloc fail!");
		return -1;
	}
	newnode->val = x;		//数据域存入x
	newnode->next = NULL;	//指针域暂时指向NULL
}

void LinkPushFront(LNode** pphead, LinkDataType x)	//头插
{
	assert(pphead);		//检查pphead是否为空

	LNode* newnode = CreateNode(x);	//创建新结点
	newnode->next = *pphead;		//新结点的指针域存放原链表第一个结点的地址
	*pphead = newnode;			//*pphead再指向第一个结点
}

void LinkPushBack(LNode** pphead, LinkDataType x)	//尾插
{
	assert(pphead);

	LNode* newnode = CreateNode(x);	//新结点

	if (*pphead == NULL)
	{
		*pphead = newnode;		//如果链表为空就直接指向新结点
	}
	else
	{
		LNode* tail = *pphead;
		while (tail->next != NULL)
		{
			tail = tail->next;	//找尾(最终会在最后一个结点跳出循环)
		}
		tail->next = newnode;	//原链表最后一个结点指向新结点
	}
}

void LinkInsert(LNode** pphead, LNode* pos, LinkDataType x)	//任意位置前插入
{
	//检查1:
	assert(pphead);
	assert(*pphead);	//检查链表是否为空
	assert(pos);	//pos是链表的一个有效结点(pos不为空)

	//检查2:
	assert((!pos && !(*pphead)) || (pos && *pphead));	//pos 和 *pphead 保持一致(都为空或都不为空)

	/*  检查1,检查2 根据需要任选其一即可  */

	if (*pphead == pos)		//pos为第一个结点时
	{
		LinkPushFront(pphead, x);	//头插
	}
	else
	{
		LNode* prev = *pphead;
		while (prev->next != pos)	//寻找 pos 的位置
		{
			prev = prev->next;
		}
		//插入:
		LNode* newnode = CreateNode(x);
		prev->next = newnode;
		newnode->next = pos;
	}
}

void LinkInsertAfter(LNode* pos, LinkDataType x)	//任意位置后插入
{
	assert(pos);
	LNode* newnode = CreateNode(x);

	newnode->next = pos->next;	//新结点指向原链表指向的位置
	pos->next = newnode;	//原链表指向新结点的位置
}

void LinkPopFront(LNode** pphead)	//头删
{
	assert(pphead);
	assert(*pphead);

	//方法1:
	LNode* tail = (*pphead)->next;	//新建指针指向第二个结点
	free(*pphead);	//释放第一个结点空间
	*pphead = tail;	//头指针指向原链表第二个结点

	//方法2:
	//LNode* tail = *pphead;	//新建指针指向第一个结点
	//*pphead = (*pphead)->next;	//头指针指向第二个结点
	//free(tail);		//释放第一个结点空间
}

void LinkPopBack(LNode** pphead)	//尾删
{
	assert(pphead);
	assert(*pphead);

	//方法1:
	//1.一个结点
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	//2.多个结点
	else
	{
		LNode* tail = *pphead;
		LNode* prev = NULL;
		while (tail->next != NULL)
		{
			prev = tail;
			tail = tail->next;
		}
		/* 跳出循环 prev 停在倒数第二个结点,tail 停在最后一个结点 */

		free(tail);		//删除结点
		tail = NULL;

		prev->next = NULL;
	}

	//方法2:
	// /* 先删除,再判断链表的情况 */
	//LNode* tail = *pphead;
	//LNode* prev = NULL;
	//while (tail->next != NULL)
	//{
	//	prev = tail;
	//	tail = tail->next;
	//}
	//free(tail);
	//tail = NULL;

	//if (prev == NULL)
	//{
	//	*pphead = NULL;
	//}
	//else
	//{
	//	prev->next = NULL;
	//}

	//方法3:
	//(尽量使用以上两种)
	//LNode* tail = *pphead;
	//while (tail->next->next != NULL)
	//{
	//	tail = tail->next;
	//}
	/* tail在倒数第二个结点跳出循环 */
	//free(tail->next);
	//tail->next = NULL;

}

void LinkErase(LNode** pphead, LNode* pos)	//任意位置前删除
{
	assert(pphead);
	assert(*pphead);
	assert(pos);

	if (*pphead == pos)
	{
		LinkPopFront(pphead);	//头删
	}
	else
	{
		LNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		prev->next = pos->next;	// pos 前的结点直接指向 pos 后的结点
		free(pos);
		pos = NULL;
	}
}

void LinkEraseAfter(LNode* pos)		//任意位置后删除
{
	assert(pos);
	assert(pos->next);	//检查pos是否为最后一个结点

	//方法1:
	LNode* tail = pos->next->next;
	free(pos->next);
	pos->next = tail;

	//方法2:
	//LNode* tail = pos->next;
	//pos->next = pos->next->next;

	//free(tail);
	//tail = NULL;
}

void LinkPrint(LNode* phead)
{
	LNode* cur = phead;
	while (cur != NULL)
	{
		printf("%d->", cur->val);
		cur = cur->next;
	}
	printf("NULL\n");
}

void LinkDestory(LNode** pphead)	//销毁
{
	assert(pphead);

	LNode* cur = *pphead;
	while (cur)
	{
		LNode* Next = cur->next;
		free(cur);
		cur = Next;
	}
	*pphead = NULL;
}

int main()
{
	LNode* plist = NULL;
	LinkPushBack(&plist, 1);
	LinkPushBack(&plist, 2);
	LinkPushBack(&plist, 3);
	LinkPushBack(&plist, 4);

	LinkPrint(plist);

	LNode* pos = LinkFind(plist, 1);
	LinkErase(&plist, pos);
	LinkPrint(plist);

	pos = LinkFind(plist, 3);
	LinkErase(&plist, pos);
	LinkPrint(plist);
	return 0;
}

4. 链表和顺序表的选择

  顺序表的优点主要是访问方便,而链表主要是在空间的扩容上和对数据的增删上占优势。

  • 当线性表需要大量对数据的增加和删除,同时对查找要求不高时使用链表;反之使用顺序表。
  • 当我们对元素的个数及变化范围不确定时,尽量使用单链表。
  • 27
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值