数据结构与算法之线性表基础——单链表(C与C++双人打)

单链表

链表是线性表的链式存储方式。逻辑上相邻的数据在计算机内的存储位置不一定相邻

单链表的存储方式

可以给每个元素附加一个指针域,指向下一个元素的存储位置
在这里插入图片描述
每个节点包含两个域:数据域和指针域。数据域存储数据元素,指针域存储下一个节点的地址,因此指针指向的类型也是节点类型。每个指针都指向下一个节点,都是朝一个方向的,这样的链表称为单向链表或单链表。

单链表的节点结构体定义

在这里插入图片描述
定义了节点结构体之后,就可以把若干个节点连接在一起,形成一个单链表
在这里插入图片描述
不管这个铁链有多长,只要找到它的头,就可以拉起整个铁链。因此,只要给这个单链表设置一个头指针,这个链表中的每个节点就都可以找到了
在这里插入图片描述
有时为了操作方便,还会给链表增加一个不存放数据的头节点(也可以存放表长等信息)
在这里插入图片描述
有的书中还提到了首元节点,即第一个数据元素节点。其实完全没必要混淆视听,只需要知道头指针、头节点就可以了。

在顺序表中,想找第i个元素,可以立即通过L.elem[i-1]找到,想找哪个就找哪个,称为随机存取。但是在单链表中,想找第i个元素就没那么容易,必须从头开始,按顺序一个一个找,一直数到第i个元素,称为顺序存取。

单链表的存储结构

在这里插入图片描述

LinkList和LNode*的区别

(1)这里定义的是单链表中每个结点的存储结构,它包括两部分:存储结点的数据域data,其类型用通用类型标识符ElemType表示;存储后继结点位置的指针域next,其类型为指向结点的指针类型LNode*。

(2)为了提高程序的可读性,在此对同一结构体指针类型起了两个名称,LinkList与LNode *,两者本质上是等价的。通常习惯上用LinkList定义单链表,强调定义的是某个单链表的头指针;用LNode *定义指向单链表中任意结点的指针变量。

例如,若定义LinkList L,则L为单链表的头指针,若定义LNode *p,则p为指向单链表中某个结点的指针,用*p代表该结点。当然也可以使用定义LinkList p,这种定义形式完全等价于LNode *p。

(3)单链表是由表头指针唯一确定的,因此单链表可以用头指针的名字来命名。若头指针名是L,则简称该链表为表L。

(4)注意区分指针变量和结点变量两个不同的概念,若定义LinkList p或LNode *p,则p为指向某结点的指针变量,表示该结点的地址;而*p为对应的结点变量,表示该结点的名称。
在这里插入图片描述

头结点和头指针的区别

(1)首元结点是指链表中存储第一个数据元素a1的结点。如图所示的结点“ZHAO”。

(2)头结点是在首元结点之前附设的一个结点,其指针域指向首元结点。头结点的数据域可以不存储任何信息,也可存储与数据元素类型相同的其他附加信息。例如,当数据元素为整数型时,头结点的数据域中可存放该线性表的长度。

(3)头指针是指向链表中第一个结点的指针。若链表设有头结点,则头指针所指结点为线性表的头结点;若链表不设头结点,则头指针所指结点为该线性表的首元结点。

链表增加头结点的作用如下

(1)便于首元结点的处理增加了头结点后,首元结点的地址保存在头结点(即其“前驱”结点)的指针域中,则对链表的第一个数据元素的操作与其他数据元素相同,无需进行特殊处理。

(2)便于空表和非空表的统一处理当链表不设头结点时,假设L为单链表的头指针,它应该指向首元结点,则当单链表为长度n为0的空表时,L指针为空(判定空表的条件可记为:L==NULL)。

增加头结点后,无论链表是否为空,头指针都是指向头结点的非空指针。如图(a)所示的非空单链表,头指针指向头结点。若为空表,则头结点的指针域为空(判定空表的条件可记为:L−>next==NULL),如图(b)所示。
在这里插入图片描述

单链表的基本操作

下面以带头节点的单链表为例,讲解单链表的初始化、创建、取值、查找、插入、删除等基本操作。

初始化

单链表的初始化是指构建一个空表。先创建一个头节点,不存储数据,然后令其指针域为空
在这里插入图片描述

bool InitList(LinkList &L)
{
	L = new LNode;
	if(L == NULL)
	{
		return false;
	}
	L->next = NULL;
	return true;
 } 

创建

创建单链表分为头插法和尾插法两种

头插法是指每次把新节点插到头节点之后,其创建的单链表和数据输入顺序正好相反,因此也称为逆序建表。

尾插法是指每次把新节点链接到链表的尾部,其创建的单链表和数据输入顺序一致,因此也称为正序建表。

我们先讲头插法建表。头插法每次把新节点插入到头节点之后,创建的单链表和数据输入顺序相反。

头插法建表图解

1)初始状态。初始状态是指初始化后的空表,只有一个头节点
在这里插入图片描述
2)输入数据元素1,创建新节点,把元素1放入新节点数据域
在这里插入图片描述
在这里插入图片描述
3)头插操作,插入头节点的后面
在这里插入图片描述
4)输入数据元素2,创建新节点,把元素2放入新节点数据域
在这里插入图片描述
5)头插操作,插入头节点的后面
在这里插入图片描述

赋值解释

在这里插入图片描述
赋值语句两端,等号的右侧是节点的地址,等号的左侧是节点的指针域。
在这里插入图片描述
在这里插入图片描述

修改指针顺序为什么要先修改后面那个指针呢?

因为一旦修改了L节点的指针域指向s,那么原来L节点后面的节点就找不到了,因此修改指针是有顺序的。

修改指针的顺序原则:先修改没有指针标记的那一端
在这里插入图片描述
在这里插入图片描述

void CreateList_H(LinkList &L)
{
	int n;
	LinkList s;
	L = new LNode;
	L->next = NULL;
	cout << "请输入元素的个数:" << endl;
	cin >> n;
	cout << "请依次输入n个元素:" << endl;
	cout << "头插法创建单链表..." << endl;
	while(n--)
	{
		s = new LNode;
		cin >> s->data;
		s->next = L->next;
		L->next = s;
	}
}

接下来,我们讲尾插法建表。尾插法每次把新节点链接到链表的尾部,其创建的单链表和数据输入顺序一致。

尾插法建表图解

尾插法每次把新节点链接到链表的尾部,因此需要一个尾指针永远指向链表的尾节点。

1)初始状态。初始状态是指初始化后的空表,只有一个头节点,设置一个尾指针r指向该节点
在这里插入图片描述
2)输入数据元素1,创建新节点,把元素1放入新节点数据域
在这里插入图片描述
在这里插入图片描述
3)完成尾插操作,插入尾节点的后面
在这里插入图片描述
在这里插入图片描述
4)输入数据元素2,创建新节点,把元素2放入新节点数据域
在这里插入图片描述
5)完成尾插操作,插入尾节点的后面
在这里插入图片描述
6)继续依次输入数据元素3、4、5、6、7、8、9、10,尾插法创建的单链表如图所示。可以看出,尾插法创建的单链表与数据输入顺序一致。
在这里插入图片描述

void CreateList_R(LinkList &L)
{
	int n;
	LinkList s, r;
	L = new LNode;
	L->next = NULL;
	r = L;
	cout << "请输入元素个数n:" << endl;
	cin >> n;
	cout << "请依次输入n个元素:" << endl;
	cout << "尾插法创建单链表..." << endl;
	while(n--)
	{
		s = new LNode;
		cin >> s->data;
		s->next = NULL;
		r->next = s;
		r = s; 
	}
}

取值

单链表的取值不像顺序表那样可以随机访问任何一个元素,单链表只有头指针,各个节点的物理地址是不连续的。要想找到第i个节点,就必须从第一个节点开始按顺序向后找,一直找到第i个节点。那么具体怎么做呢?

注意:链表的头指针不可以随意改动!

一个链表是由头指针来标识的,一旦头指针改动或丢失,这个链表就不完整或找不到了。想想看,你拉着铁链子的一头,另一头绑着水桶,到井里打水,如果你手一松,链子掉到井里,就找不到了。所以链表的头指针是不能随意改动的,如果需要用指针移动,可定义一个指针变量进行移动。

算法步骤

1)先定义一个p指针,指向第一个元素节点,用j作为计数器,j=1。

2)如果p不为空且j<i,则p指向p的下一个节点,然后j加1,即:p=p->next;j++。

3)直到p为空或者j=i停止。p为空,说明没有数到i,链表就结束了,即不存在第i个节点;j=i,说明找到了第i个节点。

图解

1)p指针指向第一个元素节点,j=1
在这里插入图片描述
在这里插入图片描述

bool GetElem_L(LinkList L, int i, int &e)
{
	int j;
	LinkList p;
	p = L->next;
	j = 1;
	
	while(j < i && p)
	{
		p = p->next;
		j++;
	}
	if(!p || j > i)
	{
		return false;
	}
	e = p->data;
	return true;
} 

查找

在一个单链表中查找是否存在元素e,可以定义一个p指针,指向第一个元素节点,比较p指向节点的数据域是否等于e。如果相等,查找成功,返回true;如果不等,则p指向p的下一个节点,继续比较,如果p为空,查找失败,返回false,如图所示。
在这里插入图片描述

bool LocateElem_L(LinkList L, int e)
{
	LNode *p;
	p = L->next;
	while(p && p->data != e)
	{
		p = p->next;
	}
	if(!p)
	{
		return false;
	}
	return true;
}

指定结点的插入

如果要在第i个节点之前插入一个元素,则必须先找到第i-1个节点,想一想:为什么?

单链表只有一个指针域,是向后操作的,不可以向前操作。如果直接找到第i个节点,就无法向前操作,把新节点插入第i个节点之前。实际上,在第i个节点之前插入一个元素相当于在第i-1个节点之后插入一个元素,因此先找到第i-1个节点,然后将新节点插在其后面即可。

算法步骤

1)定义一个p指针,指向头节点,用j作为计数器,j=0。

2)如果p不为空且j<i-1,则p指向p的下一个节点,然后j加1,即:p=p->next;j++。

3)直到p为空或j >=i-1停止。

4)p为空,说明没有数到i-1,链表就结束了,即i>n+1, i值不合法;j >i-1说明i<1,此时i值不合法,返回false。如果j=i-1,说明找到了第i-1个节点。

5)将新节点插到第i-1个节点之后。

图解

假设已经找到了第i-1个节点,并用p指针指向该节点,s指向待插入的新节点,则插入操作如图所示。
在这里插入图片描述
在这里插入图片描述
前面讲的前插法建链表,就是每次将新节点插到头节点之后,现在是将新节点插到第i-1个节点之后

bool ListInsert_L(LinkList &L, int i, int e)
{
	int j;
	LinkList p, s;
	p = L;
	j = 0;
	while(p && j < i - 1)
	{
		p = p->next;
		j++;
	}
	if(!p || j > i - 1)
	{
		return false;
	}
	s = new LNode;
	s->data = e;
	s->next = p->next;
	p->next = s;
	return true;
}

删除

删除一个节点,实际上是把这个节点跳过去。根据单向链表向后操作的特性,要想跳过第i个节点,就必须先找到第i-1个节点,否则是无法跳过去的。删除操作如图所示。
在这里插入图片描述

bool ListDelete_L(LinkList &L, int i)
{
	LNode* p, q;
	int j;
	p = L;
	j = 0;
	while((p->next) && (j < i - 1))
	{
		p = p->next;
		j++;
	}
	if(!(p->next) || (j > i - 1))
	{
		return false;
	}
	q = p->next;
	p->next = q->next;
	delete q;
	return true;
}

小小样例

#include<bits/stdc++.h>
using namespace std;

typedef int ElemType;
typedef struct LNode{
	ElemType data;
	struct LNode *next;
}LNode, *LinkList;

//不带头结点的单链表 
bool InitList2(LinkList &L)
{
	L = NULL;
	return true;
} 

//带头结点的单链表
bool InitList(LinkList &L)
{
	cout << "创建一个单链表..." << endl;
	L = new LNode;
	if(L == NULL)
	{
		return false;
	}
	L->next = NULL;
	return true;
 } 
 
bool Empty(LinkList L)
{
	if(L->next == NULL)
	{
		return true;
	}
	else
	{
		return false;
	}
} 

//头插法 
void CreateList_H(LinkList &L)
{
	int n;
	LinkList s;
	L = new LNode;
	L->next = NULL;
	cout << "请输入元素的个数:" << endl;
	cin >> n;
	cout << "请依次输入n个元素:" << endl;
	cout << "头插法创建单链表..." << endl;
	while(n--)
	{
		s = new LNode;
		cin >> s->data;
		s->next = L->next;
		L->next = s;
	}
}

//尾插法
void CreateList_R(LinkList &L)
{
	int n;
	LinkList s, r;
	L = new LNode;
	L->next = NULL;
	r = L;
	cout << "请输入元素个数n:" << endl;
	cin >> n;
	cout << "请依次输入n个元素:" << endl;
	cout << "尾插法创建单链表..." << endl;
	while(n--)
	{
		s = new LNode;
		cin >> s->data;
		s->next = NULL;
		r->next = s;
		r = s; 
	}
}


//头插法 
bool ListInsert(LinkList &L, int i, ElemType 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;
	}
	
	LNode *s = (LNode *)malloc(sizeof(LNode));
	s->data = e;
	s->next = p->next;
	p->next = s;
	return true;
} 

void PrintLk(LinkList &L)
{
	LNode *p = L;
	p = p->next; 
	cout << "打印链表中所有元素:" << endl; 
	while(p != NULL)
	{
		cout << p->data << "   ";
		p = p->next; 
	}
	cout << endl; 
	cout << "打印完毕..." << endl;
}

bool GetElem_L(LinkList L, int i, int &e)
{
	int j;
	LinkList p;
	p = L->next;
	j = 1;
	
	while(j < i && p)
	{
		p = p->next;
		j++;
	}
	if(!p || j > i)
	{
		return false;
	}
	e = p->data;
	return true;
} 

bool LocateElem_L(LinkList L, int e)
{
	LNode *p;
	p = L->next;
	while(p && p->data != e)
	{
		p = p->next;
	}
	if(!p)
	{
		return false;
	}
	return true;
}

bool ListInsert_L(LinkList &L, int i, int e)
{
	int j;
	LinkList p, s;
	p = L;
	j = 0;
	while(p && j < i - 1)
	{
		p = p->next;
		j++;
	}
	if(!p || j > i - 1)
	{
		return false;
	}
	s = new LNode;
	s->data = e;
	s->next = p->next;
	p->next = s;
	return true;
}

bool ListDelete_L(LinkList &L, int i)
{
	cout << "开始删除元素..." << endl; 
	LNode* p, *q;
	int j;
	p = L;
	j = 0;
	while((p->next) && (j < i - 1))
	{
		p = p->next;
		j++;
	}
	if(!(p->next) || (j > i - 1))
	{
		return false;
	}
	q = p->next;
	p->next = q->next;
	delete q;
	return true;
}

int main()
{
	LinkList L;
	InitList(L);
	CreateList_H(L);
	PrintLk(L); 
	ListDelete_L(L, 3);
	PrintLk(L); 
	
	LinkList L2; 
	InitList(L2);
	CreateList_R(L2);
	PrintLk(L2); 
	return 0;
} 
 

样例运行结果:
在这里插入图片描述

之后我会持续更新,如果喜欢我的文章,请记得一键三连哦,点赞关注收藏,你的每一个赞每一份关注每一次收藏都将是我前进路上的无限动力 !!!↖(▔▽▔)↗感谢支持!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值