数据结构--算法

数据结构

是相互之间存在一种或多种特定关系的数据元素的集合。
数据结构是一门研究非数值计算的程序设计问题中的操作对象,以及它们之间的关系和操作等相关问题的学科。
术语
数据:是描述客观事物的符号,是计算机中可以操作的对象,是能被计算机识别,并输入给计算机处理的符号集合。数据不仅仅包括整型、实型等数值类型,还包括字符及声音、图像、视频等非数值类型。
这里的数据即符号,这些符号必须具备两个前提:(1)可以输入到计算机中。(2)能被计算机程序处理。
声音、图像、视频等其实是可以通过编码的手段编程字符数据来处理。
数据元素:是组成数据的、有一定意义的基本单位,在计算机中通常作为整体处理,也被称为记录。
数据项:一个数据元素可以由若干个数据项组成。
数据项是数据不可分割的最小单位。数据项数据最小单位,但真正讨论问题时,数据元素才是数据结构中建立数据模型的着眼点。
数据对象:是性质相同的数据元素的集合,是数据的子集
数据结构:是相互之间存在一种或多种特定关系的数据元素的集合。
不同数据元素之间不是独立的,而是存在一种或多种特定关系的数据元素的集合。

逻辑结构与物理结构

逻辑结构
逻辑结构是指数据对象数据元素之间的相互关系。其实这也是我们今后最需要关注的问题。
逻辑结构分四种:
1、集合结构
集合结构中的数据元素除了同属于一个集合外,它们之间没有其他关系。各个数据元素是“平等”的。类似数学中的集合。
在这里插入图片描述
2、线性结构
线性结构中的数据元素之间是一对一的关系。
在这里插入图片描述
3、树形结构
树形结构中的数据元素之间存在一种一对多的层次关系。
在这里插入图片描述
4、图形结构
图形结构的数据元素是多对多的关系。
在这里插入图片描述
注意两点:
1)将每一数据元素看做一个结点,用圆圈表示。
2)元素之间的逻辑关系用结点之间的连线表示,如果这个关系是有方向的,那么用带箭头的连线表示。
物理结构
物理结构是指数据的逻辑结构在计算机中的存储形式。
数据元素的存储结构形式有两种:顺序存储和链式存储。
1、顺序存储结构
顺序存储结构是把数据元素存放在地址连续的存储单元里,其数据间的逻辑关系和物理关系是一致的。
在这里插入图片描述
数组就是这样的顺序存储结构。
2、链式存储结构
链式存储结构是把数据元素存放在任意的存储单元里,这组存储单元可以是连续的,也可以是不连续的。数据元素的存储关系不能反映其逻辑关系。
在这里插入图片描述
链式存储就灵活多了,数据存在哪里不重要,只要有一个指针存放了相应的地址就能找到它了。
逻辑结构是面向问题的,而物理结构就是面向计算机的。
数据类型
数据类型是指一组性质相同的值得集合及定义在此集合上的一些操作的总称。
类型就是用来说明变量或表达式的取值范围和所能进行的操作。
抽象数据类型是指一个数学模型及定义在该模型上的一组操作,抽象数据类型的定义仅取决于它的一组逻辑特性,而与其在计算机内部如何表示和实现无关。
总结在这里插入图片描述
在这里插入图片描述

算法

算法是解决特定问题求解步骤的描述,在计算机中表现为指令的有限序列,并且 每条指令表示一个或多个操作。
算法5个基本特性:输入、输出、有穷性、确定性和可行性。

输入输出
算法具有零个或多个输入。
算法至少有一个或多个输出。
有穷性
有穷性指算法在执行有限的步骤之后,自动结束而不会出现无限循环,并且每一个步骤在可接受的时间内完成。
确定性
确定性:算法的每一步都具有确定的含义,不会出现二义性。算法每个步骤被精确定义而无歧义。
可行性
可行性:算法的每一步都必须是可行的,也就是说,每一步都能够通过执行有限次数完成。

算法设计要求
正确性
算法的正确性是指算法至少应该具有输入、输出和加工处理无歧义性、能正确反映问题的需求、能够得到问题的正确答案。
算法的“正确”大体分为四个层次:
1)算法程序没有语法错误
2)算法程序对于合法的输入数据能够产生满足要求的输出结果
3)算法程序对于非法的输入数据能够得出满足规格说明的结果
4)算法程序对于精心选择的,甚至刁难的测试数据都有满足要求的输出结果。
可读性
算法设计的另一目的是为了便于阅读、理解和交流。
写代码的目的,一方面是为了让计算机执行,但还有一个重要的目的是为了便于他人阅读,让人理解和交流。
健壮性
当输入数据不合法时,算法也能做出相关处理,而不是产生异常或莫名其妙的结果。
时间效率高和存储量低
好的算法还应该具备时间效率高和存储量低的特点。

算法效率的度量方法
1、事后统计法(不采用此方法)
这种方法主要是通过设计好的测试程序和数据,利用计算机计时器对不同算法编制的程序的运行时间进行比较,从而确定算法效率的高低。
2、事前分析估算方法
在计算机程序编制前,依据统计方法对算法进行估算。
一个用高级程序语言编写的程序在计算机上运行时所消耗的时间取决于下列因素:
1、算法采用的策略、方法 (算法)
2、编译产生的代码质量 (软件支持)
3、问题的输入规模
4、机器执行指令的速度 (硬件性能)

在分析程序运行时间时,最重要的是把程序看成是独立于程序设计语言的算法或者一系列步骤。

函数的渐近增长
给定两个函数f(n) 和g(n),如果存在一个整数N,使得对于所有的n>N,f(n)总比g(n)大,那么,我们说f(n)的增长渐近快于g(n).

判断一个算法的效率时,函数中的常数和其他次要项常常可以忽略,而更应该关注主项(最高阶项)的阶数。

算法时间复杂度
在进行算法分析时,语句总的执行次数T(n)是关于问题规模n的函数,进而分析T(n)随n的变化情况并确定T(n)的数量级。算法的时间复杂度,也就是算法的时间度量,记作:T(n)=O(f(n))。它表示随问题规模n的增大,算法执行时间的增长率和f(n)的增长率相同,称作算法的渐近时间复杂度,简称为时间复杂度,其中f(n)是问题规模n的某个函数。
非官方名称:
O(1)叫常数阶、O(n)叫线性阶、O(n**2)叫平方阶。

推导大O阶:
1、用常数1取代运行时间中的所有加法常数。
2、在修改后的运行次数函数中,只保留最高阶项。
3、如果最高阶项存在且不是1.则去除与这个项相乘的常数。
得到的结果就是最大O项。

常数阶

sum=1+n)*n/2 

算法中无论有多少句,都记作O(1)
对于分支结构,无论真假执行的次数都是恒定的。
线性阶
线性阶的循环结构会复杂很多,关键就是要分析循环结构的运行情况。
该代码它的循环的时间复杂度为O(n),因为循环体中的代码要执行n次。

int i;
for(i=0;i<n;i++)
{
	/*时间复杂度为O(1)的程序步骤序列*/
}

对数阶

int count=1;
while(count<n)
{
	count=count*2;
	/*时间复杂度为O(1)的程序步骤序列*/
}

由于每次count乘以2之后,就距离n更近了一分,也就是有多少个2相乘后大于n,则会退出循环,由 2**x=n 得到x=log2 n,所以这个循环的时间复杂度为O(logn)

平方阶
循环嵌套,内循环已分析过,时间复杂度为O(n)

int i.j;
for(int=0;i<n;i++)
{
	for(j=0;j<n;j++){
	 /*时间复杂度为O(1)的程序步骤序列*/
	 }
}

对于外层循环,不过是内部这个时间复杂度为O(n)的语句,在循环n次,所以这段代码的时间复杂度为O(n**2),如果外层循环次数改为m,时间复杂度就变为O(m*n)

例:

int i,j;
for(i=0;i<n;i++)
{
    for(j=i;j<n;j++)
    {
	/*时间复杂度为O(1)的程序步骤序列*/
	}
}

由于当i=0时,内循环执行了n次,当i=1时,执行了n-1次,…当i=n-1时,执行了1次,所以总的执行次数为:
n+(n-1)+(n-2)+…+1=n(n+1)/2=n2/2+n/2
时间复杂度为O(n
2)

对于方法调用的时间复杂度分析:

int i,j;
for (i=0;i<n;i++)
{
	function(i);
}
void function(int count){
 print(count);
}

function函数的时间复杂度是O(1)。所以整体复杂度为O(n)。
假如function 是下面:

void function(int count)
{
	int j;
	for (j=count;j<n;j++)
	{
	/*时间复杂度为O(1)的程序步骤序列*/
	}
}

最终的时间复杂度为O(n**2)

在这里插入图片描述

线性表

定义:线性表(List)零个或多个数据元素的有限序列。
1、首先它是一个序列,元素之间是有顺序的,
2、若元素存在多个,则第一元素无前驱,最后一个元素无后继,其他每个元素都有且只有一个前驱和后继。
3、线性表强调的是有限,元素个数也是有限的。
在这里插入图片描述
所以线性表元素的个数n(n>0)定义为线性表的长度,当n=0时,称为空表。在非空表中,每个数据元素都有一个确定的位置。

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

ADT线性表(List)
Data
		线性表的数据对象集合为{a1,a2,.....,an},每个元素的类型均为DataType。其中,除第一个元素a1外,每个元素有且只有一个直接前驱元素,除了最后一个元素an外,每一个元素有且只有一个直接后继元素。数据元素之间的关系是一对一的关系。
Operation
		InitList(*L):         初始化操作,建立一个空的线性表L。
		ListEmpty(L):    	  若线性表为空,返回true。否则返回false。
		ClearList(*L):       将线性表清空
		GetElem(L,i,*e):      将线性表L中第i个位置元素返回给e。
		LocateElem(L,e):      在线性表L中查找与给定值e相等的元素,如果查找成功,返回该元素在表中序号表示成功;否则,返回0表示失败。
		ListInsert(*L,i,e): 在线性表L中的第i个位置插入新元素e.
		ListDelete(*L,i,*e):  删除线性表L中第i个位置元素,并用e返回其值。
		ListLength(L):       返回线性表L的元素个数。
endADT

例1实现两个线性表集合A和B的并集操作。就是要把存在集合B中但并不存在A中的数据元素插入到A中即可。
假设La表示集合A,Lb表示集合B,则实现的代码如下:

/*将所有的在线性表Lb中但不在La 中的数据元素插入到La中/*
void union(List *La,List Lb)
{
	int La_len,Lb_len,i;
	ElemType e;            /*声明与La和Lb相同的数据元素e*/
	La_len=ListLength(La); /*求线性表的长度*/
	Lb-Len=ListLength(Lb);
	for(i=1;i<=Lb_len;i++)
	{
    	GetElem(Lb,i,e);   /*取Lb中第i个数据元素赋给e*/
    	if(!LocateElem(La,e,equal)) /*La中不存在和e相同数据元素*/
    		ListInsert(La,++La_len,e); /*插入*/	
    }
}

对于union操作,用到了前面线性表基本操作ListLength、GetElem、LocateElem、ListInsert等。对于复杂的个性化操作,其实就是把基本操作组合起来实现。

线性表的顺序存储结构

线性表的顺序存储结构,指的是用一段地址连续的存储单元依次存储线性表的数据元素
在这里插入图片描述
在内存中找了块地,通过占位的形式,把一定内存空间给占了,然后把相同数据类型的数据元素依次存放在这块空地中。
一维数组来实现顺序存储结构。即把第一个数据元素存到数组下标为0的位置中,接着把线性表相邻的元素存储在数组中相邻的位置。
线性表的顺序存储的结构代码:

#define MAXSIZE 20     /*存储空间初始分配量*/
typedef int ElemType;  /*ElemType 类型根据实际情况而定,这里假设为int */
typedef struct
{
	ElemType data [MAXSIZE]; /*数组存储数据元素,最大值为MAXSIZE*/
	int length;  /*线性表当前长度*/
	
}SqlList;

顺序存储的三个属性:
1、存储空间的起始位置:数组data。它的存储位置就是存储空间的存储位置。
2、线性表的最大存储容量:数组长度MaxSize。
3、线性表的当前长度:length。

数组的长度和线性表的长度需要区分一下。
数组的长度是存放线性表的存储空间的长度,存储分配后这个量一般是不变的。(c\vb\c++都可以用编程手段实现动态分配数组,不过会带来性能上的损耗)
线性表的长度是线性表中数据元素的个数,随着线性表的插入和删除操作的进行,这个量是变化的。
在任意时刻,线性表的长度应该小于等于数组的长度。
存储器中的每一存储单元都有自己的编号,这个编号称为地址。

顺序存储结构的插入与删除
获取元素操作,即GetElem操作

#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
typedef int Status;

/*Status 是函数的类型,其值是函数结果状态代码,如OK等*/
/*初始条件:顺序线性表L已存在,1<=i<=ListLength(L)*/
/*操作结果:用e返回L中第i个数据元素的值*/
Status GetElem(SqList L,int i,ElemType *e)
{
	if(L.length==0||i<1||i>L.length)
			return ERROR;
	*e=L.data[i-1];
	return OK;		
}

插入操作

在这里插入图片描述
实现ListInsert(*L,i,e),即在线性表L中的第i个位置插入新元素e.
插入算法的思路:
1、如果插入位置不合理,抛出异常;
2、如果线性表长度大于等于数组长度,则抛出异常或动态增加容量;
3、从最后一个元素开始向前遍历到第i个位置,分别将它们都向后移动一个位置;
4、将要插入元素填入位置i处;
5、表长加1.

实现代码如下:

/*初始化条件:顺序线性表L已存在,1<=i<=ListLength(L) */
/*操作结果:在L中第i个位置之前插入新的数据元素e,L的长度加1 */

Status ListInsert(SqList *L,int i ,ElemType e)
{
	int k;
	if(L->length==MAXSIZE)  /*顺序线性表已满*/
			return ERROR;
	if(i<1 ||  i>L->length+1)   /*当i不在范围内时*/
			return ERROR;
	if(i<=L->length)   /*若插入数据未在不在表尾*/
	{
	       for(k=L->length-1;k>=i-1;k--)  /*将要插入位置后数据元素向后移动一位*/
					L->data[k+1]=L->data[k];
					
    }
	L->data[i-1]=e;
	L->length++;
	return OK;				
}

删除操作

在这里插入图片描述
删除算法的思路:
1、如果删除位置不合理,抛出异常;
2、取出删除元素;
3、从删除元素位置开始遍历到最后一个元素位置,分别将他们都向前移动一个位置;
4、表长减一;
实现代码如下:

/*初始化条件:顺序线性表L已存在,1<=i<=ListLength(L)*/
/*操作结果:删除L的第i个数据元素,并用e返回其值,L的长度减1*/
Status ListDelete(SqList *L,int i, ElemType *e)

{
	int k;
	if (L->length==0)     /*线性表为空*/
			return ERROR;
     if(i<|| i>L->length)    /*删除位置不正确*/
     	return ERROR;
     	*e=L->data[i-1];
     	if (i<L->length)
     	{
				for (k=i;k<->length;k++)  /*如果删除不是最后位置*/
						L->data[k-1]=L->data[k];
			}
     		L->length--;
     		return OK;
}

线性表的顺序结构的优缺点:
优点
1、无须为表示表中元素之间的逻辑关系而增加额外的存储空间
2、可以快速地存取表中任一位置的元素
缺点
1、插入和删除操作需要移动大量元素。
2、当线性表长度变化较大时,难以确定存储空间的容量
3、造成存储空间的“碎片”

**

线性表的链式存储结构

**

顺序存储结构最大的缺点就是插入和删除时需要移动大量元素,,这显然就是需要耗费时间。
线性表的链式存储结构的特点是用一组任意的存储单元存储线性表的数据元素,这组存储单元可以是连续的,也可以是不连续的。这就意味着,这些数据元素可以存在内存未被占用的任意位置。
在这里插入图片描述
现在链式结构 ,除了要存数据元素信息外 还要存储它的后继元素的存储地址。。我们把存储数据 素信 的域称为数据域 把存储直接后继位置的域称为指针域 。指针域中存储的信息称做指针或链 。这两部分信息组成数据元素ai 的存储映像,称为 结点(Node)。
n个结点(ai的存储映像)链结成一个链表,即为线性表(a1,a2,…,an)的链式存储结构。因为此链表的每个结点中只包含一个指针域,所以叫做单链表。
在这里插入图片描述
链表中第一个结点的存储位置叫做头指针。之后的每一个结点其实就是上个后继指针指向的位置。最后一个结点,直接后继不存在,因此结点指针为“空”(通常NULL或“^”符号表示)。

在这里插入图片描述
有时,为了更加方便地对链表进行操作,会在单链表的第一个结点前附设一个结点,称为头结点。
在这里插入图片描述
头指针与头结点的异同
头指针:
1、头指针是指链表指向第一个结点的指针,若链表有头结点,则是指向头结点的指针。
2、头指针具有标识作用,所以常用头指针冠以链表的名字
3、无论链表是否为空,头指针均不为空。头指针是链表的必要元素。
头结点:
1、头结点是为了操作的统一和方便而设立的,放在第一个元素的结点之前,其数据域一般无意义(也可存放链表的长度)
2、有了头结点,对在第一元素结点前插入结点和删除第一结点,其操作与其它结点的操作就统一了。
3、头结点不一定是链表必须要素。

单链表:
在这里插入图片描述
带有头结点的单链表:
在这里插入图片描述
空链表:
在这里插入图片描述
单链表中,我们在C语言中可用结构指针来描述。

/*线性表的单链表存储结构*/
typedef struct Node 
{
	ElemTypè data: 
	struct Node *next ; 
} Node; 
typedef struct Node *LinkList; /*定义 LinkList*/

结点由存放数据元素的数据域、存放后继结点地址的指针域组成。
假设p是指向钱性表第i 个元素的指针,则该结点 ai 的数据域
我们可以用 p->data 来表示, p->data的值是一个数据元素,结点 ai 的指针域可以用
p->next 来表示, p->next 的值是一个指针。 p->next 指向第 i+l元素,即指向 ai+1 的指针。也就是说 ,如果 p->data=ai,那么 p->next->data=ai+l
在这里插入图片描述
单链表的读取

获得链表第i个数据的算法思路:
1、声明一个结点p指向链表的第一个结点,初始化j从1开始;
2、当j<i时,就遍历链表,让p的指针向后移动,不断指向下一个结点,j累加1;
3、若到链表末尾p为空,则说明第i个元素不存在;
4、否则查找成功,返回结点p的数据。
实现代码算法如下:

/*初始化条件:顺序线性表L已存在,1<=i<=ListLength(L)*/
/*操作结果:用e返回L中第i个数据元素的值*/
Status GetElem(LinkList L,int i,ElemType *e)
{
	int j;
	LinkList p;  /*声明一结点p*/
	p=L->next;  /*让p指向链表L的第一个结点*/
	j=1; /*j为计算器*/
	while(p&&j<i)  /*p不为空或者计算器j还没有等于i时,循环继续*/
	{
			p=p->next;   /*让p指向下一个结点*/
			++j;
	  }
	  if(!p  ||   j>i)
	  		return ERROR;  /*第i个元素不存在*/
	  *e=p->data;   /*取第i个元素的数据*/
	  return OK;
}

说白了就是从头开始找,直到第i个元素为止。这个算法的时间复杂度取决于i的位置,最坏的情况的时间复杂度是O(n).

单链表的优势在于插入和删除

单链表的插入:
先来看单链表的插入, 假设存储元素 e的结点为 s,要实现结点p、 p->next和s
之间逻辑关系的变化,只需将结点 s 插入到结点 p和p->next 之间即可。

在这里插入图片描述
插入,只需让s->next 和 p->next的指针做一点改变即可。

s->next=p->next;
p->next=s;

代码解读为让p的后继结点改为s 的后继结点,再把结点s变成p的后继结点。顺序不可调换,s->next=p->next,其实就等于s->next=s

在这里插入图片描述
插入结点S后:
在这里插入图片描述
对于单链表的表表头和表尾的特殊情况,操作是相同的。
在这里插入图片描述
单链表第i个数据插入结点的算法思路:
1、声明一结点p指向链表第一个结点,初始化j从1开始;
2、当 j<i 时,就遍历链表,让p 的指针向后移动,不断指向下一结点,j 累加1;
3、若到链表末尾 p 为空,则说明第 i 个元素不存在;
4、否则查找成功,在系统中生成一个空结点 s;
5、将数据元素 e 赋值给 s->data;
6、单链表的插入标准语句 s->next=p->next; p->next=s;
7、返回成功。

实现算法代码如下:

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

Status  ListInsert(LinkList *L,int i,ElemType e)
{
	int j;
	LinkList p,s;
	p=*L;
	j=1;
	while (p  && j <i)   /*寻找第i 个结点*/
	{
			p=p->next;
			++j;
	 }
	if (!p || j>i)
		return ERROR;       /*第i个元素不存在*/
	s=(LinkList)malloc(sizeof(Node));  /*生成新结点(c 标准函数)*/ 
	s->data=e;
	s->next=p->next;
	p->next=s;
	return OK;
}

注:代码中用到c语言的malloc 标准函数,它的作用就是生成一个新的结点,其类型与Node是一样的,其实质就是在内存中找到了一小块空地,准备用来存放e 数据s 结点。

单链表的删除

设存储元素ai 结点为q ,要实现将结点 q 删除单链衰的操作 ,其实就是将它的前继结点的指针绕过,指向它的后继结点即可.

在这里插入图片描述
p->next=p->next->next,用q来取代p->next.即:

q=p->next;
p->next=q->next;

让p 的后继的后继结点改成p 的后继结点。

单链表第i 个数据删除结点的算法思路:
1、声明一结点p指向链表第一个结点,初始化j 从1开始;
2、当 j < i 时,就遍历链表,让p 的指针向后移动,不断指向下一个结点, j 累加1;
3、若到链表末尾p 为空,则说明第 i 个元素不存在;
4、否则查找成功,将欲删除的结点 p ->next 赋值给 q;
5、单链表的删除标准语句 p->next=q->next;
6、将q 结点中的数据赋值给 e, 作为返回;
7、释放 q 结点;
8、返回成功。

实现算法代码:

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

Status ListDelete(LinkList *L,int i,ElemType *e)
{
     int j;
     LinkList p,q;
     p=*L;
     j=1;
     while(p->next && j <i)    /*遍历寻找第i 个元素*/
	 {
	 	     p=p->next;
	 	     ++j;
	 	     
	  }
	  if (!(p->next)  ||  j>i  )
	           return ERROR;   /*第i个元素不存在*/
	  q= p->next;
	  p->next=q->next;         /*将q的后继赋值给p的后继*/
	  *e =q->data;                /*将q结点中的数据给e*/
	  free(q);                         /*让系统回收此结点,释放内存*/
	  return  OK;        
}

注:使用c语言的标准函数free,它的作用就是让系统回收一个Node结点,释放内存。

单链表的整表创建与删除

单链表整表创建的算法思路:

1、声明一结点p 和计算器变量 i;
2、初始化一空链表L;
3、让L的头结点的指针指向NULL,即建立一个带头结点的单链表;
4、循环:
1)生成一新结点赋值给p;
2)随机生成一数字赋值给p 的数据域 p->data;
3)将p 插入到头结点与前一新结点之间。

实现算法代码:

/*随机产生n个元素的值,建立带表头结点的单链线性表L(头插法)*/
void CreateListHead(LinkList *L, int n)
{
	LinkList p;
	int i;
	srand(time (0));                                 /*初始化随机数种子*/
	*L=(LinkList)malloc(sizeof(Node));
	(*L)->next=NULL;                              /*先建立一个带头结点的单链表*/
	for (i=0; i<n ; i++)
	{
		p=(LinkList)malloc (sizeof(Node));  /*生成新结点*/
		p->data=rand() %100+1;                /*随机生成100以内的数字*/
		p->next=(*L)->next;
		(*L)->next=p;                                  /*插入到表头*/
				
		}
}

在这里插入图片描述

实现算法代码:
/*随机产生n个元素的值,建立带表头结点的单链线性表L(尾插法)*/
void CreateListTail(LinkList *L,int n)
{
		LinkList p,r;
		int i;
		srand(time (0) );
		*L=(LinkList)malloc(sizeof(Node));  /*为整个线性表*/
		r=*L;											/*r为指向尾部的结点*/
		for (i=0;  i<n;  i++)
		{
					p=(Node *)malloc (sizeof (Node));  /*生成新结点*/
					p->data=rand()%100+1;      /*随机生成100以内的数字*/
					r=next=p;                             /*将表尾终端结点的指针指向新结点*/
					r=p;                                     /*将当前的新结点定义为表尾终端结点*/
			}
			r->next=NULL;                            /*表示当前链表结束*/
}

r->next=p; 的意思,其实就是将刚才的表尾终端结点r 的指针指向新结点p.

在这里插入图片描述
r=p; 的意思。本来r是在ai-1元素的结点,可现在它已经不是最后的结点了,现在最后的结点是ai,所以应该要让将p结点这个最后的结点赋值给r.此时r 又是最终的尾结点了。
在这里插入图片描述
循环结束后,应该让这个链表的指针域置空,因此有了“r->next=NULL;’ 以便以后遍历时可以确认其是尾部。

单链表整表删除的算法思路:
1、声明一结点 p和 q;
2、将第一个结点赋值给p;
3、循环:
1)将下一结点赋值给q;
2)释放p;
3)将q 赋值给p.
实现算法代码:

/*初始化条件:顺序线性表L已存在,操作结果:将L重置为空表*/
Status ClearList(LinkList *L)
{
		LinkList p,q;
		p=(*L)->next;     /*p指向第一个结点*/
		while(p)            /*没到表尾*/
		{
				q=p->next;
				free(p);
				p=q;
			}
			(*L)->next=NULL;    /*头结点指针域为空*/
			return OK;
}		

p是一个结点,它除了有数据域,还有指针域。

若线性表需要频繁查找,很少进行插入和删除操作时,宜采用顺序存储结
。若需要频繁插入和删除时,宜采用单链表结构

比如说游戏开发中,对于用户注册的个人信息,除了注册时插入数据外,绝大多数情况都是读取,所以应该考虑用顺序存储结构 。而游戏中的玩家的武器或者装备列表,随着玩家的游戏过程中,可能会随时增加或删除,此时再用顺序存储就不大合适了,单链表结构就可以大展拳脚。当然,这只是简单的类比,现实中的软件开发,要考虑的问题会复杂得多。

当线性表中的元素个数变化较大或者根本不知道有多大时,最好用单链表
结构, 这样可以不需要考虑存储空间的大小问题。而如果事先知道线性表
的大致长度,比如一年 12 个月,一周就是星期一至星期日共七天,这种用
顺序存储结构效率会高很多。

静态链表

让数组的元素都是由两个数据域组成, data和 cur 。也就是说,数组的每
个下标都对应一个 data 和一个 cur 数据域data ,用来存放数据元素, 就是通常我
们要处理的数据;而游标 cur 相当于单链表中的 next 指针,存放该元素的后继在数组
中的下标。我们把这种用 数组描述的链表叫做静态链表。

静态链表其实是为了给没有指针的高级语言设计的 种实现单链表能力的方法。尽管大家不一定会用得上,但这样的思考方式是非常巧妙的,应该理解其思想,以备不时之需。

循环链表

将单链表中终端结点的指针端由空指针改为指向头结点,就使整个单链表形成一
个环,这种头尾相接的单链表称 单循环链表,简称循环链表(circular linked list)

非空的循环链表:

在这里插入图片描述
其实循环链表和单链表的主要差异就在于循环的判断条件上,原来是判断p->next 是否为空,现在则是p->next 不等于头结点,则循环未结束。

双向链表

。双向链表
双向链表(double linked List ) 是在单链表的每个结点中,再设置 一个指向其前驱结点的指针域。所以在双向链表中的结点都有两个指针域, 个指向直接后继,另一个指向直接前驱。
/线性表的双向链表存储结构/

typedef struct DulNode
{
ElemType data;
struct DuLNode *prior; /直接前驱指针/
struct DuLNode *next; /直接后继指针/
}DulNode, *DuLinkList;

双向链表的循环带头结点的空链表
在这里插入图片描述
非空的循环的带头结点的双向链表

在这里插入图片描述
插入操作:
在这里插入图片描述
s->prior=p; /把p赋值给s 的前驱如图1/
s->next=p->next; /p->next赋值给s 的后继,如图2/
p->next->prior=s; /把s赋值给p->next的前驱,如图3/
p->next=s; /把s赋值给p的后继,如图4/
关键在于他们的顺序,由于第二步和第三步都用到了p->next。
顺序是先搞定s的前驱和后继,再搞定后结点的前驱,最后解决前结点的后继。

删除操作:
在这里插入图片描述
p->prior->next=p->next; /把p->next赋值给p->prior 的后继,如图中1/
p->next->prior=p->prior; /把p->prior赋值给p->next的前驱,如图2/
free§; /释放结点/

在这里插入图片描述

栈与队列
栈:是限定仅在表尾进行插入和删除操作的线性表

我们把允许插入和删除的一端称为楼顶 (top),另一端称为核底 (bottom)。不
含任何数据元素的栈称为空栈。栈又称为后进先出 ( Last In Filrst Out) 的线性表,简
称 LlFO 结构。
理解栈的定义需要注意:
首先它是线性表。也就是栈元素具有线性关系,即前驱后继关系。定义中说的是在线性表的表尾进行插入和删除操作,这里表尾是指栈顶。

栈的插入操作,叫作进栈,也称压栈、入栈。
栈的删除操作,叫作出栈,也有的叫作弹栈。
在这里插入图片描述
进栈出栈变化形式

最先进栈的元素,是不是就只能是最后出栈呢?
答案是不一定,要看什么情况。栈对线性表的插入和删除的位置进行了限制 ,并
没有对元素进出的时间进行限制,也就是说,在不是所有元素都进栈的情况下,事先
进去的元素也可以出栈,只要保证是栈顶元素出栈就可以。
举例来说,如果我们现在是有 3个整型数字元素 1、2、3依次进栈,会有哪些出
栈次序呢?
• 第一种: 1、2、3进,再 3、2、 1 出。这是最简单的最好蹦车的 一种,出钱
次序为 321
• 第二种: 1进,1 出, 2进.2 出,3 进, 3出。也就是进一 个就出一个,出
枝次序为 123
• 第三种: 1进,2 进,2 出,1 出, 3进, 3出。出栈次序为213
• 第四种: 1进, 1出,2 进,3 进,3 出,2 出。出栈次序为 132
• 第五种 1进,2 进, 2出, 3进,3 出,1 出。 出栈次序为 231
有没有可能是 312 这样的次序出钱呢?答案是肯定不会。因为3 先出栈,就意味
着,3 曾经进栈,既然 3都进栈了,那也就意味着, 1和2已经进栈了,此时,
2一定是在 1的上面,就是更接近栈顶,那么出栈只可能是 321 ,不然不满足 123 依次进栈的要求,所以此时不会发生1比2 先出栈的情况。

ADT 栈(stack)
Data
		同线性表。元素具有相同的类型,相邻元素具有前驱和后继关系。
Operation
		InitStack(*s): 初始化操作,建立一个空栈S。
		DestroyStack(*s):若栈存在,则销毁它。
		ClearStack(*s):将栈清空。
		StackEmpty(S):若栈为空,返回true,否则返回false。
		GetTop(S, *e):若栈存在且非空,用e返回S的栈顶元素。
		Push(*S,e):若栈S存在,插入新元素e到栈S中,并成为栈顶元素。
		Pop(*S,*e):删除栈S中栈顶元素,并用e返回其值。
		StackLength(S):返回栈S的元素个数。
				
endADT

两栈共享空间
在这里插入图片描述
数组有两个端点,两个栈有两个栈底,让 一个栈的栈底为数组的始端,即下标为0 ,另一个栈为数组的末端,即下标为数组长度n-1 处。这样,两个栈如果增加元素,就是两端点向中间延伸。

栈的应用,递归四则运算

队列是指允许在一段进行插入操作、而在另一端进行删除操作的线性表。
队列是一种先进先出 First In First out 的线性表,简称 FIFO 。允许插入的一
端称为队尾,允许删除的一端称为队头。
在这里插入图片描述
同样是线性表,队列也有类似线性表的各种操作,不同的是插入数据只能在队尾进行,删除数据只能在队头进行。

ADT 队列(Queue)

Data
		同线性表。元素具有相同的类型,相邻元素具有前驱和后继关系。
Operation
		InitQueue(*Q):初始化操作,建立一个空队列Q。
		DestroyQueue(*Q):若队列Q存在,则销毁它。
		ClearQueue(*Q):将队列Q清空。
		QueueEmpty(Q):若队列为空,返回true,否则返回false。
		GetHead(Q,*e):若队列Q存在且非空,用e返回队列Q的对头元素。
		EnQueue(*Q,e):若队列Q存在,插入新元素e到队列Q中并成为队尾元素。
		DeQueue(*Q,*e):删除队列Q中对头元素,并用e返回其值。
		QueueLength(Q):返回队列Q的元素个数
endADT	

循环队列

队列顺序存储的不足

假设 一个队列有n 个元素,则顺序存储的队列需建立一个大于 n的数组,并
把队列的所有元素存储在数组的前 n个单元,数组下标为0 的一端即是队头。所谓的
插入队列操作,其实就是在队尾追加一 个元素,不需要移动任何元素,出列需要从对头移出,其余都要往前移动一位。

循环队列的定义: 所以解决假溢出的办法就是后面满 ,就再从头开始,也就是头尾相接的循环。我们把队列的这种 尾相接的顺序存储结构称为循环队列,、

因此通用的计算队列长度公式为:
(rear- front + QueueSize) %QueueSize

循环队列顺序存储结构代码:

typedef int QElemType;   /*QElemType 类型根据实际情况而定,这里假设为int*/
/*循环队列顺序存储结构*/
typedef struct
{
	QElemType data [MAXSIZE];
	int front;  /*头指针*/
	int rear;  /*尾指针,若队列不空,指向队列微元素的下一位置*/
	
}SqQueue;

循环队列初始化代码如下:

Status InitQueue (SqQueue  *Q)
{
		Q->front=0;
		Q->rear=0;
		return OK;
}

循环队列求队列长度代码如下:

/*返回Q的元素个数,也就是队列的当前长度*/
int QueueLength(SqQueue Q)
{
	return (Q.rear-Q.front+MAXSIZE)%MAXSIZE;
}

循环队列的入队列操作:

/*若队列未满,则插入元素e为Q新的队尾元素
Status EnQueue(SqQueue  *Q,QElemType  e)
{
	if((Q->rear+1)%MAXSIZE==Q->front) /*队列满的判断*/
			return ERROR;
	Q->data[Q->rear]=e; /*将元素e赋值给队尾*/
	Q->rear=(Q->rear+1)%MAXSIZE;/*rear指针向后移一位置,,若到最后则转到数组头部*/
	return OK;		
}

循环队列的出队列操作代码如下:

/*若队列不空,则删除Q中队头元素,用e返回其值*/
Status  DeQueue(SqQueue *Q,QElemType *e)
{
	if (Q->front==Q->rear)         /*队列空的判断*/
			return ERROR;
	*e=Q->data[Q->front];   /*将队头元素赋值给e*/
	Q->front=(Q->front+1)%MAXSIZE; /*front指针向后移一位置,若到最后则转到数组头部*/
	return OK;		
}

队列的链式存储结构

队列的链式存储结构,其实就是线性表的单链表,只不过它只能尾进头出而已,
我们把它简称为链队列。为了操作上的方便,我们将队头指针指向链队列的头结点,而队尾指针指向终端结点。

5 串

串(string)是由零个或多个字符组成的有限序列,又名叫字符串
串的比较是通过组成串的字符之间的编码来进行的 而字符的编码指的
是字符在对应字符集中的序号。
计算机中的常用字符是使用标准的 ASCII 编码,更准确一点, 由7位二进制数表
示一个字符,总共可以表示 128 字符。于是扩展 ASCII 码由8 位二进制数表示 一个字符,总共可以表示 256 字符。后来就有了Unicode编码,比较常用的是由16位的二进制数表示一个字符,,为了和 ASCII 码兼容,Unicode 的前 256 个字符与 ASC Il 码完全相同。

串的抽象数据类型:

ADT  串(string)
Data
		串中元素仅由一个字符组成,相邻元素具有前驱和后继关系。
Operation
	StrAssign(T,*chars): 生成一个其值等于字符串常量chars的串T。
	StrCopy(T,S): 	串S存在,由串s复制得串T。	
	ClearStrinq (S) :串s 存在.将串S清空
	StringEmpty (s) : 若串S为空 返回 true ,否则返回 false
	StrLength (S) :返回串S的元素个数,即串的长度。
	StrCompare (S, T) :若S>T ,返回值>0 ,若 S=T ,返回 0. 若S<T ,返回值<0
	Concat(T,S1, S2):用T 返回由 S1和S2 联结接而成的新串。
	SubString (Sub, S, pos, len) :串S 存在. 1<=pos<=StrLength(S),0<=len<=StrLength(S)-pos+1,用Sub返回串S的第pos个字符起长度为len的子串。
	Index (S, T, pos): 串S和T 存在.T 是非空 ,1<=pos<=StrLength(S)若主串 S中存在和串T值相同的子串,则返回它在主串S中第pos个字符之后第一次出现的位置,否则返回0.
	Replace (S, T, V) : 串S,T和V存在. T是非空串。用V 替换主串S 中出现的所有T相等的不重叠的子串。
	StrInsert (S, pos, T ): 串S和T存在, 1<=pos<=StrLength (S) +1 ,在串S 的第 pos 个字符之前插入串T。
	StrDelete (S, pos, len) :串S 存在. l<=pos<=StrLength (S) -len+1,从串S中删除第 pos 个字符起长度为 len 的子串。
endADT

第6章 树

树(Tree)是n(n>=0)个结点的有限集。n=0时称为空树。
在任意一棵树中:
(1)有且仅有一个特定的称为根(Root)的结点;
(2)当n>1时,其余结点可分为m(m>0)个互不相交的有限集T1、T2、。。。、Tn,其中每一个集合本身又是一棵树,并且称为根的子树(SubTree).
在这里插入图片描述
在这里插入图片描述
对于树的定义强调两点:
1、你>0时,根节点是唯一的。不可能存在多个根节点。
2、m>0时,子树的个数没有限制,但他们一定是互不相交的。

结点分类:
树的结点包含 一个数据元素及若干指向其子树的分支。 结点拥有的子树数称为结
点的度 (Degree) 。度为0 的结点称为叶结点(Leaf) 或终端结点;度不为 0的结点
称为非终端结点或分支结点. 除根结点之外,分支结点也称为内部结点。树的度是树
内各结点的度的最大值。
在这里插入图片描述
结点间关系:
结点的子树的根称为该结点的孩子(Child) ,相应地,该结点称为孩子的双亲
(Parent) .同一个双亲的孩子之间直称兄弟 (Sibling)结点的祖先是从根到该结点所经分支上的所有结点。
在这里插入图片描述
在这里插入图片描述
如果将树中结点的各子树看成从左至右是有次序的,不能互换的,则称该树为有
序树,否则称为无序树.
森林 (Forest )是 m (m >=0) 棵互不相交的树的集合。对树中每个结点而言 ,其
子树的集合即为森林。

树的表示方法:
双亲表示法、孩子表示法、孩子兄弟表示法。

双亲表示法:
在这里插入图片描述
在这里插入图片描述
二叉树的定义

二叉树(Binary Tree)是n (n>=0)个结点的有限集合,该集合或者为空集(称为空二叉树),或者由一个根节点和两棵互不相交的,分别称为根节点的左子树和右子树的二叉树组成。

在这里插入图片描述
二叉树特点:

1、每个结点最多有两棵子树,所以二叉树中不存在度大于2的结点,注意不是只有两棵子树,而是最多有,没有子树或者有一棵子树都是可以的。
2、左子树和右子树是有顺序的,次序不能任意颠倒。就像人是双手、双脚,但显然左手、左脚和右手、右脚是不一样的,右手戴左手套、右脚穿左鞋都会极其别扭和难受。
3、即使树中某结点只有一颗子树,也要区分它是左子树还是右子树。

二叉树具有五种基本形态:
1、空二叉树
2、只有一个根节点
3、根节点只有左子树。
4、根节点只有右子树。
5、根节点既有左子树又有右子树。

特殊二叉树:
1、斜树

所有的结点都只有左子树的二叉树叫左斜树。所有结点都是只有右子树的二叉树叫右斜树。这两者统称为斜树。
2、满二叉树
在一棵二叉树中,如果所有分支结点都存在左子树和右子树,并且所有叶子都
同一层上,这样的二叉树称为满二叉树。

在这里插入图片描述
3、完全二叉树
对一棵具有 n个结点的二叉树按层序编号,如果编号为i (l<=i<=n) 的结点与同
样深度的满二叉树中编号为i的结点在二叉树中位置完全相同,则这棵 二叉树称为完
全二叉树。

在这里插入图片描述
满二叉树一定是一棵完全二叉树,但完全二叉树不一定是满的。
在这里插入图片描述
完全二叉树的特点:
1、叶子结点只能出现在最下两层。
2、最下层的叶子一定集中在左部连续位置。
3、倒数第二层,若有叶子结点,一定都在右部连续位置。
4、如果结点度为1,则该结点只有左孩子,即不存在只有右子树的情况。
5、同样结点树的二叉树,完全二叉树的深度最小。

二叉树性质
1、在二叉树的第 i 层上至多有2的(i-1)次方个结点(i>=1)。

2、深度为K的二叉树至多有2的k次方减一个结点(k>=1).
深度为K意思就是有K层的二叉树。

3、对任何一棵二叉树T,如果其终端结点树为n0,度为2 的结点树为n2,则n0=n2+1.
在这里插入图片描述
结点总数为 10 ,它是由A、B、C、D 等度为2 结点,E度为1的结点,F、G、H、I、J度为0的叶子结点组成。总和为 4+1+5=10。
4、具有n 个结点的完全二叉树的深度为[log2 n]+1([x]表示不大于x的最大整数)。

5、如果对于一棵有n个结点的完全二叉树(其深度为[log2 n]+1)的结点按层序编号(从第1层到第[log2 n]+1层,每层从左到右),对任一结点i (1<=i<=n)有:
1)、如果i=1,则结点i 是二叉树的根,无双亲;如果i>1,则其双亲是结点[i/2].
2)、如果2i>n,则结点i无左孩子(结点i为叶子结点);否则其左孩子是结点2i。
3)、如果2i+1>n,则结点i 无右孩子,否则其右孩子是结点2i+1.
在这里插入图片描述
对于第一条,i=l 时就是根结。 i>1 时,比如结点 7,它的双亲
就是 [7/2]=3. 结点 9,它的双亲就是[ 9/2]=4.
对于第二条, 如结点6 ,因为 2X6=12 过了结点总 10 ,所以结点6 无左孩子
它是叶子结点。 同样,而结点5, 因为 2X,5=10 正好是结点总数10 ,所以它的左孩子
是结点 10。
第三条,,比如结点5 ,因为 2X5+1=11 ,大于结点总数 10 ,所以它无右孩子。
结点3 ,因为 2x3+1=7 小于 10 ,所以它的右孩子是结点7.

二叉树顺序存储结构:
顺序存储结构一般只用于完全二叉树。

在这里插入图片描述
二叉链表;
二叉树每个结点最多有两个孩子,所以为它设计一个数据域和两个指针域 是比较自然的想法, 我们称这样的链表叫做 二叉链表。
在这里插入图片描述
其中data是数据域,lchild 和rchild 都是指针域,分别存放指向左孩子和右孩子的指针。

二叉树遍历:
1、前序遍历

先访问根节点,然后前序遍历左子树,在前序遍历右子树。
在这里插入图片描述
2、中序遍历

中序遍历根节点的左子树,然后访问根节点,最后中序遍历右子树。
在这里插入图片描述

3、后序遍历

从左到右先叶子后结点的方式遍历访问左右子树,最后访问根节点。
在这里插入图片描述
4、层序遍历
也就是从根节点开始访问,从上而下逐层遍历,在同一层中,按从左到右的顺序对结点逐个访问。
在这里插入图片描述
树、深林、转为二叉树

第七章 图

图(Graph)是由顶点的有穷非空集合和顶点之间的集合组成,通常表示为:G(V,E),其中,G表示一个图,V是图中顶点的集合,E是图G中边的集合。

线性表中,数据元素之间是被串起来的,仅有线性关系,每个数据元素只有
一个直接前驱和 一个直接后继。
树形结构中,数据元素之间有着明显的层次关系,并且每一层上的数据元素可能和下 层中多个元素相关,但只能和上一层中一个元素相关。这和一对父母可以有多个孩子,但每个孩子却只能有一对父母是一个道理。
是一 种较线性表和树更加复杂的数据结构。在图形结构中,结点之间的关系可以是任意的,图中任意两个数据元素之间都可能相关。

对于图的定义,需注意的地方:
1、线性表中,我们把数据元素叫元素,树中将数据元素叫结点,在图中数据元素,我们则称之为顶点(vertex)。
2、线性表中可以没有数据元素,称为空表。树中可以没有结点,叫做空树。在图结构中,不允许没有顶点。在定义中,若V是顶点的集合,则强调了顶点集合V有穷非空。
3、线性表中,相邻的数据元素之间既有线性关系。树结构中,相邻两层的结点具有层次关系,而图中,任意两顶点之间都可能有关系,顶点之间的逻辑关系用边来表示,边集可以是空的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值