数据结构学习

一、绪论

TARGET

掌握数据结构中涉及的基本概念

掌握算法的时间、空间复杂度及其简易分析方法

1、复杂现实问题

涉及线性表、树、图之类的数据结构

2、研究内容::研究非数值计算的程序设 计问题中计算机的操作对象以及它们之间的关系和操作

3、基本概念

数据:能输入到计算机中去的描述客观事物的符号

包括数值型数据和非数值型数据

数据元素:数据的基本单位,也称节点或记录

数据项:有独立含义的数据最小单位,也称域

数据对象:相同特性数据元素的集合,是数据的一个子集

数据结构:相互之间存在的一种或多种特定关系的数据元素的集合

4、数据结构的内涵

数据逻辑结构:与计算机无关,包括集合、线性结构、树形结构、图形结构

数据存储结构

顺序结构:使用相对位置(ps:可以看成相对于基址偏移)来表示数据元素间的逻辑关系(随机存储)

链式结构:用指针来表示数据元素间的逻辑关系

数据的运算

a、逻辑结构和存储结构相同,但运算不同,则数据结构不同,如栈和队列

问题:栈和队列是两种数据结构,他们的不同体现在哪里?

b、数据结构本身的运算:增、删、查、改、排序

5、数据类型和抽象数据类型

数据类型:即变量所具有的的数据种类,可以看做计算机已经实现的数据结构

抽象数据结构:用户自定义,由基本数据类型组成,包括一组相关的操作,类似c语言中的结构体

ADT = (D,S,P)

6、算法与算法分析

算法定义:求解某类问题所使用的一系列清晰的操作序列

特性:

  • 输入 0或多个输入项
  • 输出 至少一个输出项
  • 有穷性 算法步骤和执行时间都是有限的
  • 确定性 每一步都有明确的意义
  • 可行性 能达到语气目标

算法的评价方法

正确性、可读性、健壮性、高效性

高效性:时间复杂度、空间复杂度

时间复杂度

算法在计算机上执行的时间,两种度量方法

  1. 事后统计

  2. 事前分析估计

取决于:算法选用的策略、问题规模,用O()表示

求解方法:时间复杂度由最深层嵌套语句的重复执行次数决定

ps:有的情况下,算法中基本操作重复执行的次数随问题的输入实例不同而不同

空间复杂度

算法所需存储空间的度量,记作: S(n)=O(f(n))

算法执行要占据的空间 包括

  1. 算法本身要占据的空间,输入/输出,指令,常数,变量等
  2. 算法要使用的辅助空间

二、线性表

线性结构:

只有一个开始节点和一个终端节点

最多只有一个直接前驱和后继

反映的逻辑关系是一对一

TARGET

掌插顺序表的定义、查找、插入和删除

掌插链表的定义、创建、查找、插入和删除

1、线性表:

n(n>=0)个数据元素的有限序列

ps:n=0时称为空表

​ 同一线性表中的元素必定具有相同特性

2、线性表的顺序表示

(又称顺序存储结构或顺序映像)和实现

​ 定义:把逻辑上相邻的数据元素存储在物理上相邻的存储单元中的 存储结构。即:逻辑上相邻,物理上也相邻

​ 方法:用一组地址连续的存储单元依次存储线性表的元素,可通过 数组V[n]来实现

​ 实现:

初始化

两种方式:参数用引用、参数用指针

代码部分

插入
算法描述:
(1)判断揑入位置i 是否合法;
(2)判断顺序表的存储空间是否已满;
(3)将第n至第i 位的元素依次向后移动
一个位置,空出第i个位置;
(4)将要揑入的新元素e放入第i个位置;
(5)表长加1,返回OK。

代码

时间复杂度

最差:O(n)

最好:O(1)

平均:O(n/2)

删除
算法描述:
算法描述:
(1)判断删除位置i 是否合法;
(2)将第i+1至第n 位的元素依次向前移
动一个位置;
(3)表长减1,返回OK。

代码

最差:移动n-1次

最好:不用移动

平均:移动(n-1)/ 2

查找

随机存取,时间复杂度为O(1)

销毁
if(L.elem) delete[]L.elem;
清空
L.length=0
求长度
return L.length
判读是否为空
if (L.length == 0) return true;
else return 0;

特点:

逻辑结构与存储结构一致

访问每个元素花的时间相等

优点:

存储密度大(结点本身所占存储量/结点结构所占存储量)

可以实现随机存取

缺点:

在插入、删除某一元素时需要移动大量元素

当元素数量大且变化多时,浪费存储空间

属于静态存储模式,数据元素不能自由扩充

3、线性表的链式表示

又称为非顺序映像或链式映像

定义:结点在存储器中的位置是任意的,即逻辑上相邻的数据元素 在物理上不一定相邻

方法:指针

相关术语:

单链表或线性链表、双链表、循环链表

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

思考:在链表中设置头结点有什么好处?

将非空表、空表、首元结点的操作统一

typedef struct LNode{
 ElemType data; //数据域
 struct LNode *next; //指针域
}LNode,*LinkList;
// *LinkList为LNode类型的指针

LNode *p <<——>> LinkList p

初始化:

ps:注意区分有无头结点

代码

代码后续补充,未完待续……

双链表、循环链表

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

思考:在链表中设置头结点有什么好处?

将非空表、空表、首元结点的操作统一

typedef struct LNode{
 ElemType data; //数据域
 struct LNode *next; //指针域
}LNode,*LinkList;
// *LinkList为LNode类型的指针

LNode *p <<——>> LinkList p

初始化:

ps:注意区分有无头结点

代码

Status InitList_L (LinkList &L){
 L=new LNode;
 L->next=NULL;
 return OK;
}
插入

头插法

 p->data = v;
 p->next = L->next;  //p的指针域指向L的指针域,即p指向空,
 L->next = p;        //但是此时只有p这个结点,还需要头结点指向p
					//此时头结点指向p,而p指针域指向最后,
					//可以认为p是尾结点,它的指针域自然要指向空了。

尾插法

p->data = v;                 	//输入元素值
p->next = NULL; r->next = p;  	//插入到表尾
r = p;                       	//r指向新的尾结点
								//插入的结点p是尾结点,所以要使r指向p,才能让再次插入的
删除
Status ListDelete_L(LinkList &L,int i){
 p=L;j=0;
 while(p->next &&j<i-1){p=p->next; ++j; }
 if(!(p->next)||j>i-1) return ERROR;
 q=p->next; //保存被删结点的地址以备释放
 p->next=q->next;//改变删除结点前驱结点指针
 delete q; //释放删除结点的空间
 return OK;
}//ListDelete_L
查找、取值
p=L->next; j=1;
while(p &&p->data!=e) {p=p->next; j++;}
if(p) return j;
 else return 0;
p=L->next;j=1; //初始化
 while(p&&j<i) {//直到p指向第i个或p为空
 p=p->next; ++j;
 }
 if(!p || j>i)return ERROR; //第i个元素不存在
 e=p->data; //取第i个元素
 return OK; 
销毁
while(L) {
 p=L;
 L=L->next;
 delete p;
 }
清空
while(p) //没到表尾
{ 
  q=p->next; 
  delete p; 
  p=q;
}
L->next=NULL; //头结点指针域为空
返回表长、判断表是否为空

特点

(1)结点在存储器中的位置是任意的,即逻辑上相邻的数据元素在物 理上丌一定相邻

(2)访问时只能通过头指针进入链表,并通过每个结点的指针域向后扫描其余结点,所以寻找每个结点所花费的时间不等

优点

数据元素的个数可以自由扩充

插入、删除等操作不必移动数据,只需修改链接指针

缺点

存储密度小

存取效率不高,必须采用顺序存取

循环链表

特点:从循环链表中的任何一个结点的位置都可以找到其他所有结点,而单链表做不到;

问题:没有明显的尾端,如何避免死循环?

将判断条件改成遇到头结点结束

循环条件:p!=NULL ->  p!=L
 		p->next!=NULL ->   p->next!=L

ps:给出了尾指针,可以找到尾结点和首元结点,但是注意有个头结点

首元结点: rear->next->next
终端节点: rear
两个链表合并
p = Ta->next;
Ta->next = Tb->next->next;
delete Tb->next;
Tb->next = p;
return Tb;
约瑟夫问题

代码

双向链表

4、线性表的应用

线性表的合并
算法描述:
依次取出Lb 中的每个元素
在La中查找该元素
如果找不到,则将其揑入La的最后
有序表的合并
算法描述:
创建一个空表Lc

依次从 La 或 Lb 中“摘取”元
素值较小的结点插入到 Lc 表的
最后,直至其中一个表变空为止

继续将 La 或 Lb 其中一个表的
剩余结点揑入在 Lc 表的最后
有序链表的合并

要求:结果链表仍使用原来两个链表的存储空间, 不另外占用其它的存储空间;表中允许有重复的数据

算法描述:
 (1)Lc指向La
 (2) 依次从 La 或 Lb 中“摘取”元素值较小的结点揑入到 Lc 表的最后,直至其中一个表变空为止
 (3) 继续将 La 或 Lb 其中一个表的剩余结点插入在Lc 表的最后
 (4) 释放 Lb 表的表头结点

思考:如何排除相同的元素

通过增加判断条件,单独考虑相等,只增加一个值,指针同时后移

思考:将两个非递减的有序链表合并为一个非递增的有序链表,如何实现?

用头插法,即可以逆序

三、栈与队列

TARGET

掌插栈和队列的特点,并能在应用问题中正确选用

熟练掌插栈的两种存储结构的基本操作实现算法,特别应注意栈满和栈空的条件

理解递归算法执行过程中栈的状态变化过程

熟练掌插循环队列和链队列的基本操作实现算法,特别注意队满和队空的条件

掌插进制转换表达式求值等问题的方法实现

1、栈和队列的定义
栈:

定义:只能在表的一端进行插入和删除运算的线性表

逻辑结构:一对一

存储结构:顺序栈、链栈

实现方式:根据存储方式的不同而不同,包含入栈、出栈、取栈顶元素、建栈、判断栈空、栈满

队列:

定义:只能在表的一端(队尾)进行插入,在另一端(队头)进行删除运算的线性表

逻辑结构:一对一

存储结构:顺序队列、链队列

实现方式:根据顺序队或链队的不同而不同

入队、出队、建队列、判断队列满、队列空等

2、栈的表示和操作
顺序栈
初始化
元素进栈
元素出栈

思考:在一个程序中需要同时使用具有相同数据类型的两个栈,如何高效存储这两个栈?(从节约空间角度)

​ 一种可取的方法是充分利用顺序栈单向延伸的特性,使用一个数组来存储两个栈,让每个栈的栈底为该数组的始端,另一个栈底为该数组的末端。每个栈从各自的端点向中间延伸。

链栈

如何改造链表,实现栈的链式存储?

a、将链表头作为栈顶(前揑法),方便操作

b、删除头结点

3、栈与递归

递归定义:若一个函数、过程或者数据结构定义的内部又直接 (或间接)的出现定义本身,则称它是递归的。

特点

a、后调用,先返回

应用在递归定义的数学函数

具有递归特性的数据结构

可递归求解的问题如汉诺塔问题

递归算法复杂度分析

时间复杂度:与递归树的节点数成正比

空间复杂度:与递归树的高度成正比

优缺点

优点:结构清晰,程序易读

缺点:使用递归时间开销大,每次调用时保存状态信息,入栈;返回时要出栈,恢复状态信息

4、队列的表示和操作
顺序队列

问题1:假溢出

即front != 0 但 rear = MAXSIZE在入队

解决:循环队列,用模运算实现

 入队:
 base[rear]=x;
 rear=(rear+1)%MAXQSIZE ;
 出队:
 x=base[front];
 front=(front+1)%MAXQSIZE; 

问题2:队空、队满情况一样

解决:

1.另外设一个标志以区别队空、队满
2.少用一个元素空间:
 队空:front==rear
 队满:(rear+1)%==front
初始化
Status InitQueue (SqQueue &Q){
 Q.base =new QElemType[MAXQSIZE]
 if(!Q.base) exit(OVERFLOW);
 Q.front=Q.rear=0;
 return OK;
}
入队列
if((Q.rear+1)%MAXQSIZE==Q.front) return ERROR;
 Q.base[Q.rear]=e;
 Q.rear=(Q.rear+1)%MAXQSIZE;
 return OK;
出队列
if(Q.front==Q.rear) return ERROR;
 e=Q.base[Q.front];
 Q.front=(Q.front+1)%MAXQSIZE;
 return OK;
链队
5、具体问题

十进制与其他进制的转换

括号匹配的检验

表达式的求值:需要设置OPND(操作数或运算结果)栈和OPTR(运算符)栈两个栈

舞伴问题:用队列实现

四、串、数组、广义表

1、串的定义

定义:零个或多个字符组成的有限序列

串的存储结构

​ 顺序存储

​ 链式存储:存储密度比较低

2、串的运算

模式匹配算法:确定主串中所含子串第一次出现的位置(定位)

包括BF算法和KMP算法

BF算法

int Index(SString S,SString T,int pos)
{
    i = pos;
    j = 1;
    while(i <= S.length && j <= T.length)
    {
        if(S[i] == T[j])
        {
        	i++;
        	j++;
        }
        else
        {
        	i = i - j + 2;
            j = 1;
        }
    }
    if(j > T.length) 
        return i - T.length ;
    else
    	return false;
}

时间复杂度:O(m*n)

KMP算法

利用已经部分匹配的结果加快模式 串的滑动速度

主串S的指针i不必回退

可提速到O(n+m)

算法策略:出现字符不等时,不回退主指针i,利用已得到的“部分匹配”结果将模式向右滑动尽可能远的一段距离。

void get_next(SString T, int &next[]){	i = 1;    next[1] = 0;    j = 0;    while(j == 0 || i <= T.length )    {        if(T[i] == T[j])        {            i++;        	j++;        	next[i] = j;        }        else            j = next[j];    }
void Index(SString S, SString T, int pos){    int i = pos;    j  = 0;    while(i <= S.length && j <= T.length)    {        if(S[i] == T[j])        {            i++;            j++;        }        else            j = next[i];    }    if(j > T.length)         return i - T.length;    else        return false;}
3、具体问题

病毒感染检测

算法步骤(实训题参考)]① 从文件中读取待检测的任务数num;② 循环num次,执行以下操作:	从文件中分别读取一对病毒DNA序列和人的DNA序列;	设置标志性变量flag,用来标识是否匹配成功,初始为0,表示未匹配;	病毒DNA序列的长度是m,将其复制扩大为2m;	循环m次,重复执行以下操作:		依次取得每个长度为m的病毒DNA环状字符串;		将此字符串作为模式串,将人的DNA序列作为主串,调用BF算法进行模式匹配,将匹配结果返回赋值给flag;	若flag非0,表示匹配成功,中止循环,表明该人感染了对应的病毒。	退出循环时,判断flag的值,若flag非0,输出“YES”,否则,输出“NO”
4、数组
二维数组与顺序存储

行向量(列序)

列向量(行序)

矩阵的压缩存储

压缩存储:若多个数据元素的值都相同,则只分配一个元素值的存储空间 ,且零元素不占存储空间。

一些特殊的矩阵能压缩:如对称矩阵,三角矩阵,对角矩阵

5、广义表
1、定义:

n (>= 0 )个表元素组成的有限序列,记作LS = (a0 , a1 , a2 , …, an-1 )

表元素可以是表 (称为子表),可以是数据元素(称为原子)

**广义表不线性表的区别 **

广义表是线性表的扩展,扩展后不一定是线性表

2、基本运算

GetHead(L):非空广义表的第一个元素,可以是一个单元素,也 可以是一个子表

GetTail(L):非空广义表除去表头元素以外其它元素所构成的表。 表尾一定是一个表

五、树和二叉树

TARGET

掌握二叉树的前、中、后序遍历方法

掌握哈夫曼树的实现方法、构造哈夫曼编码的方法

1、树和二叉树的定义
树的定义:

n(n≥0)个结点的有限集, n = 0为空树; n > 0为非空树

对于非空树T:

​ 有且仅有一个称之为根的结点;

​ 除根结点以外如果有其余结点,则可分为m(m>0)个互不相交的 有限集T1 , T2 , …, Tm, 其中每一个集合本身又是一棵树,并且称为根的子树(SubTree)

基本术语

结点、根、叶子、双亲、孩子、兄弟、堂兄弟、祖先、子孙

结点的度、树的度、结点的层次、树的深度(高度)、有序树、无序树、森林

二叉树的定义

n(n≥0)个结点的有限集, n = 0为空树, n > 0为非空树

对于非空树T:

有且仅有一个称之为根的结点;

除根结点以外如果有其余结点,则可分为两个互不相交的子集T1和 T2,分别称为T的左子树和右子树,且T1和T2本身又都是二叉树

ps:所有的树都能转为唯一对应的的二叉树

特点

​ 结点的度小于等于2

​ 左右子树不一样,不能颠倒

2、二叉树的性质和存储结构
性质:

性质1: 在二叉树的第i层上至多有 2i-1 个结点

性质2: 深度为k的二叉树至多有 2k-1个结点

性质3: 对于任何一棵二叉树,若2度的结点数有n2个,则叶 子数n0必定为 n2+1(即n0=n2+1)

满二叉树::一棵深度为k 且有2k -1个结点的二叉树

完全二叉树:深度为k 的,有n个结点的二叉树,当且仅当其每一个结点都不深度为k 的满二叉树中编号从1至n的结点一一对应

特点:叧有最后一层叶子不满,且全部集中在左边

性质4: 具有n个结点的完全二叉树的深度必为 log2n+1

性质5: 对完全二叉树,若从上至下、从左至右编号,则编号为i(1 ≤ i ≤ n) 的结点,其左孩子编号必为2i (如果有) ,其右孩子编号 必为2i+1(如果有);其父结点的编号必为i/2 (如果有) 。

思考:二叉树有无顺序存储结构?

有顺序存储结构,按照完全二叉树的节点层次编号,一次存放二叉树中的数据元素

**题目:**在n个结点的二叉链表中,有 n+1 个空指针域

​ 在n个结点的三叉链表中,有 n+2 个空指针域

计算方法:总指针数(2*n或3*n) 减去 边的数量((n-1) 或2*(n-1))

3、遍历二叉树与线索二叉树
递归

先序遍历

递归算法: 若二叉树为空,则空操作 否则    访问根结点 (D)    先序遍历左子树 (L)    先序遍历右子树 (R)

中序遍历

递归算法描述: 若二叉树为空,则空操作 否则    中序遍历左子树 (L)    访问根结点 (D)    中序遍历右子树 (R)

后序遍历

递归算法描述: 若二叉树为空,则空操作 否则	后序遍历左子树 (L) 	后序遍历右子树 (R) 	访问根结点 (D)

时间效率:O(n) 空间效率:O(k)(k为树高),最差O(n)

ps:代数表达式与中序遍历一致

非递归

如何实现中序遍历的非递归实现分析算法?

使用栈数据结构

遇到结点就入栈,遍历它的左子树当左子树遍历结束,栈顶弹出结点访问继续遍历它的右子树 

结论:若二叉树中各结点的值均不相同,则由二叉树的前序序列和中序序列,或由其后序序列和中序序列均能唯一地确定一棵二叉树

线索二叉树
定义:

利用空链域(n+1个空链域)

1)若结点有左子树,则lchild指向其左孩子; 否则, lchild指向其直接前驱(即线索);

2)若结点有右子树,则rchild指向其右孩子; 否则, rchild指向其直接后继(即线索) 。

为了避免混淆,增加两个标志域 lchild LTag data RTag rchild

标志位为1,则说明指向线索 RTag = 1 或 LTag = 1

分类

先序线索二叉树

中序线索二叉树

后续线索二叉树

4、树与森林
数的存储结构

父节点表示法:找父节点容易,找孩子难

孩子表示法:找孩子容易,找父节点难

孩子兄弟表示法:一般树转换成二叉树的方法

树与二叉树的转换

树转换为二叉树

(1)加线。在所有兄弟结点之间加一条连线。

(2)去线。树中的每个结点,只保留它与第一个孩子结点的连线,删除它与其它孩子结点之间的连线。

(3)层次调整。以树的根节点为轴心,将整棵树顺时针旋转一定角度,使之结构层次分明。(注意第一个孩子是结点的左孩子,兄弟转换过来的孩子是结点的右孩子)

森林转换为二叉树

(1)把每棵树转换为二叉树。

(2)第一棵二叉树不动,从第二棵二叉树开始,依次把后一棵二叉树的根结点作为前一棵二叉树的根结点的右孩子,用线连接起来。

5、哈夫曼树及其应用

**前缀编码(**简称前缀码):所有的代码字都不是另一个字符代码字的前缀。

目的:高频字符编码短 低频字符编码长

哈夫曼编码的压缩率:(定长编码 - 哈夫曼编码长度)/定长编码

基本概念

路径、路径长度、树的路径长度、权、结点的带权路径长度、数的权路径长度、哈夫曼树

哈夫曼树

哈夫曼树中只有度为0和度为2的结点

二叉树的性质3(P118):n0=n2+1

哈夫曼树总结点数:2n-1

树的构造
哈夫曼编码

六、图

1、定义与概念

图的定义:

稀疏图、稠密图、邻接点、度

简单路径、简单回路

子图(边和点都属于另一图就是子图)

连通图(强连通图)

2、存储方式:

顺序存储方式(邻接矩阵)、链式存储方式(邻接表、十字链表、邻接多重表)

邻接矩阵表示法
特点

a、无向图

无向图的邻接矩阵是对称的

顶点i 的度=第 i 行 (列) 中 1 的个数

特别地:完全图的邻接矩阵中,对角元素为0,其余1

b、有向图

特点

顶点的出度=第i行元素之和;顶点的入度=第i列元素之和

顶点的度=第i行元素之和+第i列元素之和

优缺点

优点:容易实现查找顶点、判断是否邻接、计算度操作

缺点: 不便于增加和删除顶点;

n个顶点需要n*n个单元存储边;空间效率为O(n2),对稀疏图 而言尤其浪费空间

邻接表表示法

表示:对每个顶点vi 建立一个单链表,把与vi相邻接的顶点放在这个链 表中,包括表头结点和边表

ps:表头结点另外用顺序存储方式存储

ps:顶点的度=出边表中的结点数OD( Vi ) +邻接点域为Vi的个数ID(Vi )

十字链表
3、图的遍历

深度优先:模拟树的先序遍历过程

广度优先:模拟树的层次遍历过程

• 在访问了起始点v之后,依次访问 v的邻接点• 然后再依次访问这些顶点中未被访问过的邻接点• 直到所有顶点都被访问过为止

效率:

邻接表:O(n+m)

邻接矩阵:O(n2)

4、图的应用
最小生成树

连通网的一棵权重最小的生成树,其中树的权重定义为 树中所有边的权重总和

1、穷举法

2、Prim法(加点法)

思想:任意选择一个结点作为序列中的初始子树。每一次迭代时,把不在树中的最近结点添加到树中。当网的所有结点都包含在所构造的树中后,算法停止

3、Kruskal法(加边法)

最短路径

(一)单源最短路径—Dijkstra(迪杰斯特拉)算法

与prim算法唯一的不同是,计算到源点的距离,而prim算法计算的是到树的距离

时间复杂度:邻接矩阵O(n2) 邻接表O(m+nlogn)

(二)所有顶点间的最短路径—Floyd(弗洛伊德)算法

拓扑排序

① AOV网(Activity On Vertices):用顶点表示活动的网络

算法:重复选择没有直接前驱的顶点

输入AOV网络;2. 在AOV网络中选一个没有直接前驱的顶点, 输出;3. 从图中删去该顶点, 同时删去所有它发出的有向边;4. 重复以上 2、3 步, 直到:	全部顶点均已输出,拓扑排序完成;	或存在错误,AOV网络中必定存在有向环

② AOE网(Activity On Edges):用边表示活动的网络

关键路径

关键路径:路径长度最长 的路径

关键活动:关键路径上的活动,即这些活动的时间延迟或提前会影响整 个工程工期的延迟或提前

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值