初学数据结构——从零开始的数据结构学习总结篇(线性表)

前言

简介:

线性表是一种最简单也最常用的数据结构,它包括顺序表和链表两种实现方式。
在这篇博客中,我将介绍线性表的定义、特点、操作以及它们之间的优缺点和比较。我希望通过这篇博客,能够帮助大家更好地理解和掌握线性表这一重要的数据结构。

注:

本博客为本人在学习数据结构路途上的知识整理,如觉得对有你有所帮助,还希望不要吝啬你的赞,整理知识点是真的很累|*´Å`)ノ 。由于博主只是一名大一新生,在文章难免会出现错误,还希望指正。如果想要转载,附上链接就行。

本文中的颜色标记

  • 红色部分为重点内容
  • 蓝色部分为注释


目录

前言

简介:

注:

本文中的颜色标记

一、线性表的定义和特点

定义

逻辑特征

二、线性表的类型定义

抽象数据类型线性表的定义

线性表的基本操作

三、线性表的顺序存储

定义:

线性表中元素存储位置的计算

线性表顺序存储结构的图示

顺序表的顺序存储结构实现

创建顺序存储结构的顺序表

初始化线性表

销毁线性表

重置线性表

判断线性表是否为空

获得线性表长度

查找第i个元素

查找满足一定条件的元素

查找指定元素的前驱

查找指定元素的后继

在指定位置插入元素

删除指定位置的元素

枚举线性表中所有元素,依次调用指定操作

小结

线性表的特点

顺序表的操作算法分析

 四、线性表的链式存储结构

1、链式存储的基本概念

定义

存储方式

与链式存储有关的术语

链表(链式存储结构)的特点

2、线性表链式存储结构的实现(单链表)

创建链表

3、单链表的操作(有头结点的单链表)

单链表的初始化

建立单链表

判断链表是否为空

销毁单链表

清空单链表

获得单链表的表长

查找第i个结点的值

查找值为e的结点的位置

在指定位置插入结点

删除第i个结点

4、循环链表

定义:

图示:

优点:

注意事项:

带尾指针的循环链表的合并(合并链表a与链表b,将b链接在a后)

 5、双向链表

定义:

 创建双向链表

双向链表结构的对称性:

 双向循环链表

双向链表的操作

五、顺序表和链表的比较

1、链式存储结构的优缺点

链式存储结构的优点:

链式存储结构的缺点:

2、顺序表的优缺点

优点:

缺点:

总结:


一、线性表的定义和特点

定义

  • 具有相同特性元素的的一个有限序列


上图是由n(n≥0)个数据元素(结点)a1,a2,...a,组成的有限序列。

  1. 其中数据元素的个数n定义为表的长度
  2. 当n=0时称为空表
  3. 将非空的线性表(n>)记作:(a1. a2. ...an)
  4. 这里的数据元素ai(1≤i≤n)只是一个抽象的符号,其具体含义在不同的情况下可以不同。
  5. 同一线性表中的元素必定具有相同特性,数据元素间的关系是线性关系

逻辑特征

  • 在非空的线性表,有且仅有一个开始结点a1,没有直接前趋,而仅有一个直接后继a2
  • 有且仅有一个终端结点an,它没有直接后继,而仅有一个直接前趋an-1;
  • 其余的内部结点ai(2≤i≤n-1)都有且仅有一个直接前趋ai-1和一个直接后继ai+1
  • 是一种典型的线性结构

二、线性表的类型定义

抽象数据类型线性表的定义

如下:
ADT List {
数据对象:D = {ai|ai属于ElemSet,(i=1,2,...,n,n≥0)}

ElemSet:

ElemSet是某个确定的、将由用户自行定义的、含某个关系运算的数据对象。譬如说e1,e2,e3€ElemSet,意思是e1,e2,e3属于元素集合。D={ai|ai ∈ ElemSet,i=1,2,…n,n>=0}这句语句的意思是一个数据对象D,数据对象里面是一个叫ElemSet的集合,集合里面有n个元素。

数据关系: R=<ai-1,ai>、ai-1,ai属于D,(i=2,3,,n)

R=<ai-1,ai>:

由两个元素x和y,按照一定的顺序组成的二元组称为有序对,记作<x,y>,表示x和y存在关系。

基本操作:
InitList(&L);(初始化线性表)
DestroyList(&L);(删除线性表)
ListDelete(&L,i,&e):(删除指定的数据元素)
Listlnsert(&L,i,e);(插入指定的数据元素)
等等


}ADT List

线性表的基本操作

InitList(&L)

操作结果:构造一个空的线性表L。

DestroyList(&L)

初始条件:线性表L已经存在。
操作结果:销毁线性表L。

ClearList(&L)

初始条件:线性表L已经存在。
操作结果:将线性表l重置为空表。

ListEmpty(L)

初始条件:线性表L已经存在。
操作结果:若线性表L为空表则返回TURE;否则返回FALSE。

ListLength(L)

初始条件:线性表L已经存在。
操作结果:返回线性表L中的数据元素个数。

GetElem(L,i,&e)

初始条件:线性表L已经存在。
操作结果:用e返回线性表L中第i个数据元素的值。

LocateElem(L,e,compare())

初始条件:线性表L已经存在,compare()是数据元素判定函数。
操作结果:返回L中第1个与e满足compare()的数据元素的位序。若这样的数据元素不存在则返回值为0。

PriorElem(L,cur_e,&pre_e)

初始条件:线性表L已经存在。
操作结果:若cur_e是L的数据元素,且不是第一个,则用pre_e返回它的前驱,否则操作失败;pree无意义。

NextElem(L, cure, &next_e)

初始条件:线性表L已经存在。
操作结果:若cur_e是L的数据元素,且不是最后个,则用next_e返回它的后继,否则操作失败,next_e无意义,

istlnsert(&L, i, e)

初始条件:线性表L已经存在,1<=i<= ListLength(L)+1。
操作结果:在L的第i个位置之前插入新的数据元素e,L的长度加一。
插入元素e之前(长度为n):(a1,a2,...,ai-1,ai...,an)
插入元素e之后(长度为n+1):(a1,a2,...,aj-i,e,ai, ...an)

ListDelete(&L,i,&e)

初始条件:线性表L已经存在,1<=i<= ListLength(L)。
操作结果:删除L的第i个数据元素,并用e返回其值,L的长度减一。
删除前(长度为n):(a1,a2,...,ai-1,ai,ai+1,...an)
删除后(长度为n-1):(a1,a2,...,ai-1,ai+1,...,an)

ListTraverse(&L,visited())

初始条件:线性表L已经存在。
操作结果:依次对线性表中每人元素调用visited().

  • 以上所提及的运算是逻辑结构上定义的运算。只要给出这些运算的功能是"做什么”,至于"如何做"等实现细节,只有待确定了存储结构之后才考虑。

三、线性表的顺序存储

线性表的顺序表示又称为顺序存储结构顺序映像

定义:

把逻辑上相邻的数据元素存储在物理上相邻的存储单元中的存储结构

  •  线性表的第1个数据元素a1的存储位置,称作线性表的起始位置或基地址。
  • 线形表顺序存储结构占用一片连续的存储空间

线性表中元素存储位置的计算

假设线性表的每个元素需占x个存诸单元,则第i +1个数据元素的存储位置和第i个数据元素的存储位置之间满足关系:
LOC(ai + 1) = LOC(ai) + x
由此,所有数据元素的存储位置均可由第一个数据元素的存储位置得到:
LOC(ai) = LOC(a1) + (i - 1) * x

线性表顺序存储结构的图示

  •  以物理位置相邻表示逻辑关系,任一元素均可随机存取

顺序表的顺序存储结构实现

创建顺序存储结构的顺序表

#include <stdio.h>
#include <stdlib.h>
//引用头文件

typedef struct
{
    int data;
}ElemType;
//利用结构体储存数据元素,并命名为为ElemType,方便调用
//数据元素可以为任意数据类型,这里的数据元素为整形

typedef struct
{
    ElemType* elem;
//开辟数组空间
    int length;
//存储线性表的表长
} SqList;
//创建结构体,包含数据元素数组和表长。

初始化线性表

InitList(&L)

操作结果:构造一个空的线性表L。

void InitList(SqList* L)
{
    L->elem = (ElemType*)malloc(sizeof(ElemType) * MAXLENGTH);
    //利用malloc开辟一块动态的储存空间
    L->length = MAXLENGTH;
    //MAXLENGTH一般由我们自己利用宏来定义,为线性表初始表长
    assert("初始化失败" && L->elem != NULL);
    //断言开辟空间成功,失败则停止程序
}

销毁线性表

DestroyList(&L)

初始条件:线性表L已经存在。
操作结果:销毁线性表L。

void DestroyList(SqList& L)
{
    free(L.elem);
    //释放线性表的存储空间
    L.elem = NULL;
    //将指针置空
    L.length = 0;
    //将长度归零
}

重置线性表

ClearList(&L)

初始条件:线性表L已经存在。
操作结果:将线性表l重置为空表。

void ClearList(SqList& L)
{
    L.length = 0;
    //将长度归零
}

判断线性表是否为空

ListEmpty(L)

初始条件:线性表L已经存在。
操作结果:若线性表L为空表则返回TURE;否则返回FALSE。

bool ListEmpty(SqList L)
{
    if (L.length == 0)
        return true;
    else
}

获得线性表长度

ListLength(L)

初始条件:线性表L已经存在。
操作结果:返回线性表L中的数据元素个数。

int ListLength(SqList L)
{
    return L.length;
}

查找第i个元素

GetElem(L,i,&e)

初始条件:线性表L已经存在。
操作结果:用e返回线性表L中第i个数据元素的值。

bool GetElem(SqList L, int i, ElemType& e)
{
    if (i < 1 || i > L.length)
        return false;
    //判断位置是否合法
    e = *(L.elem + i - 1);
    return true;
}

查找满足一定条件的元素

LocateElem(L,e,compare())

初始条件:线性表L已经存在,compare()是数据元素判定函数。
操作结果:返回L中第1个与e满足compare()的数据元素的位序。若这样的数据元素不存在则返回值为0。

int LocateElem(SqList L, ElemType e, bool(*compare)(ElemType, ElemType))
{
    int i;
    ElemType* p = L.elem;
    //创建索引
    for (i = 1; i <= L.length; i++)
    {
        if (compare(*(p + i - 1), e))
            return i;
    }
    //枚举寻找目标元素,可以优化使用其他寻找方式
    return 0;
}

查找指定元素的前驱

PriorElem(L,cur_e,&pre_e)

初始条件:线性表L已经存在。
操作结果:若cur_e是L的数据元素,且不是第一个,则用pre_e返回它的前驱,否则操作失败;pree无意义。

// 若cur_e是L的数据元素,且不是第一个,则用pre_e返回它的前驱,否则操作失败;pre_e无意义。
bool PriorElem(SqList L, ElemType cur_e, ElemType& pre_e)
{
    int i;
    for (i = 2; i <= L.length; i++)
    {
        if ((L.elem + i - 1)->data == cur_e.data)
        {
            pre_e = *(L.elem + i - 2);
            return true;
        }
    }
    //枚举寻找cur_e
    return false;
}

查找指定元素的后继

NextElem(L, cure, &next_e)

初始条件:线性表L已经存在。
操作结果:若cur_e是L的数据元素,且不是最后个,则用next_e返回它的后继,否则操作失败,next_e无意义,

bool NextElem(SqList L, ElemType cur_e, ElemType& next_e)
{
    int i;
    for (i = 1; i <= L.length - 1; i++)
    {
        if ((L.elem + i - 1)->data == cur_e.data)
        {
            next_e = *(L.elem + i);
            return true;
        }
    }
    //枚举寻找cur_e
    return false;
}

在指定位置插入元素

Listlnsert(&L, i, e)

初始条件:线性表L已经存在,1<=i<= ListLength(L)+1。
操作结果:在L的第i个位置之前插入新的数据元素e,L的长度加一。
插入元素e之前(长度为n):(a1,a2,...,ai-1,ai...,an)
插入元素e之后(长度为n+1):(a1,a2,...,aj-i,e,ai, ...an)

bool ListInsert(SqList& L, int i, ElemType e)
{
    if (i < 1 || i > L.length + 1)
        return false; 
    //判断插入位置是否合法
    ElemType* q = &(L.elem[i - 1]);
    //q为插入位置
    for (ElemType* p = &(L.elem[L.length - 1]); p >= q; --p)
        *(p + 1) = *p;
    //插入位置及之后的元素右移
    *q = e;
    ++L.length; 
    //长度加1
    return true;
}

删除指定位置的元素

ListDelete(&L,i,&e)

初始条件:线性表L已经存在,1<=i<= ListLength(L)。
操作结果:删除L的第i个数据元素,并用e返回其值,L的长度减一。
删除前(长度为n):(a1,a2,...,ai-1,ai,ai+1,...an)
删除后(长度为n-1):(a1,a2,...,ai-1,ai+1,...,an)

bool ListDelete(SqList& L, int i, ElemType& e)
{
    if (i < 1 || i > L.length)
        return false;
    //判断删除位置是否合法
    ElemType* p = &(L.elem[i - 1]);
    //p为被删除元素的位置
    e = *p; // 被删除元素的值赋给e
    ElemType* q = L.elem + L.length - 1;
    //表尾元素位置
    for (++p; p <= q; ++p) *(p - 1) = *p;
    //被删除元素之后的元素左移
    --L.length;
    //长度减1
    return true;
}

枚举线性表中所有元素,依次调用指定操作

ListTraverse(&L,visited())

初始条件:线性表L已经存在。
操作结果:依次对线性表中每人元素调用visited().

void ListTraverse(SqList L, void(*visit)(ElemType))
{
    ElemType *p = L.elem;
    for (int i = 1; i <= L.length; ++i)
        visit(*(p++));
}

小结

线性表的特点

  1. 利用数据元素的存储位置表示线性表中相邻数据元素之间的前后关系,即线性表的逻辑结构与存储结构一致
  2. 在访问线性表时,可以快速地计算出任何一个数据元素的存储地址。因此可以粗略地认为,访问每个元素所花时间相等
  • 这种存取元素的方法被称为随机存取法

顺序表的操作算法分析

  • 时间复杂度

查找、插入、删除算法的平均时间复杂度为O(n)

  • 空间复杂度

显然,顺序表操作算法的空间复杂度S(n)=O(1)(没有占用辅助空间)


 四、线性表的链式存储结构

1、链式存储的基本概念

定义

线性表中各个数据元素在存储器中的位置是任意的,即逻辑上相邻的数据元素在物理上不一定相邻

  • 线性表的链式表示又称为非顺序映像链式映像

存储方式

  • 用一组物理位置任意的存储单元来存放线性表的数据元素
  • 这组存储单元既可以是连续的,也可以是不连续的,甚至是零散分布在内存中的任意位置上的。
  • 链表中元素的逻辑次序和物理次序不一定相同

与链式存储有关的术语

1、结点:数据元素的存储映像,由数据域和指针域两部分组成

  • 数据域存储数据
  • 指针域存储指向下一个数据的指针

结点示意图 

2、链表:n个结点由指针链接组成个链表。

  • 线性表的链式存储映像,称为线性表的链式存储结构

链表示意图

 3、单链表、双链表、循环链表:

  • 结点只有一个指针域的链表,称为单链表或线性链表

  • 结点有两个指针域的链表,称为双链表

  • 首尾相接的链表称为循环链表

4、头指针、头结点和首元结点

  • 头指针:是指向链表中第一个结点的指针
  • 首元结点:是指链表中存储第一个数据元素a1的结点
  • 头结点:是在链表的首元结点之前附设的一个结点

 带头结点和不带头结点的链式存储结构的区别

  • 不带头结点的链式存储结构示意图

  •  带头结点的链式存储结构示意图

  • 单链表是由表头唯一确定,因此单链表可以用头指针的名字来命名,若头指针名是L,则把链表称为表L

1、如何表示空表?

  • 无头结点时,头指针为空时表示空表
  • 有头结点时,当头结点的指针域为空时表示空表 

2、在链表中设置头结点有什么好处?

  1. 便于首元结点的处理首元结点的地址保存在头结点的指针域中,所以在链表的第一个位置上的操作和其它位置一致,无须进行特殊处理
  2. 便于空表和非空表的统一处理无论链表是否为空,头指针都是指向头结点的非空指针,因此可以统一空表和非空表的处理

3、头结点的数据域内装的是什么?

  • 头结点的数据域可以为空,也可存放线性表长度等附加信息,但此结点不能计入链表长度值 

链表(链式存储结构)的特点

  1. 结点在存储器中的位是任意的,即逻辑上相邻的数据元素在物理上不一定相邻
  2. 访问时只能通过头指针进入链表并通过每个结点的指针域依次向后顺序扫描其余结点,所以寻找第一个结点和最后一个结点所花费的时间不等。
  • 这种存取元素的方法被称为顺序存取法

2、线性表链式存储结构的实现(单链表)

创建链表

#include <stdio.h>
#include <stdlib.h>
//引用头文件

typedef int ElemType;//自定义数据元素类型,这里在链表中存储的数据元素类型为int

typedef struct Lnode
{
	 ElemType data;//数据域
	 Lnode* next;//指针域
}Lnode,*LinkList;
//将这个数据结构用typedef定义为Lnode,Lnode的指针Lnode*定义为LinkList,增加代码可读性,方便创建指针

3、单链表的操作(有头结点的单链表)

单链表的初始化

bool InitList_L(LinkList L)

步骤:

  1. 生成新结点作头结点,用头指针L指向头结点。
  2. 将头结点的指针域置空
bool InitList_L(LinkList L)
{
	L = (LinkList)malloc(sizeof(Lnode));
	if (!L)
		return false;//判断是否开辟空间失败,失败则返回false
	L->next = NULL;//将next指针置空
	return true;//初始化完成返回true
}

建立单链表

一、头插法

  • 元素查找链表头部,也叫前插法。

步骤:

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

图示:

bool CreateListH(LinkList L, int n)
{
	L = (LinkList)malloc(sizeof(Lnode));
	if (!L)
		return false;
	L->next = NULL;
	//建立一个带头结点的单链表
	LinkList p = NULL;
	for (int i = n; i > 0; --i)
	{
		p = (LinkList)malloc(sizeof(Lnode));//生成新结点p
		scanf("%d",p->data);//输入元素值;

		p->next = L->next;
		L->next = p;
		//插入到表头
	}
	return true;
}

二、尾插法

  • 元素查找链表尾部,也叫后插法。

步骤:

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

图示:

bool CreateListR(LinkList L, int n)
{
	L = (LinkList)malloc(sizeof(Lnode));
	if (!L)
		return false;
	L->next = NULL;
	//建立一个带头结点的单链表
	
	LinkList r = L;//创建尾指针r指向头结点
	LinkList p = NULL;
	for (int i = 0; i < n; ++i)
	{
		p = (LinkList)malloc(sizeof(Lnode));
		if (!p)
			return false;
		p->next = NULL; 
		//生成新结点p

		scanf("%d", p->data);//输入元素值

		r->next = p;//插入到表尾
		r = p;//r指向新的尾结点
	}
}

判断链表是否为空

bool ListEmpty(LinkList L)

空表:链表中无元素,称为空链表(头指针和头结点仍然在)

步骤:

  1. 判断头结点指针域是否为空
bool ListEmpty(LinkList L)
{
	if (L->next)//为空表的话next指针为NULL(0)
		return false;
	else
		return true;
}

销毁单链表

void DestoryList(LinkList L)

  • 销毁:链表不存在,链表所在的空间都被释放

步骤:

  1. 从头指针开始,依次释放所有结点
void DestoryList(LinkList L)
{
	LinkList p;//创建缓存指针,放置地址丢失
	while (L)//当L非空时一直循环,直到指向NULL
	{
		p = L;//缓存指向这个结点的指针
		L = L->next;//将指向这个结点的指针指向下一个结点
		free(p);//释放这个结点的空间
		p = NULL;//将p置空,防止出现野指针
	}
}

清空单链表

void ClearList(LinkList L)

  • 清空:链表仍存在,但链表中无元素,成为空链表(头指针和头结点仍然存在)

步骤:

  1. 依次释放所有结点,并将头结点指针域设置为空
void ClearList(LinkList L)
{
	LinkList p, q;
	p = L->next;
	while (p)
	{
		q = p->next;
		free(p);
		p = q;
	}//步骤与销毁单链表一致,只不过跳过了头结点,从首元结点开始销毁
	L->next = NULL;//销毁完寿元结点之后的结点后将头结点的指针域置空
}

获得单链表的表长

int ListLength(LinkList L)

步骤:

  1. 从首元结点开始,遍历所有结点,依次计数
int ListLength(LinkList L)
{
	LinkList p;
	p = L->next;//将p指向首元结点
	int cnt = 0;//创建计数器
	while (p)//直到p指向尾结点的指针域即p为空的情况下停止
	{
		cnt++;//没经过一个结点,计数器cnt加1
		p = p->next;//将p从这个结点转变为指向下一个结点
	}
	return cnt;
}

查找第i个结点的值

bool GetElemI(LinkList L, int i,ElemType* e)//用e来获得第i个元素的值

步骤:

  1. 从第1个结点(L->next)顺链扫描,用指针p指向当前扫描到的点,p初值p=L->next。
  2. 用 j 做计数器,累计当前扫描过的结点数,初值为1。
  3. 当p指向扫描到的下一结点时,计数器 j 加1。
  4. 当 j == i 时,p所指的结点就是要找的第 i 个结点。
bool GetElemI(LinkList L, int i,ElemType* e)//用e来获得第i个元素的值
{
	LinkList p = L->next;
	int j = 1;
	//初始化

	while (p&& j < i)//向后遍历,直到p指向第i个元素或p为空
	{
		p = p->next;
		j++;
	}
	if (!p || j > i)
		return false;//第i个元素不存在则返回false

	*e = p->data;//把第i个元素存进e中
	return true;
}

查找值为e的结点的位置

LinkList LocateELem(LinkList L, ElemType e)

步骤:

  1. 从第一个结点起,依次和e相比较。
  2. 如果找到一个其值与e相等的数据元素,则返回其在链表中的“位置"或地址。
  3. 如果查遍整个链表都没有找到其值和e相等的元素,则返回0或"NULL"。
//查找值为e的元素的位置
LinkList LocateELem(LinkList L, ElemType e)
{
	LinkList p = L->next;
	while (p && p->data != e)//向后遍历直到找到值为e的数据元素的地址或p指向NULL
		p = p->next;
	return p;
}

在指定位置插入结点

步骤:

  1. 首先找到ai-1的存储位置p。
  2. 生成一个数据域为e的新结点s。
  3. 插入新结点:
    ①新结点的指针域指向结点ai
    ②结点ai-1的指针域指向新结点

①s->next= p->next
②p->next=s;

步骤①和②不能互换!否则会丢失ai的地址

bool ListInsert(LinkList L, int i, ElemType e)
{
	LinkList p = L;
	int j = 0;
	while (p && j < i - 1)
	{
		p = p->next;
		j++;
	}//寻找第i-1个结点,p指向i-1结点
	if (!p || j > i - 1 || i < 1)
		return false;//如果i>表长或者小于1,插入位置非法,返回fales报错

	LinkList s = (LinkList)malloc(sizeof(Lnode));//生成新结点s
	s->data = e;//将结点s的数据域置为e

	s->next = p->next;
	p->next = s;
	//将结点s插入L中

	return true;
}

删除第i个结点

bool ListDeleteI(LinkList L, int i)

步骤:

  1. 首先找到ai-1的存储位置p,
    保存要删除的ai的值。
  2. 令p->next指向ai+1。
  3. 释放结点ai的空间。

bool ListDeleteI(LinkList L, int i)
{
	LinkList p = L; 
	int j = 0;
	//初始化

	while (p->next&& j < i - 1)
	{
		p = p->next;
		j++;
	}//寻找第i个结点,并令p指向其前驱

	if (!(p->next)||j > i - 1)
		return false;//如果删除位置非法,返回false报错

	LinkList q = p->next; //临时保存被删结点的地址以备释放
	p->next = q->next;//将删除结点的前驱结点的指针域指向删除结点的后继结点
	free(q);
	q = NULL;//释放删除结点的空间
	return true;
}

4、循环链表

定义:

  • 一种头尾相接的链表(即:表中最后一个结点的指针域指向头结点,整个链表形成一个环)

图示:

优点:

  • 从表中任一结点出发均可找到表中其他结点

注意事项:

  • 由于循环链表中没有NULL指针,故涉及遍历操作时,其终止条件就不再像非循环链表那样判断p或p->next是否为空,而是判断它们是否等于头指针。

尾指针的循环链表的合并(合并链表a与链表b,将b链接在a后)

尾指针:

一般而言,我们对链表的操作都是从链表的首部或尾部开始的,因此我们在构建链表时,一般会创建一个尾指针指向尾结点来方便我们对链表进行操作。

步骤:

  1. 创建p指针存储a表头结点。
  2. 将b表首元结点接到a表尾结点。
  3. 释放b表头结点。
  4. 修改指针。

①p=Ra->next; ②Ra->next=Rb->next->next; ③free(Rb->next); ④Rb->next=p;

LinkList Connect(LinkList Ra, LinkList Rb)//假设Ra、Rb都是非空的单循环链表
{
	LinkList p = Ra->next;//①p存表头结点
	Ra->next = Rb->next->next;//②Rb表头连结Ra表尾
	free(Rb->next);//③释放Tb表头结点
	Rb->next = p;//④修改指针
	return Rb;
}

 5、双向链表

定义:

  • 单链表的每个结点单再增加一个指向其直接前驱的指针域prior,这样链表中就形成了有两个方向不同的链,故称为双向链表。

 创建双向链表

typedef struct DuLNode{
	ElemType data;
	DuLNode* prior, * next;
} DuLNode, * DuLinkList;

双向链表结构的对称性:

p->prior -> next = p =p->next -> prior(设指针p指向某一结点)

 双向循环链表

和单链的循环表类似,双向链表也可以有循环链表

  • 让头结点的前驱指针指向链表的最后一个结点
  • 让最后一个结点的后继指针指向头结点。

双向链表的操作

  • 在双向链表中有些操作(如:ListLength、GetElem等),因仅涉及一个方向的指针,故它们的算法与线性链表的相同。但在插入、删除时,则需同时修改两个方向上的指针,其他则与单链表的操作相同,两者的操作的时间复杂度均为Q(n)

五、顺序表和链表的比较

1、链式存储结构的优缺点

链式存储结构的优点:

  • 结点空间可以动态申请和释放
  • 数据元素的逻辑次序靠结点的指针来指示,插入和删除时不需要移动数据元素

链式存储结构的缺点:

  • 存储密度小,每个结点的指针域需额外占用存储空间。当每个结点的数据域所占字节不多时,指针域所占存储空间的比重显得很大。

存储密度:结点数据本身所占的存储量和整个结点结构中所占的存储量之比
即:存储密度=结点数据本身占用的空间 / 结点占用的空间总量
例:

一般地,存储密度越大,存储空间的利用率就越高。
显然,顺序表的存储密度为1(100%),而链表的存储密度小于1。

  • 链式存储结构是非随机存取结构。对任一结点的操作都要从头指针依指针链查找到该结点,这增加了算法的复杂度。

2、顺序表的优缺点

优点:

  • 存储密度大
  • 可以随机存取表中任一元素

缺点:

  • 在插入、删除某一元素时,需要移动大量元素
  • 浪费存储空间。
  • 属于静态存储形式,数据元素的个数不能自由扩充。

总结:

  • 13
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值