【数据结构】学习笔记(一)—— 线性表、栈、队列

文章目录

  • 一.基础知识
    • I. 数据结构的概念
    • II. 算法性能分析与度量
  • 二.线性表
    • I. 线性表的类型定义
    • II. 顺序表及其操作实现
    • III. 链表及其操作实现
      • i. 单链表
      • ii. 循环链表
      • iii. 双向链表
      • iv. 静态链表
    • IV. 线性表存储表示的比较
  • 三. 栈
    • I. 栈的类型定义
    • II. 栈的存储表示
    • III. 栈的操作实现
    • IV. 栈的应用
  • 四. 队列
    • I. 队列的类型定义
    • II. 队列的存储表示
      • i. 循环队列
      • ii. 链队列
    • III. 队列的操作实现
      • i. 循环队列的操作实现
      • ii. 链队列的操作实现

一.基础知识

 

I. 数据结构的概念

 
1.数据:数据是信息的载体,是描述客观事物的数或字符,以及所有能输入到计算机中并被计算机程序识别和处理的符号的集合。

2.数据结构:由某一数据元素(数据的基本单位)的集合和该集合中数据元素之间的关系组成的结构。记作:

Data_Structure = { D, R }

其中,D指某一数据元素的集合,R指该集合中所有数据元素之间的关系的有限集合。翻译成人话,就是:

(数据构成的)集合+(数据之间的)关系 = 数据结构

3.分类:依据数据元素之间的关系的不同,我们可以把数据结构分为两大类:线性结构和非线性结构。其中又可以细分为众多分支,比如线性结构包括了一维数组、栈、队列等等,非线性结构包括了多维数组、树、图等等,这些具体的数据结构将会在后面详细列出。按逻辑结构可分为集合、线性结构、树状结构、网状结构四大类。

4.线性结构

1)线性结构也叫线性表。在线性结构当中,所有的数据元素都按照某种次序排列在一个序列当中,所以线性结构就像一根链子。也正因如此,它的第一个元素没有前驱,最后一个元素也没有后继。

2)根据对数据元素的存取方法的不同,我们还可以把线性结构分为直接存取结构、顺序存取结构和字典结构。直接存取结构可以直接存取某一指定项而不必先访问前驱;而顺序存取结构只能从第一个元素起按顺序依次访问直到指定的元素;字典结构则与数组类似,其可以通过关键码进行索引。

3)线性结构数据类型有两种传统的实现方式:基于数组的顺序表示和基于链表的链接方式。

5.非线性结构

1)和线性结构不同,非线性结构当中的每个数据元素可以与零个或者多个其他数据元素发生联系。同链状的线性结构相比,非线性结构更像是一张网。

2)根据关系的不同,我们还可以把非线性结构分为层次结构和群结构。层次结构是按照层次划分数据元素的集合,指定层次上元素可以由零个或多个处于下一层次上的直接所属下层元素(树);而群结构中所有元素之间没有顺序层次关系(图)。

6.数据类型

数据类型可以分为两大类:原子类型和结构类型。

7.抽象数据类型

抽象数据类型(ADT)是指一个数据模型以及定义在该模型上的一组操作。其采用如下格式进行表述:

ADT 抽象数据类型名
{
	Data:
		<数据对象的定义>
	Relation:
		<数据逻辑对象的定义>
	Operation:
		<基本操作的定义>
}
ADT 抽象数据类型名 

基本操作有两种参数:赋值参数和引用参数。赋值参数只为操作提供输入值,而引用参数以&开头,除了可以输入值之外,还将返回操作结果。

例如,复数的抽象数据类型定义如下:

ADT Complex
{
	Data:
		D={e1,e2|e1,e2∈R}
	Relation:
		R={<e1,e2>|e1是复数的实部,e2是复数的虚部}
	Operation:
		InitComplex(&z,v1,v2)
		    初始条件:无。
			操作结果:构造并返回复数z,其实部和虚部分别赋予参数v1和v2的值。
		GetReal(z,&RealPart)
		    初始条件:复数z已经存在。
			操作结果:用RealPart返回复数z的实部值。
		GetImag(z,&ImagPart)
		    初始条件:复数z已经存在。
			操作结果:用ImagPart返回复数z的虚部值。
		Add(z1,z2,&sum)
		    初始条件:复数z1和z2已经存在。
			操作结果:用sum返回两个复数z1,z2的和值。
	    Subtract(z1,z2,&sub)
		    初始条件:复数z1和z2已经存在。
			操作结果:用sub返回两个复数z1,z2的差值。 
		Multiply(z1,z2,&mult)
		    初始条件:复数z1和z2已经存在。 
			操作结果:用mult返回两个复数z1,z2的积值。
		Division(z1,z2,&div)
		    初始条件:复数z1和z2已经存在。
			操作结果:用div返回两个复数z1,z2的商值。 
}
ADT Complex

8.存储结构

存储结构又称为物理结构,其分为顺序存储结构和链式存储结构两大类。
 

II. 算法性能分析与度量

 
1.定义:算法是为解决某一特定任务规定的一个运算序列的指令集。

2.特性

1)有输入:一个算法必须有0个或多个输入。
2)有输出:一个算法必须有1个或多个输出。
3)确定性:算法每一步应该确切地、无歧义地进行定义。
4)有穷性:一个算法在任何情况下都应该在执行有穷步之后结束。
5)能行性:算法中每一条运算都必须是足够基本的,即都可以被计算机执行。

3.描述方式:语言方式是最常见的算法描述方式,我们经常把高级编程语言和自然语言相结合来描述算法。此外,还有图形方式和表格方式。

4.性能标准

1)正确性:要求算法能够正确地执行预定的功能和性能要求。
2)可使用性:要求算法能够很方便地使用,即人性化。
3)可读性:要求算法可读,其逻辑应该清晰简单,并且加以注释理解。
4)效率:算法执行时计算机资源的消耗(空间代价、时间代价)应该尽可能地小。
5)健壮性:算法中需要加入对输入参数、打开文件、读文件记录、自动检错、报错纠错的功能。
6)简单性:算法所采用的数据结构和方法应该尽可能简单,提升可靠程度。

5.效率的度量:包括事前估计和事后估计。

1)事前估计:分为空间复杂度度量和时间复杂度度量,即对算法复杂性的估计。
2)事后估计:主要通过在算法中的某些部位插入时间函数来测定算法完成某一规定功能所需的时间。

6.时间复杂度度量

算法语句的执行次数称为语句频度。语句频度与语句执行一次所需要时间的乘积定义为每条语句的执行时间。算法中每条语句的执行时间之和即为一个算法所耗费的时间,记为T(n)。其中n称为问题规模,即算法求解问题的输入量。

一般情况下,一个算法所耗费的时间T(n)是算法所求解问题规模n的某个函数f(n),算法的时间度量则记为T(n)=O(f(n)),当n趋向无穷大时,T(n)的数量级称为算法的时间复杂度。

例如,交换函数Exchange的时间复杂度为常量阶,即T(n)=O(1)。

void Exchange(int a,int b)  //常量阶,O(1)
{
	t=a;  //频度为 1 
	a=b;  //频度为 1 
	b=t;  //频度为 1 
}

一般情况下,如果算法内不存在与n有关的循环,它的时间复杂度基本为O(1)。

对于步进循环语句,算法的时间复杂度是由循环体中基本操作执行次数来决定的。

void Count_1()  //O(n^2)
{
	x=0; y=0;
	for(k=1;k<=n;k++)
	{
		x++;
	}
	for(i=1;i<=n;i++)
	{
		for(j=1;j<=n;j++)
		{
			y++;  //双重循环,频度为n^2 
		}
	}
}

常见的时间复杂度(按数量级递增)

1.所有递减函数的阶均小于常数阶。

2.常量阶      O(1)

3.对数阶      O(log2n)

4.线性阶      O(n)

5.线性对数阶  O(nlog2n)

6.幂函数阶    O(n^k)

7.普通指数阶  O(2^n)

8.阶乘阶      O(n!)

9.幂指函数阶  O(n^n)

时间复杂度要尽可能地小,如果不是必须,应该避免使用高阶算法。比如说,乘法的时间复杂度大于加法,同等条件下应该优先使用加法。

算法是时间复杂度不仅与问题的规模有关,还与输入实例中的初始状态有关。但是在最坏的情况下,其时间复杂度就是只与求解问题的规模相关的。在讨论时间复杂度时,一般都是以最坏情况下的时间复杂度为标准的。

7.空间复杂度度量

空间复杂度为该算法所耗费的存储空间S(n),其也是关于n的函数,分析方法与算法的时间复杂度类似。

算法在执行期间所耗费的存储空间应该包括三个部分。第一部分为输入数据所耗费的存储空间,其只取决于问题本身,与算法无关。第二部分为程序代码所耗费的存储空间,对不同的算法而言也无较大差别。第三部分为辅助变量所耗费的存储空间,随算法的不同而改变。

一般而言,在求解算法的空间复杂度时,只需要分析算法执行过程中辅助变量所耗费的存储空间即可。
 

二.线性表

 

I. 线性表的类型定义

 
1.线性表

线性表是n个具有相同属性的数据元素组成的有限序列,表中的数据元素属于同一个数据对象,且相邻的数据元素之间存在着“序偶”关系,当n的值为0的时候称作空表。线性表也是最简单、最基本、最常用的一种线性结构,其长度可以根据需要增长或缩短。它的主要基本操作是插入、删除和查找。

2.线性表的抽象数据类型

ADT List
{
	Data:
		D={ai|ai∈ElemSet,i=1,2,...,n,n>=0} (具有相同类型的数据元素集合)
		
	Relation:
		R={<a(i-1),ai>|a(i-1),ai∈D,i=2,3,...,n} (相邻数据元素具有前驱和后继的关系)
	
	Operation:
		InitList(&L)
		    初始条件:无。
			操作结果:构造一个空线性表L。
		DestroyList(&L)
		    初始条件:线性表L已经存在。
			操作结果:销毁L。
		ClearList(&L)
		    初始条件:线性表L已经存在。
			操作结果:重置L为空表。
		ListLength(L)
		    初始条件:线性表L已经存在。
			操作结果:返回L中的数据元素的个数。
		GetElem(L,i,&e)
		    初始条件:线性表L已经存在,且1<=i<=ListLength(L)。
			操作结果:用e返回L中的第i个数据元素的值。
		LocateElem(L,e)
		    初始条件:线性表L已经存在。 
			操作结果:在L中查找第1个其值与e相等的数据元素,并返回该元素在L中的位序;如果L中没有此元素,则返回0ListInsert(&L,i,e)
		    初始条件:线性表L已经存在,且1<=i<=ListLength(L)+1。
			操作结果:在L中第i个位置之前插入新的数据元素e。
		ListDelete(&L,i,&e)
		    初始条件:线性表L已经存在,且1<=i<=ListLength(L)。
			操作结果:删除L的第i个数据元素,并用e返回其值,且令L的长度减1TraverseList(L)
		    初始条件:线性表L已经存在。
			操作结果:依次访问L中的每个数据元素。 
}
ADT List 

当然,对于不同的应用,线性表的基本操作并不相同,故并非任何时候都需要以上所有的操作,有些只需要一部分操作即可。
 

II. 顺序表及其操作实现

 
1.顺序表

采用顺序存储表示的线性表称作顺序表,其用一维数组实现。数组的下标可以看做是线性表数据元素在内存中的相对地址,即以数据元素在计算机内“物理位置相邻”来表示线性表中数据元素之间的逻辑关系。

设线性表中每个数据元素占用d个存储单元,第一个数据元素的起始地址为LOC(a1),则第i个数据元素的起始地址为:

LOC(ai)=LOC(a1)+(n-1)d  (i=1,2,3...,n)

可以看出,在顺序表中,每个数据元素ai的存储地址是该元素在线性表中的位置i的线性函数,只需确定第一个元素的起始地址和每个元素的空间大小,就可以求出任何一个数据元素的存储地址。这个特性又叫做随机存取结构。

顺序表的示意图如下:

【数据结构】学习笔记(一)—— 线性表、栈、队列_第1张图片
顺序表的代码表示:

#define LIST_INIT_SIZE 100  //线性表存储空间的初始分配量 
#define LIST_INCREMENT 10   //线性表存储空间的分配增补量 

typedef struct SqList  //顺序表结构
{
	ElemType *elem;  //线性表存储空间的基地址 
	int length;      //线性表的当前长度 
	int listsize;    //线性表当前分配的存储容量 
} 

2.顺序表的操作实现

初始化操作

void InitList_Sq(SqList &L)  //构造顺序表 L 
{
	L.elem=new ElemType[LIST_INIT_SIZE];  //分配存储区域,设置线性表的存储容量
	if(!L.elem)  //存储分配失败的情况 
	{
		Error("存储分配失败!")
	}
	L.length=0;  //设置空表的长度为0 
	L.listsize=LIST_INIT_SIZE;  //设置线性表的当前存储容量为顺序表的最大容量 
}

void Error(char *s)  //出错信息处理函数 
{
	cout<<s<<endl;
	exit(1);
}

销毁操作

void DestroyList_Sq(SqList &L)  //销毁顺序表 L 
{
	delete [] L.elem;  //释放 L中元素所占用的存储空间 
	L.length=0;
	L.listsize=0;
}

清空操作

void ClearList_Sq(SqList &L)  //清空顺序表 L 
{
	L.length=0;  //重置顺序表为空表 
}

求表长操作

int ListLength_Sq(SqList L)  //求顺序表 L的长度 
{
	return L.length;  //返回L的长度 
}

取值操作

void GetElem_Sq(SqList L,int i,ElemType &e)  //取指定位置元素的值 
{
	if((i<1)||(i>L.length))  //i超范围,报错 
	{
		Error("参数输入错误!")
	}
	e=L.elem[i-1];  //用e返回L中的第i个元素值 
}

定位操作

int LocateElem_Sq(SqList L,ElemType e)  //定位指定位置的元素 
{
	i=1;  //指示位序,初始值为 1 
	p=L.elem;  //指向 L的第 i个元素,初始时指向首元素 
	while((i <= L.length) && (*p++ != e))  //查找L中第 1个与 e相等的数据元素的位序 
	{
		i++;
	}
	if(i<=L.length)
	{
		return i;  //返回定位的次序i 
	}
	else
	{
		return 0;  //未找到则返回0
	}
}

插入操作

void Increment(SqList &L)  //为顺序表扩充空间 
{
	newlist=new ElemType[L.listsize+LIST_INCREMENT];  //增加 LIST_INCREMENT个数据元素的空间 
	if(!newlist)
	{
		Error("存储分配失败!");
	}
	for(i=0;i<L.length;i++)
	{
		newlist[i]=L.elem[i];  //将原空间中的数据转移到 newlist中 
	}
	delete [] L.elem;  //释放原空间 
	L.elem=newlist;  //移交空间首地址 
	L.listsize+=LIST_INCREMENT;  //修改扩充后的最大空间 
}

void ListInsert_Sq(SqList &L,int i,ElemType e)  //在顺序表中的指定位置插入元素 
{
	if((i<1) || (i>L.length+1))  //i值不合法 
	{
		Error("插入元素的参数不合法!");
	}
	if(L.length>=L.listsize)  //判断存储空间是否已满,若满则需要扩充空间 
	{
		Increment(L);
	}
	q = &(L.elem[i-1]);  //用指针q指向插入位置 
	for(p=&(L.elem[L.length-1]);p>=q;--p)
	{
		*(p+1)=*p;  //依次将顺序表中的元素向后移 
	}
	*q=e;  //在 L的第 i的位置中插入e 
	++L.length;  //修正 L的长度,增加 1 
}

删除操作

void ListDelete_Sq(SqList &L,int i,ElemType &e)  //删除指定位置的元素 
{
	if((i<1) || (i>L.length))  //删除元素的参数不合法 
	{
		Error("删除元素的参数不合法!");
	}
	e=L.elem[i-1];  //将待删除元素的值赋给e 
	p=&(L.elem[i-1]);  //指向L中待删除元素的位置 
	q=L.elem+L.length-1;  //指向L中最后一个元素的位置 
	for(++p;p<=q;++p)
	{
		*(p-1)=*p;  //元素前移 
	}
	--L.length;  //修正 L的长度 ,减少 1 
}

输出操作

void TraverseList_Sq(SqList L)  //按顺序输出顺序表中的每一个元素 
{
	if(L.length!=0)
	{
		i=1;  //指示位序,初值为 1 
		p=L.elem;  //指向 L的第 i个元素,初始指向首元素 
		while(i<=L.length) 
		{
			cout<<*p++;  //依次输出L中的元素
			i++;
		}
	}
}

合并操作

void Merge_Sq(SqList A,SqList B,SqList C)  //两个顺序表合成一个新的顺序表 
{
	C.length=A.length+B.length;  //设置C的表长 
	C.listsize=C.length;  //设置C的存储容量 
	C.elem=new ElemType[C.length];  //设置C的动态分配存储空间 
	if(!C.elem)
	{
		Error("存储分配失败!");
	}
	i=0,j=0,k=0;
	while((i<A.length) && (j<B.length))  //合并A和B 
	{
		if(A.elem[i]<=B.elem[j])
		{
			C.elem[k]=A.elem[i];
			i++;
			k++;
		}
		else
		{
			C.elem[k]=B.elem[j];
			j++;
			k++;
		}
	}
	while(i<A.length)  //插入A的剩余段 
	{
		C.elem[k]=A.elem[i];
		i++;
		k++;
	}
	while(j<B.elem[j])  //插入B的剩余段 
	{
		C.elem[k]=B.elem[j];
		j++;
		k++;
	}
}

逆序操作

void Invert_Sq(SqList &L)  //逆置顺序表 
{
	n=L.length;
	m=n/2;
	for(i=0;i<m;i++)  //交换对应元素 
	{
		t=L.elem[i];
		L.elem[i]=L.elem[n-i-1];
		L.elem[n-i-1]=t;
	}
}

 

III. 链表及其操作实现

通过每个结点的指针将线性表中的数据元素按照其逻辑顺序链接在一起的存储方式称为线性表的链式存储结构,而采用这种存储结构的线性表称作链表。
 

i. 单链表

1.单链表

每个结点只有一个指针域的链表称为单链表。其结点结构分为data和next两个部分,data为数据域,存放数据元素的值;next为指针域,存放后继元素的地址。由于最后一个元素没有后继,所以其结点指针域中的指针为空指针NULL。

2.单链表的逻辑状态表示

由于在使用单链表时,不用关心每个结点在存储器中的实际物理位置,只需注重结点之间的逻辑顺序即可,所以我们可以把单链表画成用箭头相连接的结点序列。其中,Head表示头指针,指向链表中第一个结点的存储位置,而空指针用^表示。

我们通常还会在单链表第一个结点之前附加一个同结构的结点,这个结点叫头结点。头结点的数据域可以不存储信息,其指针域存储指向单链表第一个结点的存储地址。当头结点的指针域为空时,这个单链表就为空链表。而指向头结点的指针就是头指针L。图示如下:

【数据结构】学习笔记(一)—— 线性表、栈、队列_第2张图片
3.单链表的类型定义

typedef struct LNode  //单链表结构 
{
	ElemType data;        //数据域 
	struct LNode *next;   //指针域 
}
LNode;  //普通结点指针 
typedef LNode *LinkList;  //头指针 

4.单链表的操作实现

初始化操作

void InitList_L(LinkList &L)
{
	L=new LNode;
	L->next=NULL;
}

销毁操作

void DestroyList_L(LinkList &L)
{
	while(L)
	{
		p=L;
		L=L->next;
		delete p;
	}
}

清空操作

void ClearList_L(LinkList &L)
{
	p=L->next;
	while(p)
	{
		q=p;
		p=p->next;
		delete q;
	}
	L->next=NULL;
}

求表长操作

int ListLength_L(LinkList L)
{
	p=L;
	length=0;
	while(p->next)
	{
		length++;
		p=p->next;
	}
	return length;
}

取值操作

void GetElem_L(LinkList L,int i,ElemType &e)
{
	p=L->next;
	j=1;
	while(p&&(j<i))
	{
		p=p->next;
		++j;
	}
	e=p->data;
}

定位操作

LNode *LocateElem_L(LinkList L,ElemType e)
{
	p=L->next;
	while(p && (p->data!=e))
	{
		p=p->next;
	}
	return p;
}

插入操作

void ListInsert_L(LinkList &L,int i,ElemType e)
{
	p=L;
	j=0;
	while(p && (j<i-1))
	{
		p=p->next;
		++j;
	}
	s=new LNode;
	s->data=e;
	s->next=p->next;
	p->next=s;
}

删除操作

void ListDelete_L(LinkList &L,int i,ElemType &e)
{
	p=L;
	j=0;
	while((p->next) && (j<i-1))
	{
		p=p->next;
		++j;
	}
	q=p->next;
	e=q->data;
	p->next=q->next;
	delete q;
}

输出操作

void TraverseList_L(LinkList L)
{
	p=L->next;
	while(p)
	{
		cout<<p->data;
		p=p->next;
	}
}

逆序操作

void CreateList_L(LinkList &L,int n)
{
	InitList_L(L);
	for(i=n;i>0;--i)
	{
		p=new LNode;
		cin>>p->data;
		p->next=L->next;
		L->next=p;
	}
}

合并操作

void Merge_L(LinkList La,LinkList Lb,LinkList &Lc)
{
	pa=La->next;
	pb=Lb->next;
	Lc=pc=La;
	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;
	}
}

 

ii. 循环链表

如果将单链表的最后一个结点的指针域NULL改为指向头结点,那么这种结构就称为循环链表。图示如下:
【数据结构】学习笔记(一)—— 线性表、栈、队列_第3张图片
与单链表不同,循环链表从任意结点出发都可以访问到表中所有的结点,从而使某些运算在循环链表上更容易实现。

在实际运用中,我们多使用尾指针Rear来表示循环链表,图示如下:

【数据结构】学习笔记(一)—— 线性表、栈、队列_第4张图片

我们可以利用尾指针来实现链表的拼接操作:

void Connect_CL(LinkList &La,LinkList &Lb)
{
	p=La->next;  //p指向La的头结点 
	La->next=Lb->next->next;  //将Lb连接到 La之后 
	delete Lb->next;  //释放Lb的头结点 
	Lb->next=p;  //连接之后构成循环 
	La=Lb;  //重置尾指针 
}

 

iii. 双向链表

在单链表和循环链表中,求后继的时间复杂度为O(1),但求前驱的时间复杂度为O(n),这种单向性有着诸多的不便,为了解决这个问题我们可以使用双向链表,其求给定点的前驱结点和后继结点的时间复杂度均为O(1)。

1.双向链表:双向链表比较特殊,其有两个指针域,一个叫前向指针域prior,用来存放前驱的地址;另一个叫后向指针域next,用来存放后继的地址。在双向链表中,头结点的前向指针域存储指向最后一个结点的指针,后向指针域存储指向第一个结点的指针。当头结点的前向指针域和后向指针域均指向头结点自己的时候即为空双向链表。图示如下:

【数据结构】学习笔记(一)—— 线性表、栈、队列_第5张图片
2.双向链表的类型定义

typedef struct DuLNode  //双向链表结构 
{
	ElemType data;  //数据域 
	struct DuLNode *prior;  //前向指针域 
	struct DuLNode *next;   //后向指针域 
}
DuLNode;
typedef DuLNode *DuLinkList;

3.双向链表的操作实现

插入结点到双向链表中

void ListInsert_DuL(DuLinkList &L,int i,ElemType e)  //在第i个结点之前插入值为e的结点 
{
	p=GetElemP_DuL(L,i);  //确定双向链表L中第i个结点的指针P 
	s=new DuLNode;
	s->data=e;
	s->prior=p->prior;  //设新结点前向指针指向p结点前驱 
	s->next=p;  //设新结点后向指针指向p结点 
	p->prior->next=s;  //修改p结点前驱的后向指针 
	p->prior=s;  //修改p结点的前向指针 
}

DuLNode *GettElemP_DuL(DuLinkList L,int i)  //返回第i个结点指针 
{
	p=L->next;
	j=1;
	while((p!=L) && (j<i))  //在L中顺链向后扫描寻找第i个结点 
	{
		p=p->next;
		++j;
	}
	if( ((p==L) && (j!=i)) || (j>i))
	{
		return NULL;
	}
	return p;  //返回第i个结点指针 
}

删除双向链表中的结点

void ListDelete_DuL(DuLinkList &L,int i,ElemType &e)
{
	p=GetElemP_DuL(L,i);  //确定双向链表L中第i个结点的指针p 
	e=p->data;  //取出第i个结点数据域值 
	p->prior->next=p->next;  //修改第i-1个结点后向指针 
	p->next->prior=p->prior;  //修改第i+1个结点前向指针 
	delete p;  //释放第i个结点空间 
}

DuLNode *GetElemP_DuL(DuLinkList L,int i)
{
	p=L;
	i=0;
	while((p->next!=L) && (j<i-1))   //在L中顺链向后扫描寻找第i个结点
	{
		p=p->next;
		++j;
	}
	if((p->next==L)||(j>i-1))
	{
		return NULL;
	}
	return p->next;  //返回第i个结点指针 
}

 

iv. 静态链表

有的时候我们需要借助一维数组来描述线性表,数组的大小一般要大于线性表当前长度。数组中一个分量表示一个结点,在存储元素的同时,还需使用游标cur来代替单链表中的指针,存储指示其后继的相对地址,这种用数组表示的链表即为静态链表。

1.静态链表

静态链表的数据域data用于存放元素的数据,游标域cur存放其后继的相对地址。数组中的第0个分量可以看成头结点,其游标域指示静态链表的第一个结点。

2.静态链表的类型定义

#define List_Size 100  //静态链表大小 
typedef struct
{
	ElemType data;  //数据域 
	int cur;  //游标域 
}
component;
typedef component SlinkList[List_Sizet1];

3.静态链表的操作实现

查找指定值元素并返回位置

int LocateElem_SL(SlinkList S,ElemType e)  //查找指定值元素,并返回位置 
{
	i=S[0].cur;  //令i指示S的第一个结点 
	while(i&&(S[i].data!=e))  //在S中顺链向后查找 
	{
		i=S[i].cur;
	}
	return i;
}

 

IV. 线性表存储表示的比较

 
1.基于空间的比较

从分配方式来看,顺序表采用静态分配方式,链表采用动态分配方式。当预先确定了问题的规模时,用顺序表较好;当线性表的变化较大,难以事先估计其存储规模时,用链表更好。

从存储密度来看,顺序表的存储密度为1,链表的存储密度则小于1。为了节约空间,使用顺序表更好。因为链表中的结点除了数据域之外,还需要额外设置指针域,从存储密度来讲不是很经济。

2.基于时间的比较

由于顺序表是随机存取结构,其可以在O(1)时间内取得表中任何元素,所以当对线性表主要进行的是查找操作、删除插入较少时,优先使用顺序表。而链表是顺序存取结构,时间复杂度为O(n)。虽然链表的时间复杂度更高,但是当对线性表进行插入和删除操作时,链表只需要修改指针即可,而顺序表必须移动大量的元素,耗费时间更多,此时应该优先使用链表。
 

三. 栈

 

I. 栈的类型定义

 
1.栈的定义

栈是限定仅可以在表尾进行插入和删除的线性表,不含任何数据元素的栈则被称作空栈。栈也被称作LIFO表。

允许插入和删除的一端称为栈顶Top,栈顶可以随着栈中的数据元素的增减而浮动,其还可以通过栈顶指针指明当前数据元素的位置。

与栈顶相反,不允许插入和删除的一端称为栈底,栈底也有自己的栈底指针,只不过其不随着栈中的数据元素的增减而移动。

由于在任何时候出栈的数据元素都只能是栈顶元素,所以栈有一个特点:先进后出,后进先出。我们可以用向弹夹装填或取出子弹来具现化这个过程。

2.栈的抽象数据类型

ADT Stack
{
	Data:
		D={ai|ai∈ElemSet,i=1,2,...,n,n>=0} (具有相同类型的数据元素集合)
	Relation:
		R={<a(i-1),ai>|a(i-1),ai∈D,i=2,...,n} (约定an端为栈顶,a1端为栈底)
	Operation:
		InitStack(&S)
		    初始条件:无。
			操作结果:构造一个空栈S。 
	    DestroyStack(&S)
	        初始条件:栈S已经存在。 
			操作结果:销毁S。 
	    ClearStack(&S)
	        初始条件:栈S已经存在。
			操作结果:重置S为空栈。 
	    StackLength(&S)
	        初始条件:栈S已经存在。
			操作结果:返回S中的数据元素个数。 
	    GetTop(S,&e)
	        初始条件:栈S已经存在且非空。
			操作结果:用e返回S的栈顶数据元素。 
	    Push(&S,e)
	        初始条件:栈S已经存在。
			操作结果:插入数据元素e为S的新栈顶元素。 
	    Pop(&S,&e)
	        初始条件:栈S已经存在且非空。
			操作结果:删除S的栈顶数据元素,并用e返回其值。 
}
ADT Stack

 

II. 栈的存储表示

 
1.顺序栈

我们把自栈底到栈顶的元素按照逻辑顺序依次存放在一组地址连续的存储单元里的方式称为栈的顺序存储结构,采用这种存储结构的栈便是顺序栈。

2.top指针

top指针指示栈顶元素在顺序栈中的相对地址。通常以top=0表示空栈。但是由于C语言中数组下标从0开始,所以当用C语言描述空栈时应该用top = -1。

3.顺序栈的类型定义

同顺序表类似,顺序栈也用数组来描述。

#define STACK_INIT_SIZE 100  //栈存储空间的初始分配量 
#define STACK_INCREMENT 10   //栈存储空间的分配增补量 
typedef struct
{
	ElemType *elem;  //栈存储空间的基地址 
	int top;  //栈顶指针,栈非空时指向栈顶元素 
	int stacksize;  //当前分配的存储容量 
}
SqStack;

 

III. 栈的操作实现

初始化操作

void InitStack_Sq(SqStack &S)  //构造一个空顺序栈S 
{
	S.elem=new ElemType[STACK_INIT_SIZE];
	S.top=-1;
	S.stacksize=STACK_INIT_SIZE;
}

销毁操作

void DestroyStack_Sq(SqStack S)  //释放顺序栈S所占用的存储空间 
{
	delete [] S.elem;
	S.top=-1;
	S.stacksize=0;
}

清空操作

void ClearStack_Sq(SqStack &S)  //重置顺序栈S为空栈 
{
	S.top=-1;
}

求栈长操作

int StackLength_Sq(SqStack S)  //返回顺序栈的长度 
{
	return (S.top+1);
} 

取栈顶元素值的操作

void GetTop_Sq(SqStack S,ElemType &e)  //用e返回栈顶元素值 
{
	if(S.top==-1)
	{
		cout<<"栈为空栈!"<<endl;
	}
	e=S.elem[S.top];
}

入栈操作

void Push_Sq(SqStack &S,ElemType e)  //插入元素e,作为新的栈顶元素 
{
	if(S.top==(S.stacksize-1))  //若当前存储空间已满,则增加空间 
	{
		Increment(S);
	}
	S.elem[++S.top]=e;  //栈顶指针先加1,再将e压入栈顶 
}

void Increment(SqStack &S)  //为顺序栈S扩充STACK_INCREMENT个数据元素的空间 
{
	newstack=new ElemType[S.stacksize+STACK_INCREMENT];  //增加STACK_INCREMENT个存储分配 
	for(i=0;i<=S.top;i++)
	{
		newstack[i]=S.elem[i];  //将原空间的数据转移到newstack中 
	}
	delete [] S.elem;  //释放元素所占用的原空间S.elem 
	S.elem=newstack;   //移交空间首地址 
	S.stacksize += STACK_INCREMENT;  //扩充后的顺序栈的最大空间 
}

出栈操作

void Pop_Sq(SqStack &S,ElemType &e)  //若顺序栈S为空,给出相应信息并退出运行,否则用e返回栈顶元素值并修改栈顶指针 
{
	if(S.top==-1)  //空栈情况 
	{
		cout<<"Stack Empty!"<<endl;
	}
	e=S.elem[S.top--];
}

 

IV. 栈的应用

 
1.括号匹配检验

栈还常用来进行括号匹配的检验。左括号和右括号配对一定是先有左括号,后有右括号。又因为括号是可以连续嵌套使用的,所以左括号允许单个或连续出现,并等待右括号出现而配对消解。我们可以先将读到的左括号压入指定的栈中,当读到右括号时就和栈中的左括号配对消解,即将栈顶的左括号移出栈。代码描述如下:

int matching()  //检验表达式中所含括号是否正确嵌套(bool) 
{
	InitStack_Sq(S);             //构造空顺序栈S 
	Push_Sq(S,'#');              //将“# ”入栈表示括号串的开始 ,#做指示用,无意义 
	ch=getchar();                //读取表达式中的一个字符 
	state=1;                     //默认为1,正确;若为0,错误 
	while((ch='\n') && state)    //只要表达式未结束且检验未出错则继续检验 
	{
		if(ch=='(')
		{
			Push_Sq(S,ch);       //若遇到左括号则使其入栈 
		}
		if(ch==')')              //若遇到右括号 
		{
			GetTop_Sq(S,e);      //取栈顶元素 
			if(e=='#')           //若栈顶元素为#,则无左括号与右括号配对 ,此时配对错误 
			{
				state=0;
			}
			else                 //若有左括号配对(栈顶不为#)则与最近的右括号消解 
			{
				Pop_Sq(S,e);
			}
		}
		ch=getchar();
	}
	GetTop_Sq(S,e);
	if(e!='#')                  //若栈顶元素不为#,则左括号数多于右括号 
	{
		state=0;
	}
	if(state)                   //括号正确嵌套 
	{
		return 1;
	}
	else                        //括号不正确嵌套 
	{
		return 0;
	}
}

2.栈与递归调用

栈除了用于检验括号匹配,还常常用在递归当中。在递归算法中,当有多个函数构成嵌套调用的时候,按照后调用先返回的原则,函数之间的信息传递必须通过栈来实现。系统将整个程序运行时所需要的数据空间都安排在一个栈中,每当调用一个函数时,就为它在栈顶分配一个存储区;每当从一个函数退出时,就释放它的存储区,所以当前运行的函数的数据域必须在栈顶。

每进入一层递归,就会产生一个新的工作记录压入栈顶;每退出一层递归,就从栈顶弹出一个工作记录,从而保证当前执行层的工作记录必然是递归工作栈栈顶的工作记录。

 

四. 队列

 

I. 队列的类型定义

 
1.队列的定义

队列是只允许在表的一端进行插入操作,而在表的另一端进行删除操作的线性表。允许插入的一端称作队尾,队尾会随着队列中元素的增加而浮动,我们通常通过队尾指针指明队尾的位置。允许删除的一端称为队头,队头将随着队列中元素的减少而浮动,通过队头指针指明队头的位置。

队列简称FIFO表。与先进后出的栈不同,队列是先进先出的。插入元素的操作称为入队,删除元素的操作称为出队。不含任何元素的队列称为空队列。

2.队列的抽象数据类型

ADT Queue
{
	Data:
		D={ai|ai∈ElemSet,i=1,2,...,n,n>=0} (具有相同类型的数据元素集合)
	Relation:
		R={<a(i-1),ai>|a(i-1),ai∈D,i=2,...,n} (约定ai端为队列头,an端为队列尾)
	Operation:
		InitQueue(&Q)
		    初始条件:无。 
			操作结果:构造一个空队列Q。
		DestroyQueue(&Q)
		    初始条件:队列Q已经存在。
			操作结果:销毁Q。 
		ClearQueue(&Q)
		    初始条件:队列Q已经存在。 
			操作结果:重置Q为空队列。 
		QueueLength(Q) 
		    初始条件:队列Q已经存在。
			操作结果:返回Q的元素个数。 
		GetHead(Q,&e)
		    初始条件:队列Q已经存在且非空。 
			操作结果:用e返回Q的队头元素。 
		EnQueue(&Q,e)
		    初始条件:队列Q已经存在。
			操作结果:插入元素e为Q的新队尾元素。 
		DeQueue(&Q,&e)
		    初始条件:队列Q已经存在且非空。
			操作结果:删除Q的队头元素,并用e返回其值。 
}
ADT Queue

 

II. 队列的存储表示

 

i. 循环队列

 
1.头指针和尾指针

在队列的顺序存储结构中,除了用一组地址连续的存储单元依次存放队列头到队列尾的数据元素之外,还需要设立front指针和rear指针分别指示队列头元素和队列尾元素的位置。

在初始化建立空队列时,可以令front=rear=0,每当插入新的队列尾元素时,尾指针便+1,;每当删除队列头元素时,头指针便+1。

所以,在非空队列中,队列头指针front始终指向队列头元素,而队尾指针rear始终指向队列尾元素的下一个位置

2.循环队列的定义

为了防止数组越界、低端存在大量空闲空间的假溢出情况出现。我们可以将存储队列的数组看成是头尾相接的循环结构,即允许队列直接从数组下标最大的位置延续到下标最小的位置,从而在逻辑上实现循环。这种头尾相接的顺序存储结构就被称为循环队列。

3.相关数学表达式

设存储循环队列的数组长度为QUEUE_MAX_SIZE。

队列的长度表达式为

QueueLength=(rear-front+Queue_Size)%QUEUE_MAX_SIZE

在循环队列的队尾插入元素后,队尾指针的修改的表达式为

rear=(rear+1)%QUEUE_MAX_SIZE

在循环队列的队头删除元素后,队头指针的修改的表达式为

front=(front+1)%QUEUE_MAX_SIZE

队列满的判定条件的表达式为

(rear+1)%QUEUE_MAX_SIZE=front

队列空的判定条件的表达式为

front=rear

4.循环队列的类型定义

#define QUEUE_MAX_SIZE 100  //循环队列的大小 

typedef struct
{
	ElemType *elem;  //存储空间基地址 
	int front;  //头指针,队列非空指向队头元素 
	int rear;   //尾指针,队列非空指向队尾元素下一位置 
}
SqQueue;

 

ii. 链队列

 
1.链队列的定义

用链表表示的队列称为链队列。链队列也有头尾两个指针。和线性表的单链表类似,链队列也有头结点,头结点的指针域中的指针指向队列的第一个结点,尾指针指向队列的最后一个结点。

2.链队列的类型定义

typedef struct QNode
{
	ElemType data;  //数据域 
	struct QNode *next;  //指针域 
}

QNode,*QueuePtr;

typedef struct
{
	QueuePtr front;  //头指针,指向链队列的头结点 
	QueuePtr rear;   //尾指针,指向连队列的最后一个结点 
}

LinkQueue;

 

III. 队列的操作实现

 

i. 循环队列的操作实现

 
初始化操作

void InitQueue_Sq(SqQueue &Q)  //构造一个循环空队列Q 
{
	Q.elem=new ElemType[QUEUE_MAX_SIZE];
	if(!Q.elem)
	{
		cout<<"Overflow"<<endl;  //存储分配失败 
	}
	Q.front=Q.rear=0;
}

销毁操作

void DestroyQueue_Sq(SqQueue &Q)  //释放循环队列Q所占用的存储空间 
{
	delete [] Q.elem;
	Q.front=Q.rear=0;
}

清空操作

void ClearQueue_Sq(SqStack &Q)  //重置循环队列Q为空队列 
{
	Q.front=Q.rear=0;
}

求队列长操作

int QueueLength_Sq(SqQueue Q)  //返回循环队列Q的数据元素个数 
{
	length=(Q.rear-Q.front+QUEUE_MAX_SIZE)%QUEUE_MAX_SIZE;
	return length;
}

取队头元素值操作

void GetHead_Sq(SqQueue Q,ElemType &e)  //若循环队列为空,则给出相应信息并退出运行,否则用返回队头元素值 
{
	if(Q.front==Q.rear)
	{
		cout<<"Queue Empty"<<endl;
	}
	e=Q.elem[Q.front];
}

入队操作

void EnQueue_Sq(SqQueue &Q,ElemType e)  //若循环队列Q已满,则给出相应信息退出运行,否则将元素e插入队尾,并修改队尾指针 
{
	if(((Q.rear+1)%QUEUE_MAX_SIZE)==Q.front)
	{
		cout<<"Queue Overflow"<<endl;
	}
	Q.elem[Q.rear]=e;
	Q.rear=(Q.rear+1)%Queue_Size;
}

出队操作

void DeQueue_Sq(SqQueue &Q,ElemType &e)  //若循环队列Q为空,则给出相应信息退出运行,否则用e返回队头元素,并修改队头指针 
{
	if(Q.front==Q.rear)
	{
		cout<<"Queue Empty"<<endl;
	}
	e=Q.elem[Q.front];
	Q.front=(Q.front+1)%QUEUE_MAX_SIZE;
}

ii. 链队列的操作实现

 
初始化操作

void InitQueue_L(LinkQueue &Q)  //构造一个空链队列Q 
{
	Q.front=Q.rear=new QNode;
	Q.front->next=NULL;
}

销毁操作

void DestroyQueue_L(LinkQueue &Q)  //释放链队列Q所占用的存储空间 
{
	while(Q.front)
	{
		Q.rear=Q.front->next;
		delete Q.front;
		Q.front=Q.rear;
	}
}

清空操作

void ClearQueue_L(LinkQueue &Q)  //清空链队列Q,释放所有结点空间 
{
	p=Q.front->next;
	while(p)
	{
		q=p;
		p=p->next;
		delete q;
	}
	Q.front->next=NULL;
	Q.rear=Q.front;
}

求队列长操作

int QueueLength_L(LinkQueue Q)  //返回链队列Q的长度 
{
	p=Q.front;  //设置指针,初始指向链队列头结点 
	length=0;  //设置计数器length初始值为0 
	while(p->next)
	{
		length++;
		p=p->next;
	}
	return length;
}

取队列头元素值操作

void GetHead_L(LinkQueue Q,ElemType &e)  //若链队列Q为空,则给出相应信息并退出运行,否则用e返回队列头结点数据域的值 
{
	if(Q.front==Q.rear)
	{
		cout<<"Queue Empty"<<endl;
	}
	e=Q.front->next->data;
}

入队操作

void EnQueue_L(LinkQueue &Q,ElemType e)  //插入一个数据域值为e的结点到链队列Q中,成为新的队尾结点 
{
	p=new QNode;
	p->data=e;  //生成一个数据域值为e的新结点 
	p->next=NULL;
	Q.rear->next=p;  //将新结点插到队尾 
	Q.rear=p;  //修改队尾指针,令其指向新结点 
}

出队操作

void DeQueue_L(LinkQueue &Q,ElemType &e)  //若链队列Q为空,则给出相应信息并退出运行,否则用e返回队头结点数据域的值 
{
	if(Q.front==Q.rear)
	{
		cout<<"Queue Empty"<<endl;
	}
	p=Q.front->next;  //用指针p指向链队列Q的队头结点 
	e=p->data;
	Q.front->next=p->next;  //修改队头指针 
	if(Q.rear==p)
	{
		Q.rear=Q.front;  //若队列的长度等于1,则修改队尾指针 
	}
	delete p;  //释放队头结点所占的存储空间 
}

————————————————————————————————————————————————————

后文链接:

【数据结构】学习笔记(二)—— 串、数组、广义表

你可能感兴趣的:(笔记)