0
点赞
收藏
分享

微信扫一扫

数据结构杂谈(三)

君之言之 2022-02-02 阅读 30

写在前面:需要注意的是,本博主采用的是C++的编程语言

3 单链表


文章目录

3.1 单链表的定义

3.1.1 引入

线性表有两种,第一个是我们前面讲到的顺序表,对应顺序存储。第二个是链表,对应链式存储

物理结构逻辑结构
顺序表顺序存储
链表链式存储

要谈论链表,我们就要先谈最简单的链表;所以在这里首先要提出一个单链表的概念。单链表也叫线性链表

2.1.2 单链表和顺序表的优劣

顺序表优缺点单链表优缺点
顺序表优点:可随机存取,存储密度高单链表优点:不要求大片连续空间,改变容量方便
缺点:要求大片连续空间,改变容量不方便。缺点:不可随机存取,要耗费一定空间存放指针。

2.1.3 单链表的代码定义

在前面我们说过单链表中的每个节点都包含一个数据域和一个指针域,所以在C++中我们常常用结构体的方式去定义某个节点。

//定义一个结点
typedef struct LNode{		//定义单链表结点类型
	ElemType data; //每个节点存放一个数据元素
	struct LNode *next; //指针指向下一个结点
}LNode*, LinkList;

3.2 单链表的初始化

对于链表来说是需要初始化的,这是因为结点中可能含有脏数据。对于单链表初始化即构造一个空表。

这里我们要分为两类情况,一类是不带头结点的单链表,一类是带头结点的单链表。下面先说说两者的区别:

  1. 所有的链表都要有个头指针first,带头结点的链表的头指针指向的是头结点,头结点的指针域指向首元结点,不带头结点的头指针直接指向首元结点。
  2. 两者在操作上有区别:在删除和插入操作中,无论删除和插入的位置如何,带头结点的链表不需要修改头指针的值,而不带头结点的有时候需要。在清空操作中,带头结点的保留头结点,而不带头结点的要销毁。.
  3. 在结构上,带头结点的单链表,不管链表是否为空,均含有一个头结点,不带头结点的单链表不含头结点。
  4. 在操作上,带头结点的单链表的初始化为申请一个头结点。无论插入或删除的位置是地第一个结点还是其他结点,算法步骤都相同。不带头结点的单链表,其算法步骤要分别考虑插入或删除的位置是第一个结点还是其他结点。

3.2.1 不带头结点的单链表

由于单链表不带头结点,这就导致了如果初始化表,那就是表全为空。其代码定义如下:

#include <iostream>
using namespace std;

//定义链表(不带头结点)
typedef struct  
{
	//数据域
	int data;

	//指针域
	struct LNode* next;
}LNode,* LinkList;

//初始化链表
bool InitList(LinkList& L)
{
	L = NULL;
	return true;
}

int main()
{
	LinkList L;
	InitList(L);
}

此时如果要判断单链表是否为空,只需单纯判断L是否为空即可。

//判断单链表是否为空
bool Empty(LinkList L)
{
	if (L == NULL)
		return true;
	else
		return false;
}

3.2.2 带头结点的单链表

  1. 生成新结点作头结点,用头指针L指向头结点。
  2. 将头结点的指针域置空,防止内存中有遗留的脏数据。
#include <iostream>
using namespace std;

//定义链表(不带头结点)
typedef struct LNode
{
	//数据域
	int data;

	//指针域
	struct LNode* next;
}LNode,* LinkList;

//初始化链表
bool InitList(LinkList& L)
{
	L = new LNode;
	if (L == NULL)//这里为了防止申请内存不足
		return false;
	L->next = NULL;
	return true;
}


int main()
{
	LinkList L;
	InitList(L);
}

此时如果想判断单链表是否为空,只需判断头结点中储存的指针域是否为空即可。

//判断单链表是否为空
bool Empty(LinkList L)
{
	if (L->next == NULL)
		return true;
	else
		return false;
}

3.3 单链表的插入

在下面的基本操作中,我们需要知道一些比较特殊的步骤,有个这些步骤,即使不看源码,你也能写出类似的代码。

p = L;//p指向头结点
s = L->next;//s指向首元结点
p = p->next;//p指向下一结点

这里还要多插一句:为了让我们的代码更具健壮性,我们应该多考虑极端情况;为了避免重复代码,使我们的代码简洁易维护,我们应该把基本操作封装成一个函数。

3.3.1 带头结点的单链表插入

单链表插入原理图如下:

image-20220201151552276

以下给出代码实现:

//按位插入
bool ListInsert(LinkList& L, int i, int e)
{
	if (i < 1)
		return false;
	LNode* p;//生成指针p,用于指向插入端的前一个结点
	int j = 0;//用于扫描计数
	p = L;//p初始化位置指向头结点

	//移动p到插入位置的前一个结点
	while (p!=NULL && j< i-1)
	{
		p = p->next;
		j++;
	}

	//p不能移出链表之外
	if (p == NULL)
		return false;

	//生成要插入的新结点
	LNode* s = new LNode;
	s->data = e;
	s->next = p->next;
	p->next = s;
	return true;
}

3.3.2 不带头结点的单链表插入

实际上,不带头结点的单链表插入原理和带头结点的差不多,只是在第1个位置需要做特殊处理,因为头指针指着第一个元素。为此,我们要在带头结点的单链表插入代码中添加如下代码:

if (i == 1)
	{
		LNode* s = new LNode;
		s->data = e;
		s->next = L;
		L = s;
		return true;
	}

3.3.3 指定结点后插操作

对于某一个结点,我们想要在其后插入一个新节点,代码如下:

bool InsertNextNode(LNode* p, int e)
{
	if (p == NULL)
		return false;
	LNode* s = new LNode;

	//内存不足判断
	if (s == NULL)
		return false;
	s->data = e;
	s->next = p->next;
	p->next = s;
	return true;
}

3.3.4 指定结点前插操作

对于后插来说,实际上根据指定的结点是可以找到下一个结点的。可以对于前插来说,指定的结点是不能找到前一个结点的,这是因为结点中只存放了下一个结点的指针,而没有存放上一个结点的指针域。

那么如何解决这个问题呢?我们可以用后插模仿成前插,什么意思呢?意思就是后插一个结点,然后把前一个结点的数据拷贝到后一个结点,然后对前一个结点赋值即可做成前插的效果。而且这种思路的时间复杂度为O(1),用了都说好。代码实现如下:

bool InsertPriorNode(LNode* p, int e)
{
	if (p == NULL)
		return false;
	LNode* s = new LNode;
	if (s == NULL)
		return false;
	s->next = p->next;
	p->next = s;
	s->data = p->data; 
	p->data = e;
	return true;
}

3.4 单链表的删除

3.4.1 带头结点的单链表删除

bool ListDelete(LinkList& L, int i, int& e)
{
	if (i < 1) //索引处于链表之外
		return false;
	LNode* p;
	int j = 0;
	p = L;
	while (p != NULL && j < i - 1)
	{
		p = p->next;
		j++;
	}
	if (p == NULL)//索引处于链表右边之外
		return false;
	if (p->next == NULL)
		return false;
	LNode* q = p->next;
	e = q->data;
	p->next = q->next;
	delete(q);
	return true;
}

3.4.2 指定结点的删除

//删除指定结点
bool DeleteNode(LNode* p)
{
	if (p == NULL)
		return false;
	LNode* q = p->next;
	p->data = p->next->data;
	p->next = q->next;
	delete(q);
	return true;
}

实际上,上面提供的代码具有BUG,因为如果p是最后一个结点,那么p->next是空值,无法提供data,这个时候只能从表头开始依次寻找p的前驱,时间复杂度为O(n)。

3.5 单链表的查找

3.5.1 按位查找

实际上对于按位查找,无非就是从头找到尾,直到找到第i个元素位置。由于这个算法的时间复杂度取决于i的位置,所以有最好情况最坏情况

//按位查找
LNode* GetElem(LinkList L, int i)
{
	if (i < 0)
		return NULL;
	LNode* p;
	p = L;
	int j = 0;
	while (p != NULL && j < i)
	{
		p = p->next;
		j++;
	}
	return p;
}

3.5.2 按值查找

//按值查找
LNode* LocateElem(LinkList& L, int e)
{
	LNode* p = L->next;
	while (p != NULL && p->data != e)
		p = p->next;
	return p;
}

3.5.3 求单链表长度

//求单链表长度
int Length(LinkList L)
{
	int len = 0;
	LNode* p = L; //初始化指针于头结点位置
	while (p->next != NULL)
	{
		p = p->next;
		len++;
	}
	return len;
}

3.6 单链表的建立

如果给你很多个数据元素,要把它们存到一个单链表里边,怎么达到目的呢?

3.6.1 尾插法

尾插法没什么好讲的,利用我们前面所讲的定义、初始化、插入,即可完成尾插法,需要注意的是,每次插入一个元素,需要新指定一个变量length来统计表的长度。

但是如果使用普通的遍历插入,每次插入都会涉及到while循环,时间复杂度为 O ( n 2 ) O(n^2) O(n2),这样的算法明显太垃圾了。

于是我们又思考前面在学习插入的时候,我们使用过一个叫做指定结点的后插操作。其步骤如下:

  1. 从一个空表L开始,将新结点逐个插入到链表的尾部,尾指针r指向链表的尾结点。
  2. 初始时,r同L均指向头结点。每读入一个数据元素则申请一个新结点,将新结点插入到尾结点后,r指向新结点。

为了更直观地看懂这个过程,我特意花了个图:

image-20220129152806386

代码示例如下:

//正位序输入n个元素的值,建立带表头结点的单链表L
void CreateList_R(LinkList &L,int n){
	L = new LNode;
	L->next = NULL;
	r = L; //尾指针r指向头结点
	for(i = 0;i<n;i++){
		p = new LNode;
		cin>>p->data; //生成新结点,输入元素值
		p->next = NULL;
		r->next = p; //插入到表尾
		r= p; //r指向新的尾结点
	}
}//CreateList_R

3.6.2 头插法

头插法也很好理解,本质是插入的位置一直处于头结点之后,且使用指定结点的后插操作。其基本步骤如下:

  1. 从一个空表开始重复读入数据
  2. 生成新结点,将读入数据存放到新结点的数据域中
  3. 从最后一个结点开始,依次将各结点插入到链表的前端

对于头插法我也画了个图:

image-20220129151919386

其代码示例如下:

void CreateList_H(LinkList &L,int n){
	L = new LNode;
	L->next = NULL;
	for(i = n;i>0;i--){
		p = new LNode;
		cin>>p->data;
		p->next = L->next;
		L->next = p;
	}
}//CreateList_H

3.7 补充算法

3.7.1 单链表的销毁

【算法思路】从头指针开始,依次释放所有结点

image-20211020152351152

我们销毁的思路是:我们还需要另外一个指针变量P,这个指针变量用于结点的操作。若想实现变量P对某结点的操作,首要任务就是让P指向该结点,即把该结点的地址赋给P。那该节点的地址存于头指针L,所以只需p = L即可。当然,当P = L后,不能立马删除p,否则L丢失,链表也跟着丢失;所以我们需要在P = L后,把L移到下一个结点,即L = L->next,然后再释放P(free§)即可。循环上述操作,即可删除链表。


【算法描述】

Status DestroyList_L(LinkList &L){
	//销毁单链表L
	LNode *p;
	while(L){
		p = L;
		L = L->next;
		delete(p);
	}
	return Ok;
}

3.7.2 清空链表

【算法思路】依次释放所有结点,并将头结点指针域设置为空。

image-20211020163023378

先将头指针的指针域赋给指针变量p,这样的话,p就定位了要删除的结点了,但是如果现在直接删除,那么后面的链表就会丢失了。所以这时候我们引入第三个指针变量q,q来保证后面的链表不丢失,当我们q移到p要删除结点的下一个结点后,即q = p->next,我们再去释放p,即delete§。直到清空列表为止。


【算法描述】

Status ClearList(LinkList &L){
	LNode *p,*q;
	p = L->next;
	while(p){
		p=q->next;
		free(p);
		p = q;
	}
	L->next = NULL; //头结点指针域为空
	return OK;
}
举报

相关推荐

0 条评论