线性表的链式实现(二)

本篇文章将讲解线性表的链式实现。

循环链表的定义

上篇文章我们学习了单链表,并掌握了单链表的一些基本操作,本篇文章我们继续学习循环链表和双链表的内容。 先来看看循环链表的定义:

循环链表是一种头尾相连的链表,即表中最后一个结点的指针域不再为NULL,而是指向头结点,整个链表形成一个环。

下图为带头结点的循环链表:

由于循环链表的特性,使其从表中任一结点出发都可以找到表中其它结点。

对于上面的循环链表: 如果想要查找a~1~结点,只需要通过头指针扫描一次即可找到,时间复杂度为O(1); 而如果想要查找a~n~结点,就需要通过头指针扫描n次才能找到,时间复杂度为O(n)。 可以知道,如果想要查找循环链表末尾的结点,是比较耗时的,这时候,我们可以考虑在循环链表的末尾也加上一个指针,它指向的是尾结点,通过尾指针可以很方便地获取到a~1~结点和a~n~结点。 综上所述:若是经常在循环链表的表头和表尾进行操作,则可以增设一个尾指针方便处理。

合并两个循环链表

关于循环链表的其它操作就不多说了,它和单链表是一模一样的,这里讲解一下如何合并两个循环链表。 假设有如下两个带尾指针的循环链表:

如何将Tb合并到Ta后面呢? 步骤如下: 第一个循环链表的尾指针Ta不再指向自己的头结点,而是指向第二个循环链表的首元结点:

这样第二个循环链表的头结点就没有作用了,我们可以将其释放掉。 然后将第二个循环链表的尾指针Tb指向第一个循环链表的头结点,完成合并。

综上所述,操作步骤如下:

  1. 保存第一个循环链表的头结点
  2. 尾指针Ta指向第二个循环链表的首元结点
  3. 尾指针Tb指向第一个循环链表的头节点
  4. 释放第二个循环链表的头结点

合并两个循环链表的算法实现如下:

c 复制代码
LinkList Connect(LinkList Ta,LinkList Tb){
	LinkList p;
	//保存Ta头结点
	p = Ta->next;
	//让Ta指向Tb的首元结点
	Ta->next = Tb->next->next;
	//释放Tb头结点
	free(Tb->next);
	//让Tb指向Ta的头结点
	Tb->next = p;
	return Tb;//此时Tb为合并后的循环链表的尾指针
}

该算法的时间复杂度为O(1)。

当然了,还需要注意一些问题,比如在单链表中,遍历结束的条件是p->next == NULL,此时说明已经扫描到尾结点;而循环链表因为是呈环形的,所以这个条件就不能用了,循环链表的遍历结束条件为p->next == L,当循环链表的尾指针指向头结点的时候,说明已经扫描到尾结点;

双向链表的定义

对于单链表,它能够通过头结点按顺序扫描整个链表,但是,它有一个缺陷,就是只能找到一个结点的直接后继结点,而无法通过一个结点找到其直接前驱结点,因为它是单向的。 为此,我们可以在结点中增设一个指向其直接前驱结点的指针域,这样链表中就形成了有两个不同方向的链,我们称这样的链表为双向链表,也叫双链表。 下图为带头结点的双向链表:

所以,其结构如下:

c 复制代码
#define ElemType int

typedef struct Node{
	ElemType data;//数据域
	struct Node *prior;//前驱指针
	struct Node *next;//后继指针
}Node,*LinkList;

双向链表的基本操作

对于双向链表,其遍历、查找、计算长度等等算法都和单链表类似,我就不另外说了,这里强调一些与单链表不太一样的操作。

双向链表的初始化

双向链表的初始化非常简单,直接看代码:

c 复制代码
LinkList create_list(){
	//创建头结点
	LinkList l = (LinkList) malloc(sizeof(Node));
	if(l == NULL){
		exit(-1);
	}
	//初始前驱和后继指针均为NULL
	l->next = l->prior = NULL;
	return l;//返回头结点
}

很简单,接下来我们同样分析一下两种初始化双向链表的方式:

  1. 头插法
  2. 尾插法

这两种方法在单链表中已经讲解过,但在双向链表中有些许不同,还是有必要说一说。

头插法

首先我们创建一个头结点,初始前驱指针和后继指针都指向头结点本身。

我们暂且称头结点为l,插入结点为s,步骤如下: 先插入第一个结点。

让新结点的后继指针指向头结点的后继,即:s->next = l->next。 这样只是建立了单向的连接,我们还需让新结点的前驱指针指向头结点,即:s->prior = l

当然了,事情远没有这么简单,这样的连接方式并不适用于所有结点,它只是插入第一个结点时的特殊情况。

当插入第二个结点时,同样先让新结点的后继指针指向头结点的后继:

然后让新结点的前驱指向头结点:

可以看到,这样并没有建立起双向关系,所以我们还需让头结点的后继结点的前驱指向新结点,即:l->next->prior = s

最后让头结点的后继指向新结点,完成插入。

综上所述,在插入结点的过程中,我们需要进行如下操作:

  1. 让新结点的后继指向头结点的后继
  2. 判断当前插入的结点是否为首元结点,判断条件:l->next == NULL
  3. 若插入的结点不为首元结点,则需额外进行一步操作,即:让头结点的后继结点的前驱指向新结点
  4. 让头结点的后继指向新结点
  5. 让新结点的前驱指向头结点

头插法代码实现如下:

c 复制代码
LinkList create_listH(int *a,int length){
	LinkList l,s;
	int i;
	//创建头结点
	l = (LinkList) malloc(sizeof(Node));
	if(l == NULL){
		exit(-1);
	}
	//初始前驱和后继指针均为NULL
	l->next = l->prior = NULL;
	for(i = 0;i < length;i++){
		//创建新结点
		s = (LinkList) malloc(sizeof(Node));
		if(s == NULL){
			exit(-1);
		}
		s->data = a[i];
		//头插法插入结点
		s->next = l->next;
		if(l->next != NULL){//此时说明l不为头结点
			l->next->prior = s;
		}
		l->next = s;
		s->prior = l;
	}
	return l;
}

尾插法

尾插法就简单很多了,因为每个结点的插入方式都是一样的,没有特殊情况需要考虑,比如下面的一个空结点:

插入第一个结点: 和单链表一样,我们仍然需要一个结点类型变量t辅助插入,初始变量t指向头结点,然后让t的后继指向新结点:

接着让新结点的前驱指向头结点:

最后将新结点赋值给变量t,使新结点变为尾结点,完成插入。

尾插法代码实现如下:

c 复制代码
LinkList create_listT(int *a,int length){
	LinkList l,s,t;
	int i;
	//创建头结点
	l = (LinkList) malloc(sizeof(Node));
	if(l == NULL){
		exit(-1);
	}
	//初始t指向头结点
	t = l;
	for(i = 0;i < length;i++){
		//创建新结点
		s = (LinkList) malloc(sizeof(Node));
		if(s == NULL){
			exit(-1);
		}
		s->data = a[i];
		//t的后继指向新结点
		t->next = s;
		//新结点的前驱指向t
		s->prior = t;
		//新结点成为尾结点
		t = s;
	}
	//尾结点指针域置为NULL
	t->next = NULL;
	return l;//返回头结点
}

双向链表的插入

双向链表的插入和单链表类似,我们同样需要找到插入位置的前一个结点。 比如下面的一个双向链表,要想在位置为3的位置插入一个结点,该如何实现呢?

我们找到插入位置的前一个结点,即:元素值为2的结点,暂且称其为p。通过结点p的后继指针就能够找到插入位置,即:元素值为3的结点,暂且称其为q。 插入步骤如下: 先让新结点s的后继指向结点p的后继,即:s->next = p->next

然后让结点p的后继结点的前驱指向新结点s,即:p->next->prior = s

接着让新结点的前驱指向结点p,即:s->prior = p

最后让结点p的后继指向新结点s,即:p->next = s

这样即可完成插入。

我们还得考虑插入的极端情况,比如插入位置为链表的表尾,此时如果执行p->next->prior = s,显然会出错,因为p为插入位置的前一个结点,所以如果在表尾插入,p的位置就是尾结点,而p的指针域为NULL,此时程序就会出错。而事实上,在表尾插入,因为插入位置的后面已经没有结点了,所以无需考虑后面结点与插入结点的关系。

代码实现如下:

c 复制代码
int InsertList(LinkList l,int pos,int val){
	LinkList p,s;
	int length,i = 0;
	length = ListLength(l);
	p = l;//p初始指向头结点
	//判断pos值的合法性
	if(pos < 1 || pos > length + 1){
		return -1;
	}
	//
	while(p != NULL && i < pos - 1){
		i++;
		p = p->next;
	}
	//此时p为插入位置的前一个结点
	//创建新结点
	s = (LinkList) malloc(sizeof(Node));
	if(s == NULL){
		return -1;
	}
	s->data = val;
	//插入结点
	s->next = p->next;
	if(p->next != NULL){//若p不为尾结点
		p->next->prior = s;
	}
	s->prior = p;
	p->next = s;
	return 1;//插入成功,返回1
}

双向链表的删除

接下来介绍一下双向链表的删除操作。 比如下面的一个双向链表:

要想删除位置为2的元素,该如何实现呢? 删除操作比较简单,步骤如下: 先让结点p的后继指向结点q的后继,即:p->next = q->next

然后要讨论一下要删除的结点是否为链表的尾结点,若为尾结点,那么只需释放删除结点的内存即可;若不为尾结点,则需再让结点p的后继结点的前驱指向结点p,即:p->next->prior = p

最后释放结点q的内存即可,两条语句的顺序不能颠倒。 删除算法代码如下:

c 复制代码
int DeleteList(LinkList l,int pos,int *val){
	LinkList p,q;
	int length,i = 0;
	length = ListLength(l);
	p = l;//p初始指向头结点
	//判断pos值的合法性
	if(pos < 1|| pos > length){
		return -1;
	}
	while(p != NULL && i < pos - 1){
		i++;
		p = p->next;
	}
	//此时p为待删除位置的前一个结点
	q = p->next;//q为要删除的结点
	//保存数据
	*val = q->data;
	p->next = q->next;
	if(p->next != NULL){//若当前p不为尾结点
		p->next->prior = p;
	}
	free(q);
	return 1;//删除成功,返回1
}

源代码

文章中的所有代码:

c 复制代码
#include <stdio.h>
#include <malloc.h>
#include <stdlib.h>

#define ElemType int

typedef struct Node{
	ElemType data;//数据域
	struct Node *prior;//前驱指针
	struct Node *next;//后继指针
}Node,*LinkList;

int DeleteList(LinkList l,int pos,int *val){
	LinkList p,q;
	int length,i = 0;
	length = ListLength(l);
	p = l;//p初始指向头结点
	//判断pos值的合法性
	if(pos < 1|| pos > length){
		return -1;
	}
	while(p != NULL && i < pos - 1){
		i++;
		p = p->next;
	}
	//此时p为待删除位置的前一个结点
	q = p->next;//q为要删除的结点
	//保存数据
	*val = q->data;
	p->next = q->next;
	if(p->next != NULL){//若当前p不为尾结点
		p->next->prior = p;
	}
	free(q);
	return 1;//删除成功,返回1
}

int InsertList(LinkList l,int pos,int val){
	LinkList p,s;
	int length,i = 0;
	length = ListLength(l);
	p = l;//p初始指向头结点
	//判断pos值的合法性
	if(pos < 1 || pos > length + 1){
		return -1;
	}
	//
	while(p != NULL && i < pos - 1){
		i++;
		p = p->next;
	}
	//此时p为插入位置的前一个结点
	//创建新结点
	s = (LinkList) malloc(sizeof(Node));
	if(s == NULL){
		return -1;
	}
	s->data = val;
	//插入结点
	s->next = p->next;
	if(p->next != NULL){//若p不为尾结点
		p->next->prior = s;
	}
	s->prior = p;
	p->next = s;
	return 1;//插入成功,返回1
}

int ListLength(LinkList l){
	LinkList p;
	int i = 0;//计数器
	p = l->next;//p初始指向首元结点
	while(p != NULL){
		i++;//计数器加1
		p = p->next;//p指向下一个结点
	}
	return i;//返回单链表长度
}

LinkList create_listT(int *a,int length){
	LinkList l,s,t;
	int i;
	//创建头结点
	l = (LinkList) malloc(sizeof(Node));
	if(l == NULL){
		exit(-1);
	}
	//初始t指向头结点
	t = l;
	for(i = 0;i < length;i++){
		//创建新结点
		s = (LinkList) malloc(sizeof(Node));
		if(s == NULL){
			exit(-1);
		}
		s->data = a[i];
		//t的后继指向新结点
		t->next = s;
		//新结点的前驱指向t
		s->prior = t;
		//新结点成为尾结点
		t = s;
	}
	//尾结点指针域置为NULL
	t->next = NULL;
	return l;//返回头结点
}

LinkList create_listH(int *a,int length){
	LinkList l,s;
	int i;
	//创建头结点
	l = (LinkList) malloc(sizeof(Node));
	if(l == NULL){
		exit(-1);
	}
	//初始前驱和后继指针均为NULL
	l->next = l->prior = NULL;
	for(i = 0;i < length;i++){
		//创建新结点
		s = (LinkList) malloc(sizeof(Node));
		if(s == NULL){
			exit(-1);
		}
		s->data = a[i];
		//头插法插入结点
		s->next = l->next;
		if(l->next != NULL){//此时说明l不为头结点
			l->next->prior = s;
		}
		l->next = s;
		s->prior = l;
	}
	return l;
}

LinkList create_list(){
	//创建头结点
	LinkList l = (LinkList) malloc(sizeof(Node));
	if(l == NULL){
		exit(-1);
	}
	//初始前驱和后继指针均为NULL
	l->next = l->prior = NULL;
	return l;//返回头结点
}

void traverse_list(LinkList l){
	LinkList p = l->next;
	while(p != NULL){
		printf("%d\t",p->data);
		p = p->next;
	}
}
相关推荐
blammmp24 分钟前
Java:数据结构-枚举
java·开发语言·数据结构
昂子的博客1 小时前
基础数据结构——队列(链表实现)
数据结构
齐 飞1 小时前
MongoDB笔记01-概念与安装
前端·数据库·笔记·后端·mongodb
lulu_gh_yu1 小时前
数据结构之排序补充
c语言·开发语言·数据结构·c++·学习·算法·排序算法
LunarCod1 小时前
WorkFlow源码剖析——Communicator之TCPServer(中)
后端·workflow·c/c++·网络框架·源码剖析·高性能高并发
码农派大星。2 小时前
Spring Boot 配置文件
java·spring boot·后端
杜杜的man3 小时前
【go从零单排】go中的结构体struct和method
开发语言·后端·golang
幼儿园老大*3 小时前
走进 Go 语言基础语法
开发语言·后端·学习·golang·go
llllinuuu3 小时前
Go语言结构体、方法与接口
开发语言·后端·golang
cookies_s_s3 小时前
Golang--协程和管道
开发语言·后端·golang