《数据结构与算法基础》学习笔记——第二章 线性表 List

第二章 线性表 List

2.1 线性表的定义和特点

线性表是具有相同特性数据元素的一个有限序列

其中数据元素的个数n定义为表的长度

当n=0时称为空表

将非空的线性表(n>0)记作: (a1, a2,…an)

这里的数据元素ai(1<=i<=n)只是一个抽象的符号,其具体含义在不同的情况下可以不同。

线性表的例子

例1 分析26个英文字母组成的英文表

(A, B, C, D, …, Z)

26个字母是数据元素; 元素(字母)间关系是线性

例2 某单位历年拥有计算机的数量

某单位历年拥有计算机的数量(6, 14, 18, 50, 92, 198)

例3 12星座

12星座(白羊座、金牛座、双子座、巨蟹座、狮子座、处女座、天秤座、天竭座、射手座、摩羯座、水瓶座、双鱼座)

共同特性总结

同一线性表中的元素必定具有相同特性,数据元素间的关系是线性关系

线性表的逻辑特征

在非空的线性表,有且仅有一个开始结点a,它没有直接前趋,而仅有一个直接后继a2;

有且仅有一个终端结点an,它没有直接后继,而仅有一个直接前趋an-1;

其余的内部结点ai(2<=i<=n-1)都有且仅有一个直接前趋ai-1和一个直接后继ai+1。

线性表是一种典型的线性结构。

2.2 案例引入

2.3 线性表的类型定义

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

ADT List{
	数据对象:D = {ai | ai属于Elemset, (i = 1,2,……,n,n≥0)}		//数据对象是一些元素,元素个数≥0
	数据关系:R = {<ai-1,ai> | ai-1,ai属于D, (i = 2,3,……,n)}	//数据关系序偶关系,ai-1是ai的前驱,ai是ai-1的后继
	基本操作:
		InitList(&L);		DestroyList(&L);
		ListInsert(&L,i,e);	ListDelete(&L,i,&e);
		……等等
}ADT List
InitList(&L)		//(或Initialization List)
	操作结果:构造一个空的线性表L

DestroyList(&L)
	初始条件:线性表L已经存在
	操作结果:销毁线性表L

ClearList(&L)
	初始条件:线性表L已经存在
	操作结果:将线性表L重置为空表
	
ListEmpty(L)
	初始条件:线性表L已经存在
	操作结果:若线性表L为空表(n=0),则返回TRUE;否则返回FALSE

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

GetElem(L, i, &e);
	初始条件:线性表L已经存在,1 <= i <= ListLength(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返回它的前驱。否则操作失败,pre_e无意义。

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

ListInsert(&L, i, e)
	初始条件:线性表L已经存在,1 <= i <= ListLength(L) + 1
	操作结果:在线性表L的第i个位置之前插入新的数据元素e,L的长度加一

ListDelete(&L, i, &e)
	初始条件:线性表L已经存在,1 <= i <= ListLength(L)
	操作结果:删除线性表L的第i个数据元素,并用e返回其值,L的长度减一

ListTraverse(&L, visited())
	初始条件:线性表L已经存在
	操作结果:依次对线性表中每个元素调用visited()		【遍历】

以上所提及的运算是逻辑结构上定义的运算,属于“做什么”,至于“如何做”等实现细节,只有待确定了存储结构之后才考虑。下面开始实现。

2.4 线性表的顺序表示和实现

2.4.1 线性表的顺序存储表示

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

顺序存储定义:逻辑上相邻,物理上也相邻。

线性表的第一个数据元素a1的存储位置,称作线性表的起始位置基地址

线性表(1,2,3,4,5,6)的存储结构:

依次存储,地址连续,中间没有空出存储单元——一个典型的线性表顺序存储结构

占用一片连续的存储空间。

地址不连续,中间存在空的存储单元——不是一个线性表顺序存储结构

【例】如果每个元素占用8个存储单元,ai存储位置是2000单元,则ai+1存储位置是? 2008单元

所有数据元素的存储位置均可由第一个数据元素的存储位置得到:

LOC(ai) = LOC(a1) + (i - 1) × L		//a1为基地址

顺序表(元素):地址连续、依次存放、随机存取、类型相同
(这些特点与数组一致)
用一维数组表示顺序表

线性表长度可变(删除)
C语言中,数组长度不可动态定义
解决方法:另用一变量表示顺序表的长度属性

线性表类型定义模板:

#define LIST_INIT_SIZE 100		//线性表存储空间的初始分配量
typedef struct{
	ElemType elem[LIST_INIT_SIZE];		//定义数组
	int length;		//当前长度
}SqList;

补充:元素类型说明

顺序表类型定义

typedef struct{
	ElemType data[];		//定义数组
	int length;				//当前长度
}SqList;					//总体来看,是一个【多项式】

ElemType是可变类型,根据后面data[]的实际类型变化,可以是int/char/float等

也可以先定义:

typedef char ElemType;
typedef int ElemType;

数组定义

①数组静态分配
typedef struct{
	ElemType data[MaxSize];		//data[MaxSize]放的是第一个元素的地址
	int length;
}SqList;		//顺序表类型
②数组动态分配
typedef struct{
	ElemType *data;		//*data也可以表示数组,多大呢?
	int length;
}SqList;		//顺序表类型

数组有多大?用动态内存分配的函数来分配内存:

SqList L;		//L有两个成员
L.data = (ElemType*)malloc(sizeof(ElemType)*MaxSize);
//(ElemType*)强制类型转换成指向对应类型的指针
//sizeof(ElemType)中,ElemType的大小是根据数据类型变化的。
malloc(m)函数

开辟m字节长度的地址空间,并返回这段空间的首地址。

sizeof(x)运算

计算变量x的长度

free§函数

释放指针p所指变量的存储空间,即彻底删除一个变量

(需要加载头文件<stdlib.h>)

C++的动态存储分配(借助来简化)

new 类型名T(初值列表)

int *p1 = new int; 或 int *p1 = new int(10);

功能:申请用于存放T类型对象的内存空间,并依初值列表赋以初值

结果值:

​ 成功:T类型的指针,指向新分配的内存

​ 失败:0(NULL)

delete 指针P

功能:释放指针P所指向的内存。P必须是new操作的返回值

C++的参数传递(借助来简化)

三个一致

函数调用时传送给形参表的实参必须与形参三个一致

​ 类型、个数、顺序

参数传递有两种方式

​ ①传值(参数为整形、实型、字符型等)(只改变形参,实参值不发生改变)

​ ②传地址(参数为指针变量、引用类型、数组名)(共用一块空间,形参、实参都会变化)

形参变化影响实参
形参变化不影响实参

​ 传地址方式——指针变量作参数

传地址方式——引用类型作参数

引用:用来给一个对象提供一个替代的名字

①通过引用变量直接操作实参,形参变化实参也变化

②引用变量,形参与实参共用一块存储空间

#include <iostream>
using namespace std;

void swap(float& m, float& n)
{
	float temp;
	temp = m;
	m = n;
	n = temp;
}

int main()
{
	float a, b;
	cin >> a >> b;
	swap(a, b);
	cout << a << endl << b << endl;
}

逻辑位序和物理位序相差1。

Sequence List 顺序表(SqList)

描述:

#define MAXSIZE 100
typedef struct{
	ElemTpye elem [MAXSIZE];
	int length;
} SqList;			//静态分配
typedef struct{
	ElemType *elem;
	int length;
}SqList L;	//定义变量L,L是SqList这种类型的,是个顺序表
L.elem = (ElemTpye*)malloc(sizeof(ElemTpye)*MAXSIZE);	//动态分配

顺序表示意图

普通变量用xxx.xxx来引用,指针变量用xxx->xxx来引用

2.4.2 顺序表基本操作的实现

线性表的基本操作
操作算法中预定义好的常量和类型

#define TRUE 1
#define FALSE 0
#define OK 1
#define ERROR 0
#define INFEASIBLE -1
#define OVERFLOW -2

#define int Status;
#define char ElemType;

【算法2.1】线性表L的初始化(参数用引用)
Status InitList_Sq(SqList &L){		//构造一个空的顺序表L
	L.elem = new ElemType[MAXSIZE];	//首元素地址赋值给L.elem
	if(!L.elem) exit(OVERFLOW);		//存储分配失败,返回-2
	L.length = 0;					//空表长度为0
	return OK;
}
销毁线性表L
void DestroyList(SqList &L){
	if(L.elem) delete L.elem;
}
清空线性表L
void ClearList(SqList %L){
	L.length = 0;		//将线性表长度置为0,内存仍存在
}
求线性表L的长度
int GetLength(SqList L){
	return (L.length);
}
判断线性表L是否为空
void IsEmpty(SqList L){
	if(L.length == 0) return 1;
	else return 0;
}
【算法2.2】顺序表的取值

(根据位置i获取相应位置数据元素的内容)

int GetElem(SqList L, int i, ElemType &e){
	if(i<1 || i>L.length) return ERROR;	//判断i值是否合理,若不合理,返回ERROR
	e = L.elem[i-1];					//第i-1的单元存储着第i个数据
    return OK;
}

随机存取

时间复杂度:O(1)——所有语句都只执行一次,常量阶。

【算法2.3】顺序表的查找(按值查找)
int LOcateElem(SqList L, ElemType e){
	//在线性表L中查找值为e的数据元素,返回其序号(第几个元素)
	for(i = 0; i < L.length; i++)	//亦可用while循环
		if(L.elem[i] == e) return i+1;	//查找成功,返回序号
	return 0;		//查找失败,返回0
}

时间复杂度:找到执行次数最多的一条语句。不能确定执行多少次,所以用平均查找长度。

平均查找长度ASL (Average Search Length):

为确定记录在表中的位置,需要与给定值进行比较的关键字的个数的期望值叫做查找算法的平均查找长度。

时间复杂度:O(n)——最高次方是1次

顺序表的元素插入

插入位置在最后

插入位置在中间

插入位置在首位

算法思想:

①判断插入的位置i是否合法。
②判断顺序表的存储空间是否已满,若已满返回ERROR。
③将第n至第i位的元素依次向后移动一个位置,空出第i个位置。
④将要插入的新元素e放入第i个位置。
⑤表长+1,插入成功返回OK。

Status ListInsert_Sq(SqList &L, int i, ElemType e){
	if(i<1 || i>L.length+1) return ERROR;	//①i值不合法
	if(L.length == MAXSIZE) return ERROR;	//②当前存储空间已满
	for(j = L.length-1; j >= i-1; j--)
		L.elem[j+1] = L.elem[j];		//插入位置及之后的元素后移
    L.elem[i-1] = e;	//将新元素he放入第i个位置
    L.length++;		//表长增1
    return OK;
}

找到规律:插入位置 + 移动次数 = n+1

等差数列求和方式,得平均移动次数:n/2,级数为n

所以时间复杂度:O(n)

顺序表的元素删除

删除元素在最后

删除元素在中间

删除位置在首位

算法思想:

①判断删除的位置i是否合法(合法值为1 <= i <= n)。
(②将欲删除的元素保留在e中。)
③将第i+1至第n位的元素依次向前移动一个位置。
④表长-1,删除成功返回OK。

Status ListDelete_Sq(SqList &L, int i){
	if(i<1 || i>L.length+1) return ERROR;	//①i值不合法
	for(j = i; j<=L.length-1; j++)
		L.elem[j-1] = L.elem[j];		//被删除位置之后的元素前移
    L.length--;		//表长减1
    return OK;
}

所以时间复杂度:O(n)

顺序表优缺点

2.5链表

2.5.1与链式存储有关的术语

1、结点:

​ 数据元素的存储映像,由数据域和指针域两部分组成。

2、链表:

​ n个结点由指针链组成一个链表
​ 是线性表的链式存储映像,称为线性表的链式存储结构。

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

结点只有一个指针域的链表,称为单链表或线性链表。
结点由两个指针域的链表,称为双链表。
首尾相接的链表称为循环链表。

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

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

讨论1:如何表示空表?

无头结点时:

​ 头指针为空时表示空表

有头结点时:

​ 当头结点的指针域为空时表示空表

讨论2:在链表设置头结点有什么好处?

1、便于首元结点的处理

使在链表的第一个位置上的操作和其他位置一致,无需进行特殊处理

2、便于空表和非空表的统一处理

无论链表是否为空,头指针都是指向头结点的非空指针,因此空表和非空表的处理就一致了

讨论3:头结点的数据域内装的是什么?

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

链表的特点

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

顺序表——随机存储
链表——顺序存取

带头结点的单链表

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

单链表的存储结构

嵌套定义】自己定义自己

typedef struct Lnode{
	ElemType data;
	struct Lnode *next;
}Lnode, *LinkList;

例:存储学生学号、姓名、成绩的单链表结点类型定义如下:

typedef Struct student{
	char num[8];			//数据域
	char name[8];			//数据域
	int score;				//数据域
    struct student *next;	//指针域
}Lnode, *LinkList;

LinkList L;		//定义指向类型LinkList的指针L
【更常用的定义】:

先将要存储的多个数据项定义成一个数据类型

typedef Struct{
	char num[8];
	char name[8];
	int score;
}ElemType;

typedef struct Lnode{
	ElemType data;
	struct Lnode *next;
}Lnode, *LinkList;

2.5.2【单链表基本操作的实现】

▷类型定义:
typedef struct LNode{
	ElemType data;
	struct LNode *next;
}Lnode, *LinkList;		//定义新的类型名:①类型的结点LNode; ②指向这种结点类型的指针类型*LinkList
▷变量定义:
LinkList L;		//定义头指针L	【LinkList类型本身是一个指针,所以L不用加*】
LNode *p, *s;	//指向结点的指针	【需要加*】
▷重要操作:熟练掌握!
p = L;			//p指向头结点
s = L -> next;	//s指向首元结点
p = p -> next;	//p指向下一结点
一、单链表的初始化

构造一个空表(带头结点的单链表)

步骤:
(1)生成新结点作头结点,用头指针L指向头结点
(2)将头结点的指针域置空

算法描述:

Status InitList L(LinkList &L){
	L = new LNode;	//或C语言中的L = (LinkList)malloc(sizeof(LNode));	//将得到的地址复制个L
	L -> next = NULL;
    return OK;
}
补充单链表的几个常用简单算法:
(1)判断链表是否为空

空表:头指针和头结点存在,但链表中无元素,称为空链表。

【算法思路】判断头结点指针域是否为空

int ListEmpty(LinkList L){
	if(L -> next)		//非空
		return 0;
	else
		return 1;
}
(2)单链表的销毁

链表销毁后,头指针头结点都不存在了

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

Status DestroyList_L(LinkList &L){	//销毁单链表L
	Lnode *p;		//或LinkList p;
	while(L){
		p = L;
		L = L -> next;
        delete p;		
	}
	return OK;
}
【前后要对应使用】

C: malloc(); free();
C++: new(); delete();

(3)清空链表

链表、头指针、头结点仍存在,但链表中无元素,成为空链表

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

Status ClearList(LinkList &L){		//将L重置为空表
	Lnode *p, *q;	//或LinkList p,q;
	p = L -> next;
	while(p)		//没到表尾
	{
		q = p -> next;
		delete p;
		p = q;
	}
	L -> next = NULL;	//头结点指针域置位空
	return OK;
}
(4)求单链表的表长

算法思路:从首元结点开始,依次计数所有结点

int ListLength_L(LinkList L){
	LNode *p;		//或LinkList p;
	p = L -> next;
    i = 0;
    while(p)
    {
        i++;
        p = p -> next;
    }
    return i;
}
二、单链表的取值

与顺序表不同,链表中取值只能从链表的首元结点出发,顺着链域next逐个结点向下访问,链表不是随机存取结构。

步骤:

(1)用指针p指向首元结点,用j做计数器初值赋为1
(2)从首元结点开始依次顺着链域next向下访问,只要指向当前结点的指针p不为空(NULL),并且没达到序号为i的结点,则循环执行:
p指向下一个结点
计数器j+1
(3)退出循环时,若指针p为空,或者计数器j大于1,说明指定的序号i值不合法(i大于表长n或i<=0),取值失败返回ERROR;
否则取值成功,此时j=i时,p所指的结点就是要找的第i个结点,用参数e保存当前结点的数据域,返回OK。

Status GetElem_L(LinkList L, int i, ElemType &e){	//在带头结点的单链表L中根据序号i获取元素的值,用e返回L中第i个元素的值
    p = L->next; j = 1;		//初始化,p指向首元结点,计数器j初值赋为1
    while(p && j < i)			//顺链域向后扫描,直到p为空或p指向第i个元素
    {
        p = p->next;
        ++j;
    }
    if(!p || j>1) return ERROR;
    e = p->data;
    return OK;
}
三、查找

按值查找——给一个数据,找到后返回该数据所在的位置(地址)

Lnode *LocateElem_L(LinkListL, Elemtype e){
	p = L->next;
	while(p && p->data != e)	//查找值e,找到则返回值为e的数据元素的地址
		p = p->next;
	return p;
}

按值查找——给一个数据,找到后返回该数据所在的位置序号

int LocateElem_L(LinkList L, Elemtype e){
	p = L->next;
	j = 1;
	while(p && p->data != e)
	{
		p = p->next;
		j++;
	}
	if(p)
    	return j;
	else
		return 0;
}
四、插入

在第i个结点前插入值为e的新节点

步骤:
①首先找到ai-1的存储位置p
②生成一个数据域为e的新结点s
③插入新结点:新结点的指针域指向结点ai、结点ai-1的指针域指向新结点
s->next = p->next; p->next = s;

【算法描述】

//在L中第i个元素之前插入数据元素e
Status ListInsert_L(LinkList &L, int i, ElemType e){
	p = L; j = 0;
    while(p && j<i-1)	//寻找第i-1个结点,p指向其前驱结点i-1
    {
        p = p->next;
        ++j;
    }
    if(!p || j>i-1)	return ERROR;	//如果i大于表长j+1,或者小于1,插入位置非法
    s = new Lnode;		//生成新节点s
    s->data = e;		//将结点s的数据域置为e
    s->next = p -> next;	//将结点s插入L中,接在i的前驱
    p->next = s		//将结点s接在i-1的后继
   	return OK;
}
五、删除

删除第i个结点

【p->next = p->next->next】

//将线性表L中第i个数据元素删除
Status ListDelete_L(LinkList &L, int i, ElemType &e){
	p = L; j = 0; q = i;
	while(p->next && j < i - 1)			//寻找第i个结点,并【令p指向其前驱i-1】
    {
        p = p->next;
        ++j;
    }
    if(!(p->next) || j > i - 1)	return ERROR;	//删除位置不合理
    q = p->next;	//q指针指向要删除的结点i,临时保存以备释放 
    p->next = q->next;		//改变删除结点前驱结点的指针域	//或p->next = p->next->next
    e = q->data;	//保存删除结点的数据域
    delete q;		//释放删除结点的空间
    return OK;
}
各种算法时间复杂度
1、查找:

​ 线性链表只能顺序存取,从头开始查找,时间复杂度O(n)

2、插入删除:

​ 线性链表不需要移动元素,只需要修改指针,一把情况下时间复杂度O(1)。
​ 但是如果需要查找插入位置,时间复杂度为O(n)

单链表的建立
前插法(头插法)

元素插入在链表头部(头结点后面),从最后一个结点开始操作插入

【思路】

L = new LNode;	L->next = NULL;				//或用C语言语法:L = (LinkList)malloc(sizeof(LNode));
p = new LNode;	p->data = an;		//new一个新的空间,将值an写入新空间的数据域
p->next = L->next;		//将老结点的指针域 拿到 新节点的指针域
L->next = p;			//将新结点接在头结点L后面

【算法描述】

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;		//【重点操作】新结点接到表头
    }
}

时间复杂度:O(n)

后插法(尾插法)

元素插在链表尾部

【算法描述】

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

时间复杂度:O(n)

2.5.3 循环链表

循环链表的非空表:

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

优点:从表中任一结点出发均可以找到表中其他结点。

循环链表的空表:

指针域指向本身

循环链表中没有NULL指针,故执行遍历操作时,其终止条件应该:判断是否等于头指针。

引入尾指针

表的操作常常是在表的首尾位置上进行。

带尾指针的循环链表的合并(将Tb合并在Ta之后)

操作:p存表头结点,Tb表头连接到Ta表尾,释放Tb表头结点,删除指针

p = Ta->next;
Ta->next = Tb->next->next;
delete Tb->next;

LinkList Connect(LinkList Ta, LinkList Tb){
	
    p = Ta->next;
    Ta->next = Tb->next->next;
    delete Tb->next;		//或free(Tb->nexr);
    Tb->next = p;
    return Tb;
}

时间复杂度是O(1)。

2.5.4 双向链表

在单链表的每个结点里再增加一个指向其直接前驱的指针域prior,这样链表中就形成两个方向不同的链。

双向链表的结构定义:

typedef struct DuLNode{
	Elemtype data;
	struct DuLNode *prior, *next;
}DuLNode, *DuLinkList;
双向循环链表
双向链表结构的对称性

p->prioir->next = p = p->next->prior

双向链表的插入

【算法2.13】

//在带头结点的双向循环链表L中第i个位置之前插入元素e
void ListInsert_Dul(DuLinkList &L, int i, ElemType e){
    if(!(p = GetElemP_DuL(L,i)))	return ERROR;	//调用在链表L中找第i个位置的操作,将i地址赋值给p
    s = new DuLNode;		//指针变量s指向一个新节点
    s->data = e;			//s的data域赋值。以下修改双向四个指针:
    s->prior = p->prior;
    p->prior->next = s;
    s->next = p;
    p->prior = s;
    return OK;
}
双向链表的删除

【算法2.14】

//删除带头结点的双向循环链表L的第i个元素,并用e返回。
void ListDelete_DuL(DuLink &L, int i, ElemType &e){
	if(!(p = GetElemP_DuL(L,i)))	return ERROR;
	e = p->data;
    p->prior->next = p->next;
    p->next->prior = p->prior;
    free(p);
    return OK;
}

单链表、循环链表、双向链表的时间效率比较

O(n) 变成 O(1),牺牲空间效率,换成时间效率。

2.6 顺序表和链表的比较

链式存储结构的优缺点:

存储密度:

顺序表和链表的比较:

2.7 线性表的应用

2.7.1线性表的合并

void union(List &La,List Lb){	//结果通过La返回,所以La要加&
    La_len = ListLength(La);	//求出表La的长度
    Lb_len = ListLength(Lb);	//求出表Lb的长度
    for(i = 1; i <= Lb_len; i++)
    {
        GetElem(Lb, i, e);		//将线性表Lb中第i个位置元素返回给e
        if(!LocateElem(La, e))	ListInsert(&La, ++La_len, e);	//如果没找到元素,则在线性表La中第++La_len个位置插入新元素e
    }
}

时间复杂度:执行次数最多的是:ListInsert(&La, ++La_len, e);语句。属于for循环与if循环的内层
则时间复杂度:O(ListLength(La))*O(ListLength(Lb))

注:基本操作不用再写,直接用(见前文2.4.2)

2.7.2有序表的合并

有两种方法(顺序表、链表):

顺序表法
void MergeList_Sq(SqList La, SqList Lb, SqList &Lc){
    pa = LA.elem;
    pb = LB.elem;	//指针pa和pb的初值分别指向两个表的第一个元素
    LC.length = LA.length + LB.length;	//新表长度为待合并两表的长度之和
    LC.elem = new ElemType[LC.length];	//为合并的新表分配一个数组空间
    pc = LC.elem;	//指针pc指向新表的第一个元素
    pa_last = LA.elem + LA.length - 1;	//指针pa_last指向LA表的最后一个元素
    pb_last = LB.elem + LB.length - 1;	//指针pb_last指向LB表的最后一个元素
    while(pa <= pa_last && pb <= pb_last){	//当两个表都非空,若有一个表空了则退出循环
        if(*pa <= *pb)	*pc++ = *pa++;		//依次“摘取”两表中值较小的结点
        else *pc++ = *pb++;
    }
    while(pa <= pa_last)	*pc++ = *pa++;	//LB表已到达表尾,将LA中剩余元素加入LC
    while(pb <= pb_last)	*pc++ = *pb++;	//LA表已到达表尾,将LB中剩余元素加入LC
}

时间复杂度:取决于while(pa <= pa_last && pb <= pb_last)循环,取决于其中较短的表。与while(pa <= pa_last)或while(pb <= pb_last),将剩余的内容加入表LC。
时间复杂度为:O(ListLength(LA)+ListLength(LB))

空间复杂度:表LC的表长,取决于表LA和表LB的长度和。
空间复杂度为:O(ListLength(LA)+ListLength(LB))

链表法【典型】

用带头结点的单链表

void MergeList_L(LinkList &La, LinkList &Lb, LinkList &Lc){
    pa = La->next;	pb = Lb->next;		//指针pa、pb指向两个首元结点
    pc = Lc = La;		//用La的头结点作为Lc的头结点
    while(pa && pb)
    {
        if(pa->data < pb->data)
        {
            pc->next = pa;
            pc = pa;
            pa = pa->next;
        }
        else
        {
            pc->next = pb;
            pc = pb;
            pb = pb->next;
        }
        pc->next = pa?pa:pb;	//插入剩余段
        delete Lb;				//释放Lb的头结点
    }
}

时间复杂度:while(pa && pb)较短的,最坏情况:pa、pb所有结点都要访问一次。
算法的时间复杂度:O(ListLength(La) + ListLength(Lb))

空间复杂度:只在原来的链表上操作,不需要额外的空间。
算法的空间复杂度:O(1)


阅读须知

【因笔者所使用编辑器限制,笔者无法将图片插入笔记中,大家在阅读笔记时可能有所不便。如有需要,请联系笔者发送完整笔记。给您带来不便深表歉意,谢谢!】

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

鱼 丸

您的鼓励将是我创作的最大动力!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值