线性表-单链表(Ⅲ)

数据结构-单链表(第二章)的整理笔记,若有错误,欢迎指正。
线性表-顺序表(Ⅱ)


线性表的链式表示

单链表的定义

线性表的链式存储又称单链表,它是指通过一组任意的存储单元来存储线性表中的数据元素。为了建立数据元素之间的线性关系,对每个链表结点,除存放元素自身的信息外,还需要存放一个指向其后继的指针。
在这里插入图片描述

单链表中结点类型的描述

typedef struct LNode
{
	ElemeType data; //数据域
	struct Lnode *next; //指针域
}LNode, * LinkList;

//等价于

struct LNode
{
	ElemeType data; //数据域
	struct Lnode *next; //指针域
};
typedef struct LNode LNode;
typedef struct LNode *LinkList;
  • 通常用头指针来标识一个单链表,如单链表L,头指针为NULL时表示一个空表。
  • 此外,为了操作上的方便,在单链表第一个结点之前附加一个结点,称为头结点。头结点的数据域可以不设任何信息,也可以记录表长等信息。头结点的指针域指向线性表的第一个元素结点。
  • 头结点和头指针的区分:不管带不带头结点,头指针都始终指向链表的第一个结点,而头结点是带头结点的链表中的第一个结点,结点内通常不存储信息。引入头结点后,可以带来两个优点:
  1. 由于第一个数据结点的位置被存放在头结点的指针域中,因此在链表的第一个位置上的操作和在表的其他位置上的操作一致,无须进行特殊处理。
  2. 无论链表是否为空,其头指针都指向头结点的非空指针(空表中头结点的指针域为空)因此空表和非空表的处理也就得到了统一。
    在这里插入图片描述


单链表基本操作的实现

初始化线性表(时间复杂度:O(1))

带头结点

创建一个空的单链表,即创建一个头结点并将其next域设置为空。-InitList(L)

void InitList(LNode * &L)
{
    L = (LNode *)malloc(sizeof(LNode)); //创建头结点
    L->next = NULL; //其next域置为NULL
}

程序分析:

  • 头结点的数据域可以不设任何信息,因此初始化时只对指针域赋值,不对数据域赋值。

不带头结点

void InitList(LNode * &L)
{  L = NULL;  }

程序分析:

  • 其不需要分配结点,直接令头指针为空

建立单链表

  • 整体建立单链表的常用方法有两种:
  1. 头插法——从一个空表开始依次读取元素,生成一个新结点(由s指向它),将读取元素存放到该结点的数据域中,然后将其插入到当前链表的表头上(即头结点之后),直到所有元素读完为止。
  2. 尾插法——从一个空表开始依次读取元素,生成一个新结点s,将读取的存放到该结点的数据域中,然后将其插人到当前链表的表尾上,直到所有元素读完为止。为此需要增加一个尾指针r,其始终指向当前链表的尾结点,每插人一个新结点后让r指向这个新结点,最后还需要将r所指结点(尾结点)的next域置为空。

采用头插法建立单链表

带头结点

typedef struct LNode //定义单链表结点类型
{
	ElemType data; //数据域
	struct LNode *next; //指针域
}LNode, * LinkList;

void HeadInsert(LinkList &L) //逆向建立单链表
{
	LNode *s;
	L = (LinkList)malloc(sizeof(LNode)); //创建头结点
	L->next = NULL; //初始为空链
	for (int i = 1; i <= 5; i++)
	{
		s = (LNode *)malloc(sizeof(LNode));
		s->data = i; //为数据域赋值
		s->next = L->next; 
		L->next = s;
	}
}

int main()
{
	LNode *L;
	HeadInsert(L); //调用函数,采用头插法建立单链表
	L = L->next; //指向链表中的第一个结点
	while (L)
	{
		printf("%d ", L->data);
		L = L->next; //指针后移
	}
	return 0;
}

//输出结果:5 4 3 2 1

程序分析:

在这里插入图片描述

  • 时间复杂度:O(n);空间复杂度:O(1)

不带头结点

void HeadInsert(LinkList &L) //逆向建立单链表
{
	LNode *s;
	int i;
	for (i = 1; i <= 5; i++)
	{
		s = (LNode *)malloc(sizeof(LNode));  //创建新结点
		s->data = i;
		if (L == NULL)
		{
			L = s;
			s->next = NULL;
		}
		else
		{
			s->next = L;
			L = s;
		}
	}
}

采用尾插法建立单链表

带头结点

void TailInsert(LinkList &L)
{
	L = (LinkList)malloc(sizeof(LNode));
	L->next = NULL;
	LNode *s, *r = L; //r为表尾指针
	for (int i = 1; i <= 5; i++)
	{
		s = (LNode *)malloc(sizeof(LNode));
		s->data = i;
		r->next = s;
		r = s; //r指向新的表尾结点
	}
	r->next = NULL; //尾结点指针置空
}
TailInsert(L); 调用函数,采用头插法建立单链表

//输出结果为:1 2 3 4 5

不带头结点

#include<stdio.h>
#include<malloc.h>
typedef int ElemType;
typedef struct LNode
{
	ElemType data;
	struct LNode* next;
}LNode, * LinkList;
void TailInsert(LinkList &L)
{
	ElemType num;
	L = (LinkList)malloc(sizeof(LNode));
	scanf("%d", &num);
	L->data = num;
	L->next = NULL;
	LNode* s, * r = L; //r为表尾指针
	scanf("%d", &num);
	while(num!=999)
	{
		s = (LNode*)malloc(sizeof(LNode));
		s->data = num;
		s->next = r->next;
		r->next = s;
		r = s;
		scanf("%d", &num);
	}
}
int main()
{
	LNode *L;
	TailInsert(L);
	while (L != NULL)
	{
		printf("%d ", L->data);
		L = L->next;
	}

}

程序分析:

  • 因为附设了一个指向表尾结点的指针,故时间复杂度和头插法的相同。
    在这里插入图片描述
  • 时间复杂度:O(n);空间复杂度:O(1)

销毁线性表(时间复杂度:O(n))

该运算释放单链表L占用的内存空间,即逐一释放全部结点的空间。其过程是让pre、p指向两个相邻的结点(初始时pre指向头结点,p指向首结点)。当p不为空时循环:释放结点pre,然后pre、p同步后移一个结点。循环结束后,pre指向尾结点,再将其释放。-DestoryList(L)

void DestoryList(LNode * &L)
{
    LNode *pre = L, *p = L->next; //pre指向结点p的前驱结点
    while (p != NULL) //扫描单链表L
    {
        free(pre); //释放free结点 
        pre = p; //pre、p同步后移一个结点
        p = pre->next;
    }
    free(pre); //循环结束时,p为NULL,pre指向尾结点,释放它
}

在这里插入图片描述

判断单链表是否为空表(时间复杂度:O(1))

该运算在单链表L中没有数据结点时返回真,否则返回假。-ListEmpty(L)

带头结点

bool ListEmpty(LNode * &L)
{ return (L->next == NULL); }

不带头结点

bool ListEmpty(LNode * &L)
{ return (L == NULL); }

求单链表的长度(时间复杂度:O(n))

带头结点

该运算返回单链表L中数据结点的个数。由于单链表没有存放数据结点个数的信息,
需要通过遍历来统计。其过程是让p指向头结点,n用来累计数据结点个数(初始值为0)当p不为空时循环:n增1,p指向下一个结点,循环结束后返回n。-ListLength(L)

int ListLength(LinkList &L)
{
 int n = 0;
 LNode *p=L; //p指向头结点,n置为0(即头结点的序号为0)
 while (p->next!=NULL)
 {
  n++;
  p = p->next;
 }
return n; //循环结束,p指向尾结点,其序号n为结点个数
}

程序分析:

求表长操作就是计算单链表中的数据结点(不含头结点)的个数。

不带头结点

int ListLength(LinkList &L)
{
 int n = 0;
 LNode *p=L; //p指向头结点,n置为0(即头结点的序号为0)
 while (p!=NULL)
 {
  n++;
  p = p->next;
 }
return n; //循环结束,p指向尾结点,其序号n为结点个数
}

输出单链表(时间复杂度:O(n))

带头结点

void DispList(LNode * &L)
{
	LNode *p = L->next;  //p指向首结点
	while (p!=NULL) //p不为NULL时
	{
		printf("%d ", p->data); //循环输出p结点的data域
		p = p->next; //p移向下一个结点
	}
}

不带头结点

void DispList(LinkList &L) //输出线性表
{
	LNode *p = L; //p指向首结点
	while (p != NULL)
	{
		cout<<p->data<<' '; //输出p结点的data域
		p = p->next; //p移向下一个结点
	}
}

求单链表中的某个数据元素值(时间复杂度:O(n))

带头结点

在单链表L中从头开始找到第i个结点,若存在第i个数据结点,则将其data域值赋给变量e。其过程是让p指向头结点,j用来累计遍历过的数据结点个数(初始值为0),当j<i且p不为空时循环:j增1,p指向下一个结点。
循环结束后有两种情况,若p为空,表示单链表L中没有第i个数据结点(参数i错误),返回 false;否则找到第i个数据结点,提取它的值并返回true。-Getelem(L,i,&e)

bool GetElem(LNode * &L, int i, ElemType &e)
{
	LNode *p = L; //p指向头结点,j置0(即头结点的序号为0)
	int j = 0;
	if (i <= 0) return false; //i错误返回假
	while (p!= NULL && j < i)
	{
		j++;
		p = p->next; //p移向下一个结点
	}
	if (p==NULL) return false; //不存在第i个数据结点,返回false
	else //存在第i个数据结点,返回true
	{
		e=p->data;
		return true;
	}
}

不带头结点

bool GetElem(LinkList& L, int i, ElemType& e) //求线性表中的某个数据元素值
{
	LNode* p = L; //p指向头结点
	int j = 1; //j置1(即头结点的序号为1)
	if (i <= 0) return false; //i错误返回假
	while (p!= NULL && j < i)
	{
		j++;
		p = p->next; //p移向下一个结点
	}
	if (p == NULL) return false;//不存在第i个数据结点,返回false
	else //存在第i个数据结点,返回true
	{
		e = p->data;
		return true;
	}
}

按元素值查找(时间复杂度:O(n))

带头结点

在单链表L中从头开始找第一个值域与e相等的结点,若存在这样的结点,则返回逻辑序号,否则返回0。LocateElem(L,e)

int LocateElem(LNode * &L, ElemType e)
{
	LNode *p = L->next; //p指向首结点,i置为i(即首结点的序号为1)
	ElemType i = 1;
	while (p != NULL && p->data != e) //查找data值为e的结点,其序号为i
	{
		p = p->next; //p移向下一个结点
		i++;
	}
	if (p == NULL) return 0; //不存在值为e的结点,返回0
	else return i; //存在值为e的结点,返回其逻辑序号i
}

不带头结点

bool LocateElem(LinkList &L, ElemType e) //按元素值查找
{
	LNode *p = L; //p指向首结点
	int i = 1; //i置为i(即首结点的序号为1)
	while (p != NULL && p->data != e) //查找data值为e的结点,其序号为i
	{	
		i++;
		p = p->next; //p移向下一个结点
	}
	if (p == NULL) return 0; //不存在值为e的结点,返回0
	else return i; //存在值为e的结点,返回其逻辑序号i
}

插入数据元素(时间复杂度:O(n))

带头结点

先在单链表L中找到第iー1个结点,由p指向它。若存在这样的结点,将值为e的结点(s指向它)插入到p所指结点的后面。-ListInsert(&L,i,e)

bool ListInsert(LNode *L, int i, ElemType e)
{
	LNode * p = L, * s; //p指向头结点,j置为0(即头结点的序号为0)
	int j = 0;
	if (i <= 0) return false; //i错误返回false
	while (p != NULL && j < i-1) //查找第i-1个节点
	{
		j++;
		p = p->next; //p移向下一个结点
	}
	if (p == NULL) //未找到第i-1个结点,返回false
		return false;
	else //找到第i-1个结点p,插入新结点并返回true
	{
		s = (LNode *)malloc(sizeof(LNode));
		s->next = p->next;
		s->data = e; //创建新结点s,其data域置为e
		p->next = s; //将结点s插入到结点p之后
		return true; //返回true表示成功插入第i个节点
	}
}

不带头结点

	if (i == 1) //首结点前插入元素
	{
		s = (LNode *)malloc(sizeof(LNode)); //开辟结点空间
		s->data = e;
		s->next = L;
		L = s;
		return true;
	}

程序分析:

  • 由于单链表不带头结点,不存在“第0个”结点,因此i=1时需要特殊处理。插入、删除第1个元素时,需要更改头指针L。i≠1时的逻辑和带头结点的一样。
  • 如果要在不带头结点的第一个元素之前插入一个新元素,先开辟一个结点空间(s),其数据域里面的值为e,让s指向首结点(此时s变成了首结点),再让L指向s。

删除数据元素(时间复杂度:O(n))

先在单链表L中找到第iー1个结点,由p指向它。若存在这样的结点,且也存在后继结点(由p指向它),则删除q所指的结点,返回true;否则返回false,表示参数i错误。

带头结点

bool ListDelete(LNode*& L, int i, ElemType &e)
{
	LNode* p = L, * q; //p指向头结点,j置为0(即头结点的序号为0)
	int j = 0;
	if (i <= 0) return false; //i错误返回false
	while (p != NULL && j < i - 1) //查找第i-1个节点
	{
		j++;
		p = p->next; //p移向下一个结点
	}
	if (p == NULL) return false; //未找到第i-1个结点,返回false
	else //找到第i-1个结点p
	{
		q = p->next; //q指向第i个结点
		if (q == NULL) //若不存在第i个结点,返回false
			return false;
		e = q->data;
		p->next = q->next; //从单链表中删除q结点
		free(q); //释放q结点
		return true; //返回true表示成功删除第i个节点
	}
}

在这里插入图片描述

不带头结点

if (i == 1) //删除首结点
{
	L = L->next;
	free(p);
	return true;
}

程序分析:

  • 如果要删除不带头结点中的第一个元素,首先让p指向首结点,然后L后移一个结点,最后释放p。
    在这里插入图片描述

指定结点的后插操作(时间复杂度:O(1))

在指定结点后插入一结点,此时它的后驱结点是已知的。-InsertNextNode

bool InsertNextNode(LNode *p, ElemType e)
{
	LNode *s;
	if (p == NULL) return false; 
	s = (LNode *)malloc(sizeof(LNode)); //开辟结点空间
	if (s == NULL) return false; //某些情况下有可能分配失败(如内存不足)
	s->data = e; //用结点s保存数据元素e
	s->next = p->next; //s指向p
	p->next = s; //将结点s连接到p后
	return true;
}

指定结点的前插操作(时间复杂度:O(1))

在指定结点前插入一结点,无法知晓它的前驱结点。-InsertPriorNode

bool InsertPriorNode(LNode *p, ElemType e)
{
	LNode *s;
	if (p == NULL) return false;
	s = (LNode*)malloc(sizeof(LNode));
	if (s == NULL) return false; //内存分配失败
	s->data = p->data; //将p中元素赋值到s中
	p->data = e; //p中元素置为e
	s->next = p->next;
	p->next = s; //新节点s连接到p之后
	return true;
}

程序分析

指定结点的前插操作有两种方法:

  1. 传入头指针,循环查找p的前驱q,再对q后插。
    在这里插入图片描述
  2. 后插,然后交换数据域中的值,并且使指针的指向发生改变。

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值