串行计算中 --------顺序、分支、循环、递归、无条件转移
逻辑数据结构:反映数据之间的逻辑的关系。
存储数据结构:反映成分数据在计算机内的存储安排。
数据类型:同一种类型数据的全体称为一个数据类型。
抽象数据类型:算法的一种数据模型连同定义在该模型上并作为该算法构件的一组模型;
线性表是一种非常灵便的结构,可以根据长度需要改变表的长度,也可以在表中任何位置对元素进行访问、插入和删除等操作,
由零个或多个数据元素组成的有限序列。
运算原则:随机存取
前驱:称元素a(k)在元素之前,或a(k)是a(k+1)的前驱,k = 1,2,3,4······,n-1
后继:称元素a(k+1)在a(k)元素之后,或a(k+1)是a(k)的后继。
数据域:存储数据元素信息的域。
指针域:存储直接后继位置的域。指针域中存储的信息称为指针或者链。
两部分信息组成数据元素称为存储映像。
(1)数组的单链表
(2)指针的单链表
(3)游标类型的单链表
用游标代替指针
(4)循环链表
当 rear = rear->next时,循环链表为空链表
(5)双链表
prior 为前驱指针;
next 为后继指针;
p->prior->next = p.next.prior;
1.结点空间可以动态申请和释放;
2.数据元素的逻辑次序靠结点的指针来指示,插入和删除不需要移动元素位置;
1.存储密度小,每个结点的指针域需要额外占据存储空间。(存储密度 == 结点数据本身占用的空间/结点占用的空间总量; 存储密度越高,存储空间的利用率越高)
2.链式存储结构是非随机存储结构
栈是一种特殊的线性表。这种表只在表首进行插入和删除操作。因此,表首对于栈来说具有特殊意义,为栈顶。相应地,表尾为栈底。不含任何空元素为空栈。
栈的修改是后进先出的原则。
表头(栈顶)Top 表尾(栈底)Base。
插入为入栈 删除为出栈(抛栈)。
插入和删除限定:
最后插入的元素会被最先删除。
逻辑结构:与线性表差不多的(一对一)
运算规则:后进先出
存储结构:顺序栈、链栈,
数制转换、表达式求值、括号匹配的检验、八皇后问题、行编辑程序、函数调用、迷宫求解、递归调用的实现
特点:简单、方便、但容易产生溢出(数组大小固定)
上溢:栈已经满了,又要压元素。是一种错误,使问题的处理没办法进行。
下溢:栈已经空了,还要弹出元素。是一种结束,即问题处理结束。
定义:
递归问题
递归优缺点:
优点:结构清晰,程序易读。
缺点:每次调用要生成工作记录,保存状态信息,入栈;返回时要出栈,回复状态信息。时间开销大。
递归-----> 非递归
方法1:尾递归、单向递归——>循环结构
方法2:自用栈模拟系统的运行时栈
队列是另一种特殊的线性表。这种表只在表首(队首)进行删除操作,只在表尾(队尾)进行插入操作。由于队列的修改是按照先进先出规则进行的,所以队列又称为先进先出表
,简称FIFO
表。
插入和删除限定:
只能在表尾插入。
只能在表首删除。
逻辑结构:与线性表一样,一对一
运算规则:只能在队首和队尾运算,访问结点时依照先进先出原则
存储结构:顺序队、链队,以循环顺序队列更常见
实现方式:入队和出队操作。
设数组大小为MAXSIZE
rear == MAXSIZE时,发生溢出
rear == MAXSIZE时再入队为:真溢出
rear == MAXSIZE时再入队为:假溢出
将队中元素依次向队头方向移动。缺点:浪费时间每移动一次,队中元素都要移动。
将队空间设想成一个循环的表,即分配给队列的m个存储单元可以循环使用,当rear为maxqsize时。
若向量的开始端空着,又可从头使用空着的空间。当front为maxqsize时,也是一样。
3)判断-队空-队满的方法
另外设置一个标志以区别 队空、队满
另设一个变量,记录元素的个数
少用一个元素空间
循环队列中,头指针(front)和尾指针(rear)表示 , MAXSIZE表示数组空间的大小。
如果用以表示循环队列的话,出队时,头指针的移动可以这样表示:front = (front+1) % MAXSIZE
;入队时,尾指针的移动可以这样表示:rear = (rear+1) % MAXSIZE
。
队空判断为:front == rear。
队满判断:front == (rear+1)% MAXSIZE (保留一个空间,用以判断队满)。
链队列操作
按数据存储介质:内部排序和外部排序
按比较器 :串行排序和并行排序
按主要操作 :比较排序和基数排序
#include
#include
typedef int Status;
#define OK 1
#define ERROR 0
#define true 1
#define false 0
#define MAXSIZE 20 //设记录数不超过20个
typedef int KeyType; //设关键字为整数类型·
typedef char *InfoType;
typedef struct redtype //定义每一个记录(数据元素)的结构
{
KeyType key; //关键字
InfoType info; //其他数据项
} RedType;
typedef struct sqlist *slink;
typedef struct sqlist //定义顺序表的结构
{
RedType r[MAXSIZE - 1]; //存储顺序表的向量
//r[0]一般作哨兵或缓冲区
int length; //顺序表的长度
} SqList;
直接插入排序——————————采用顺序查找法查找插入位置
直接插入排序(使用哨兵)
复制哨兵 L.r[0] = L.r[i];
记录后移,查找插入位置 for(j = i-1 ; L.r[0].key < L.r[j].ley ;–j) L.r[j+1] = L.r[j];
插入到正确位置 L.r[j+1] = L.r[0 ];
/* 直接插入排序 */
void Insert(SqList L)
{
int i, j;
for (i = 2; i < L.length; ++i)
{
if (L.r[i].key < L.r[i - 1].key)
{
L.r[0] = L.r[i]; //复制为哨兵
for (j = i - 1; L.r[0].key < L.r[j].key; --j)
{
L.r[j + 1] = L.r[j]; //记录后移
}
L.r[j + 1] = L.r[0]; //插入到正确位置
}
}
}
性能分析
实现排序的基本操作有两个
最好情况(关键字在记录序列中是顺序有序的):
11,25,32,47,56,70,81,85,92,96
(记录序列)
"比较"的次数:
"移动"的次数:0
最坏情况(关键字在记录序列中是逆序有序的):
85,92,96,81,70,56,47,32,25,11
(记录序列)
"比较"的次数:
"移动"的次数:
平均的情况
"比较"的次数:
"移动"的次数:
时间复杂度结论
查找插入位置时采用折半查找法
void InsertBin(SqList L)
{
int i, low = 1, hight, mid;
for (i = 2; i <= L.length; ++i)
{
L.r[0] = L.r[i]; //复制为哨兵
hight = i - 1;
low = 1;
while (low <= hight)
{
mid = (low + hight) / 2;
if (L.r[0].key < L.r[mid].key)
{
hight = mid - 1;
}
else
{
low = mid + 1;
}
}
for (j = i - 1; j >= hight + 1; --j)
{
L.r[j + 1] = L.r[j]; //记录后移
}
L.r[hight + 1] = L.r[0]; //插入到正确位置
}
}
算法分析
基本思想——————先整个待排记录序分割成若干子序列,分别进行直接插入排序,待整个序列中的记录”基本有序“时,再对全体记录进行一次直接插入排序
特点
思路:
希尔排序算法效率与增量序列的取值有关
Hibbard增量序列
Sedgewick增量序列
{1,5,19,41,109,。。。}
————————9*4(i)-9*2(i)+1或4(i)-3*2(i)+1
猜想:T(avg)=O(n^(7/6)) T(worst)=(n^(4/3))
稳定性
两两比较,如果发生逆序则交换,直到所有记录都排好序为止
基本思想:每趟不断将记录两两比较,并按"前小后大"规则交换
升序
/* 冒泡排序 */
void bubble_sort(SqList L)
{
int m, j, i;
int n = L.length;
RedType X;//交换临时存储单元
for (m = 1; m <= n; m++)//总共需要m趟
{
for (j = 1; j <= n - m; i++)
{
if (L.r[j].key > L.r[j + 1].key)//发生逆序
{
//交换
X = L.r[j];
L.r[j] = L.r[j + 1];
L.r[j + 1] = x;
}
}
}
}
优点:每趟结束时,不仅挤出一个最大值到最后面位置,还能同时部分理顺其他元素;
提高效率:
时间复杂度
算法评价
基本思想:
具体实现:选定一个中间数值作为参考,所有元素与之比较,小的调到其左边,大的调到其右边。
枢轴(中间数):可以是第一个数,也可以是最后一个数,最中间的一个数,任选一个数等。
特点:
/* 快速排序 */
void QSrot(SqList L, int low, int hight)
{
int pivotloc;
if (low < hight)
{
pivotloc = Partiton(L, low, hight); //将L.r[low,hinght]一分为二,pivotloc为枢轴元素排好序的位置
QSrot(L, low, pivotloc + 1); ///对低子表的递归排序
QSrot(L, pivotloc - 1, hight); //对高子表的递归排序
}
}
int Partiton(SqList L, int low, int hight)
{
KeyType pivotkey;
L.r[0] = L.r[low];
pivotkey = L.r[low].key;
while (low < hight)
{
while (low<hight && L.r[hight].key >= pivotkey)
{
--hight;
}
L.r[low] = L.r[hight];
while (low<hight && L.r[hight].key <= pivotkey)
{
++low;
}
L.r[hight] = L.r[low];
}
L.r[low] = L.r[0];
return low;
}
时间复杂度
空间复杂度:
稳定性:
由于每一次枢轴记录的关键字都是大于其他所哟记录的关键字,致使一次划分后得到的子序列(1)的长度为0,这时已经退化为成为没有改进的冒泡排序
划分元素的选取是影响时间性能的关键
堆定义:
堆排序
基本思想:
/* 堆排序的筛选 */
void HeapAdjust(elem R[], int s, int m)
{
/* 已知R[s。。m]中记录的关键字除R[s]之外均满足堆的定义,本函数调整R[s]的关键字,使R[s。m]成为一个大根堆 */
int j, i;
rc = R[s];
for (j = 2 * s; j <= m; j *= 2) //沿着key较大的孩子结点向下筛选
{
if (j < m && R[j] < R[j + 1]) //j为key较大的记录的下标
++j;
if (rc >= R[j])
break;
R[s] = R[j];
s = j;
/* code */
}
R[s] = rc;
}
/* 建立小根堆 */
void HeadScort(elem R[])
{
int i;
for (i = n / 2; i >= 1; --i)
{
HeapAdjust(R, i, n); //建初始堆
}
for (i = n; i > 1; --i)
{
Swap(R[1], R[i]); //根与最后一个元素进行交换
HeapAdjust(R, 1, i - 1); //对R[1]到R[i-1]重新建堆
}
}
基本操作:
算法性能分析
两个或两个以上的有序序列"归并"为一个有序序列
在内部排序中,通常采用的是2-路归并排序
整个归并排序需要[log₂n]趟
归并排序算法分析
稳定性:稳定
类别 | 排序方法 | 时间复杂度 | 时间复杂度 | 时间复杂度 | 空间复杂度 | 稳定性 |
---|---|---|---|---|---|---|
类别 | 排序方法 | 最好情况 | 最坏情况 | 平均情况 | 辅助存储 | 稳定性 |
插入排序 | 直接插入排序 | O(n) | O(n²) | O(n²) | O(1) | 稳定 |
插入排序 | 希尔排序 | O(n) | O(n²) | ~O(n^(1.3)) | O(1) | 不稳定 |
交换排序 | 冒泡排序 | O(n) | O(n²) | O(n²) | O(1) | 稳定 |
交换排序 | 快速排序 | O(n㏒n) | O(n²) | O(n㏒n) | O(n㏒n) | 不稳定 |
选择排序 | 直接选择排序 | O(n²) | O(n²) | O(n²) | O(1) | 不稳定 |
选择排序 | 堆排序 | O(n㏒n) | O(n㏒n) | O(n㏒n) | O(n²) | 不稳定 |
归并排序 | 归并排序 | O(n㏒n) | O(n㏒n) | O(n㏒n) | O(n) | 稳定 |
基数排序 | k:待排元素的个数,m为基数的个数 | O(n+m) | O(k*(n+m)) | O(k*(n+m)) | O(n+m) | 稳定 |
零个或者多个任意字符组成的有限序列。
S = "a1 a2 a3 ...... an" // (n>=0)
串名:S
串值:“a(1) a(2) a(3) … a(n)”
串长:n
空串:n = 0;
子串:一串中任意个连续字符组成的子序列(含空串)称为该串的子串。
主串:包含字串的串相应地称为主串
字符位置:字符在序列中的序号为该字符在串中的位置
子串位置:子串第一个字符在主串中的位置
空格串:由一个或多个空格组成的串 与空串不同
串相等:当且仅当两个串长度相等,并且各个对应位置上的字符都要相等,这两个串才是相等的。
计算机非数值处理对象(文字编辑、符号处理)
病毒感染检测
顺序存储结构:顺序串
链式存储结构:链串
优点:操作简单
缺点:存储密度低
确定主串中所含子串(模式串)第一次出现的位置
算法种类:
#define MAXLEN 255
typedef struct
{
char ch[MAXLEN + 1]; //存储串的一维数组
int length; //串当前的长度
} Sstring;
int index(Sstring S, Sstring T,int pos)
{
//i为主串下标 j为模式串下标
int j = 1, i = pos;
while (i <= S.length && j <= T.length)
{
if (S.ch[i] == T.ch[j])
{
i++;
j++;
}
else
{
//回溯算法
i = i - j + 2;
j = 1;
}
}
if (j > T.length)
{
return i - T.length //匹配位置
}
else
{
return 0;
}
}
模式串的长度m,主串长度为n
时间复杂度:最快为O(m),最慢为O((n-m)m+m)或者 O((n-m+1)m) 若模式串的长度远远小于主串此时时间复杂度为 O(m*n)
//KMP算法
int index_KMP(Sstring S, Sstring T, int pos)
{
int i = pos, j = 1;
while (i < S.length && j < T.length)
{
if (j == 0 || S.ch[i] == T.ch[j])
{
i++;
j++;
}
else
{
j = next[j];
}
}
if (j > T.length)
{
return i - T.length;
}
else
{
//匹配不成功
return 0;
}
}
//较为完善
void get_nextVal(Sstring T, int nextVal[])
{
i = 1;
nextVal[1] = 0;
j = 0;
while (i < T.length)
{
if (j == 0 || T.ch[i] == T.ch[j])
{
++i;
++j;
if (T.ch[i] != T.ch[j])
{
nextVal[i] = j;
}
else
{
nextVal[i] = nextVal[j];
}
}
else
{
j = nextVal[j];
}
}
}
//较为简单
void get_next(Sstring T; int next[])
{
i = 1;
next[1] = 0;
j = 0;
while (i < T.length)
{
if (j == 0 || T.ch[i] == T.ch[j])
{
++i;
++j;
next[i] = j;
}
else
{
j = next[j];
}
}
}
时间复杂度:为O(m+n),
数组特点:结构固定----定义后,维数和维界不变。
二维数组 :
存储单元是一维结构,而数组是个多维结构,则用连续存储单元存放数组的数据元素就有次序约定问题
以行序为主序
算存储地址单元:例:A[n][m]
a[i][j]的存储单元为:a[0][0]的地址单元 + (n*i+j)*size
以列序为主序
三维数组:
按页/行/列存放,页优先的顺序存储
a[m1][m2][m3]各维元素的个数为m1,m2,m3
下标为i1,i2, i3的数组元素的存储位置
LOC(i1,i2,i3) = a + i1*m2*m3 //前i1页的总元素数
+ i2*m3 //第i1页的前i2行总元素个数
+ i3 //第i2行前i3列元素个数
n维数组
- 各维元素个数为 m1,m2,m3,m4,......,m(n)
- 下标为i1,i2,i3,i4,......,i(n)的数组元素的存储位置:
- LOC(i1,i2,i3,i4,......,i(n)) = a + i1*m2*m3*m4*.....*m(n)+i2*m3*m4*.....*m(n)+.......+i(n-1)*m(n) +i(n)
= a + n-1 n
( ∑ i(j) * ∏ m(k) ) + i(n)
j = 1 k = j+1
特殊矩阵的存储压缩
对称矩阵
在n*n的矩阵a中,满足如下性质:
a(i)(j) = a(j)(i)(1<=i,j<=n)
存储方法
只存储下(或者上)三角(包括主对角线)的数据元素。共占用n(n+1)/2个元素空间。
存储结构
三角矩阵
特点:对角线以下(或者以上)的数据元素(不包括对角线)全部为常数c。
存储方法:重复元素 c 共享一个元素存储空间
,共占用n(n+1)/2+1
个元素
空间:Sa[1…n(n+1)/2+1]
上三角公式 下三角公式
k = { (i-1)*(2n-i+2)/2+j-i+1 i<=j; k = { i*(i-1)/2+j i>=j
n*(n+1)/2+1 i>j n*(n+1)/2+1 i
对角矩阵(带状矩阵)
特点:在n*n的方阵中,所有非零元素都集中在以主对角线为中心的带状区域中,区域外的值都是0,则称为对角矩阵。常见的有三对矩阵角,五对角矩阵,七对角矩阵等。
存储方法:以对角线的顺序存储
{1,2,3,0,0,0},
{1,2,0,4,0,0},
{1,0,3,0,5,0},
{0,2,0,4,5,6},
{0,0,3,4,5,6},
{0,0,0,4,5,6},
稀疏矩阵
顺序存储结构
什么是稀疏矩阵:矩阵中非零元素的个数较少(一般少于5%)。
设在 m*n 的矩阵中有t个非零元素。
0,12, 9, 0, 0, 0 i代表第几行,j代表第几列,aij是当前元素的值
0, 0, 0, 0, 0, 0 三元组(i,j,aij)惟一确定矩阵的一个非零元
-3, 0, 0, 0,14, 0
M = 0, 0, 0, 0, 0, 0 M = {(1,2,12),(1,3,9),(3,1,-3),(3,5,14),(5,3,24),(6,2,18),(7,1,15),(7,4,-7)}和矩阵维数(7,6)唯一确定
0, 0,24, 0, 0, 0
0,18, 0, 0, 0, 0
15, 0, 0,-7, 0, 0
压缩存储原则:存非零元的值、行列位置和矩阵的行列数。
三元组:
链式存储结构
十字链表:
优点:它能够灵活地插入因运算而产生的新的非零元素,删除因运算而产生的新元素,实现矩阵的各种运算。
在十字链表中,矩阵的每一个非零元素用一个结点表示,该结点除了(row,col,value)以外,还要有两个域:
right:用于链接同一行中的下一个非零元素;
down :用以链接同一列中的下一个非零元素。
row | col | value |
---|---|---|
down(列) | right(行) |
0, 2
N= 1, 0
-2, 4
0, 0
[ 1 ] [ 2 ] M.chead(列指针)
M.rhead(行指针)
[1][2][2]
[1]——————————————————----------[][^]
|
[2][1][1] |
[2]-----------[][^] |
|
[3][1][-2] [3][2][4]
[3]-------------[^][]----------[^][^]
[4]
广义表(又称为列表Lists)是 n >= 0 个元素 a(0),a(1),a(3),…,a(n-1)的有限序列,其中每一个a(i)或者是原子,或者是一个广义表。
通常记作 Ls = {a(1),a(2),a(3),a(4),…,a(n)} 其中 Ls 是表名 ,n为表的长度,每一个 a(i) 为表的元素
一般用大写字母
表示广义表
,小写字母
表示原子
表头:若非空(n>=1),则第一个元素a(1)就是表头
记作 head(LS) = a(1)。
注意表头:可以是原子,也可以是子表
表尾:除表头以外的其他元素组成的表。
记作 tail(LS) = (a(2),a(3),a(4),…,a(n)}。
注:表尾不是最后一个元素,而是一个子表。
注意:"原子"的深度为0,"空表"的的深度为1
。共享
;当二维数组的每一行(每一列)作为子表处理时,二维数组即为一个广义表。
病毒检测:把病毒序列(长度m)和患者DNA序列都转换成字符串,进行模式匹配。
其中由于病毒的RNA是环状结构,可以把病毒序列串复制然后加在一起(长度2m),一起进行模式匹配。
病毒序列可以不断的往后移一位,循环m次。
树的定义:
(3).基本术语
根结点:非空树中无前驱结点的结点(只有一个)
度为 0
(无分支)的结点(叶子)
:终端结点
度不为 0 的分支结点(根结点以外的分支结点)
:内部结点(非终端结点)
树的深度
:树中结点的最大层次。树的高度:
从该结点到各叶结点的最长路径的长度。(指的是根结点的高度)孩子
,该结点称为孩子的双亲
(最左边为第一个孩子)
线性结构 | 线性结构 | 树结构 | 树结构 |
---|---|---|---|
第一个数据元素 | 无前驱 | 根节点(只有一个) | 无双亲 |
最后一个数据元素 | 无后继 | 叶子结点(可以有多个) | 无孩子 |
其他数据元素 | 一个前驱,一个后继 | 其他结点–中间结点 | 一个双亲,一个孩子 |
二叉树不是树的特殊情况,它们是两个概念
(5).性质和存储结构
至多
有2^(i-1)个结点(i>=1)。
特殊二叉树
(6).遍历二叉树
(从最上面的根节点开始遍历)
(从最低的左子树开始遍历)
(从最低的左子树开始遍历)
若二叉树中各个结点的值均不同,则二叉树结点的先序遍历、中序遍历和后序遍历都是唯一的。
由二叉树的先序遍历和中序遍历,或由二叉树的后序遍历和中序遍历可以确定唯一一棵二叉树(先序和后序是不可以获取而二叉树)。
递归算法:
#include
#include
/* 二叉链表式存储结构 */
typedef struct BiTree *btlink;
typedef struct BiTree
{
btlink rchild; //右孩子
TElemType data; //
btlink lchild; //左孩子
} BiNode;
//先序遍历
int PreOrderTravel(btlink T)
{
if (T == NULL)
{
return 1;
}
else
{
printf("%d\t", T->data); // // visit(T); //根结点
PreOrderTravel(T->lchild); //左子树
PreOrderTravel(T->rchild); //右子树
}
}
//中序遍历
int InOrderTravel(btlink T)
{
if (T == NULL)
{
return 1;
}
else
{
modelOrderTravel(T->lchild);
printf("%d\t", T->data); // visit(T); //根结点
modelOrderTravel(T->rchild);
}
}
//后序遍历
int PostOrderTravel(btlink T)
{
if (T == NULL)
{
return 1;
}
else
{
OrOrderTravel(T->lchild);
OrOrderTravel(T->rchild);
printf("%d\t", T->data); // visit(T); //根结点
}
}
算法分析
一
次经过时访问 = 先序
遍历二
次经过时访问 = 中序
遍历三
次经过时访问 = 后序
遍历非递归算法:
中序遍历
基本思想:
//中序遍历
void InOrderTraverse(btlink T)
{
LinkStack S = NewStack();//栈的开辟空间
btlink p;//新建一个空树
InitStack(S);//初始化
btlink q;//出栈的数据元素;
p = T;
while (p || !StackEmpty(S))
{
if (p)
{
push(S, p);//入栈
p = p->lchild;//遍历左子树
}
else
{
push(S, q);//出栈
printf("%d\t", q->data);//遍历根结点
p = q->rchild;//遍历=右子树
}
/* code */
}
}
定义
思路
使用一个队列
#include
#include
#define MAXSIZE 100
/* 二叉链表式存储结构 */
typedef struct BiTree *btlink;
typedef struct BiTree
{
btlink rchild; //右孩子
TElemType data; //
btlink lchild; //左孩子
} BiNode;
typedef SqQueue *qlink;
typedef struct
{
BiNode data[MAXSIZE]; //存放元素
int front, rear; //队头队尾
} SqQueue; //顺序循环队列类型
//初始化
btlink NewBNode()
{
return (btlink)malloc(sizeof(BiNode));
}
/* 层次遍历 */
void LevelOrder(btlink T)
{
btlink p;//新的空的树
qlink qu;//一个空队列
InitQueue(qu);//初始化队列
enQueue(qu, T); //根结点入队
while (!QueueEmpty(qu))//队列是否为空
{
deQueue(qu, p);//出队根结点
if (p->lchild != NULL)
{
enQueue(qu, p->lchild);//左子树入队
}
if (p->rchild != NULL)
{
enQueue(qu, p->rchild);//右子树入队
}
}
}
指向其前驱
;如果某一结点的右孩子为空,则将空的右孩子指针域为指向其后继
。ltag = 0
lchild指向该结点的左孩子
rtag = 0
rchild指向该结点的右孩子
森林:是m(m>=0) 棵互不相交的树的集合。
双亲表示法
孩子链表
把每一个结点排列起来,看成是一个线性表,用单链表存储,则n个结点有n个孩子链表(叶子的孩子链表为空表)。
而n个头指针又组成一个线性表,用顺序表(含n个元素的结构数组)存储。
特点:找孩子容易,找双亲难
带双亲的孩子链表
孩子兄弟表示法(二叉树表示法、二叉链表表示法)
由于树和二叉树都可以用二叉链表作存储结构,则以二叉链表作媒介可以导出树与二叉树之间的对应关系
加线:在兄弟结点之间加一连线
抹线:对每一个结点,除了其左孩子外,去除其与其余孩子之间的关系
旋转:以树的根结点为轴心,将整树顺时针转45°
兄弟相连留长子
加线:若p结点是双亲结点的左孩子,则将p结点的右孩子,右孩子,右孩子。。。。。沿分支找到的所有的右孩子,都与p的双亲用线连起来
抹线:抹掉原二叉树中双亲与右孩子之间的连线
调整:将结点按层次排列,形成树状结构
左孩右右练双亲,去掉原来孩子线。
先(根)序遍历
后(根)序遍历
层次遍历
路径:从树中的一个节点到另一个结点之间的分支构成这两个结点间的路径。
结点的路径长度:两结点间路径上的分支数。
树的路径长度:从树到每一个结点的路径长度之和。记作:TL
根结点
到该结点
之间的路径长度
与该结点的权
的乘积
。叶子结点
的带权路径长度之和
。贪心算法:构造哈夫曼树时首先选择权值小的叶子结点。
构造森林全是根
)。选用两小造新树
)删除两小添新人
)重复2、3剩单根
)哈夫曼树中的结点只有度为 0 或 2 ,没有度为 1 的结点。
包含n棵树的森林要经过 n-1 次合并才能形成哈夫曼树,共产生 n-1 个新结点
(包含 n 个叶子结点的哈夫曼树中共有 2n-1 个结点)。
算法实现
采取顺序存储结构------一维结构数组
结点类型定义
typedef struct htnode
{
int weight;
int parent,lch,rch;
/* data */
}HTNode,;
//初始化
void initTreeNode(HuffmanTree HT, int count)
{
int m, i, cin = 1;
if (count <= 1)
{
return;
}
m = 2 * count - 1; //总的数组长度
HT[m];
//初始化为全部根结点的森林
for (i = 1; i <= m; i++)
{
HT[i].lch = 0;
HT[i].rch = 0;
HT[i].parent = 0;
/* code */
}
for (i = 1; i <= m; i++)
{
cin >> HT[i].weight; //注入权重
// HT[i].weight = cin;
}
for (i = n + 1; i < = m; i++)
{
int s1, s2;
Select(HT, i - 1, s1, s2);
HT[s1].parent = i;
HT[s2].parent = i; //从表中删除s1,s2
HT[i].lch = s1;
HT[i].rch = s2; //s1,s2分别为左右孩子
HT[i].weight = HT[s1].weight + HT[s2].weight; //i的权值为左右孩子之和
/* code */
}
}
```
*在远程通讯中,要将待传字符转换成由二进制的字符串:
*设要传送的字符为:
ABACCDA 若编码为:A—00 00010010101100
B—01
C—10
D—11
若将编码设计为长度不等的二进制编码,即让待传字符串中出现次数较多的字符采用尽可能短的编码,则转换的二进制字符串便可能减少。
*设要传送的字符为:
ABACCDA *若编码为: A—0 000011010
B—00
C—1
D—01
重码 0000
={ AAAA,ABA,BB}
关键:要设计长度不等的编码,则必须使任一字符的编码都不是另一个字符的编码的前缀——————前缀编码
哈夫曼编码:
因为
没有一片树叶是另一片树叶的祖先,所以每个叶结点的编码就不可能是其它叶结点编码的前缀。因为
哈夫曼树的带权路径长度最短,故字符编码的总长最短。文件的编码和解码
二叉树算法的应用
按先序遍历序列建立二叉树
从键盘输入二叉树的结点信息,建立二叉树存储结构
在建立二叉树的过程中按二叉树先序方式建立
#include
#include
#define MAXSIZE 100
/* 二叉树顺序存储结构 */
typedef int TElemType;
typedef TElemType SqBitree[MAXSIZE];
SqBitree bt;
/* 二叉链表式存储结构 */
typedef struct BiTree *btlink;
typedef struct BiTree
{
btlink rchild; //右孩子
TElemType data; //
btlink lchild; //左孩子
} BiNode;
//初始化
btlink NewBNode()
{
return (btlink)malloc(sizeof(BiNode));
}
/* 创建中序二叉树*/
void CreateInBritree(btlink T)
{
char ch;
scanf("%c", &ch);
if (ch == '#')
{
T = NULL;
}
else
{
if (!(T = NewBNode()))
{
exit(1);
}
else
{
T->data = ch; //生成根结点
CreateBritree(T->lchild); //构建左子树
CreateBritree(T->rchild); //构建右子树
}
}
}
复制二叉树
遍历的树是否为空,如果为空,递归结束
otherwise ,申请新的结点空间,复制根结点
#include
#include
#define MAXSIZE 100
/* 二叉树顺序存储结构 */
typedef int TElemType;
typedef TElemType SqBitree[MAXSIZE];
SqBitree bt;
/* 二叉链表式存储结构 */
typedef struct BiTree *btlink;
typedef struct BiTree
{
btlink rchild; //右孩子
TElemType data; //
btlink lchild; //左孩子
} BiNode;
//初始化
btlink NewBNode()
{
return (btlink)malloc(sizeof(BiNode));
}
/* 复制二叉树 */
int CopyBiTree(btlink T,btlink NewT)
{
if(T == NULL)
{
NewT = NULL;
return 0;
}
else
{
NewT = NewBNode();
NewT->data = T->data;
CopyBiTree(T->lchild,NewT->lchild);
CopyBiTree(T->rchild,NewT->rchild);
}
}
计算二叉树的深度
如果是否为空,是则深度为0;
否则,递归计算左子树的深度记为m,递归计算右子树的深度记为n,二叉树的深度则为m与n的较大者加1.
/* 计算二叉树的深度 */
int Depth(btlink T)
{
int m, n;
if (T == NULL)
{
return 0;
}
else
{
m = Depth(T->lchild);
n = Depth(T->rchild);
if (m > n)
{
return (m + 1);
}
else
{
return (n + 1);
}
}
}
计算二叉树结点的总数
如果是否为空,则结点个数为0;
否则,结点个数等于左子树的结点加上右子树的结点再+1
/* 计算二叉树结点的总数 */
int NodeCount(btlink T)
{
if (T == NULL)
{
return 0;
}
else
{
return NodeCount(T->lchild) + NodeCount(T->rchild) + 1;
}
}
计算叶子结点的总数
如果为空,则叶子结点个数为0;
否则,为左子树的叶子结点个数+右子树的叶子结点个数
/* 叶子结点树 */
int LeadCount(btlink T)
{
if (T == NULL)
{
return 0;
}
if (T->lchild == NULL && T->rchild == NULL)
{
return 1;
}
else
{
return LeadCount(T->lchild)+LeadCount(T->rchild);//统计左子树和右子树的叶子节点的总数
}
}
邻接矩阵
)邻接表
建立一个顶点表(记录各个顶点信息)和一个邻接矩阵(表示各个顶点之间的关系),
设图A = (V,E)有n个顶点,则:
顶点表Vesx[n] 代表图中的每一个顶点,下标表示顶点在数组的位置
图的邻接矩阵是一个二维数组A.arcs[n] [n],定义为如果存在∈E 或者(i,j)数以E
则A.arcs[i] [j] = 1,否则 A.arcs[i] [j] = 0 (i,j 体现为顶点表中顶点的下标位置,表明哪两个顶点有连接)。
无向图,则两个顶点下标都要标上 A.arcs[i] [j] = 1
无向图的邻接矩阵是对称的;
有向图
以行为始发点的顶点,如果存在与其他顶点存在关系,就为1.{以第i行含义:以结点Vi为尾的弧(即出度边)}(发出弧)
列记录终点边的顶点,如果存在与其他顶点存在关系,就为1。{第i列的含义:以结点Vi为头的弧(即入度边)}(接收弧)
有向图的邻接矩阵可能是不对称的
顶点的出度 = 第i行元素之和
顶点的入度 = 第i列元素之和
顶点的度 = 顶点的出度 + 顶点的出度
网(有权图)
存储形式
用两个数组分别表示存储顶点表和邻接矩阵
采用邻接矩阵表示法创建无向网
输入总的顶点数和边数
依次输入点的信息存入顶点表中。
初始化邻接矩阵,使每一个权值初始化为最大值。
构造邻接矩阵
typedef int Status;
#define OK 1
#define ERROR 0
#define MAXInt 32767 //初始化表示权值的最大值
#define MVNnum 100 //最大顶点数
typedef char VerTexType; //设顶点的数据类型为字符型
typedef int ArcType; //假设边的权值为整数类型
int LocateVex(AMGrapth G,VerTexType u);
/* 邻接矩阵 */
typedef struct amgrapth *amglink;
typedef struct amgrapth
{
VerTexType vexs[MVNnum]; //顶点表
ArcType arcs[MVNnum][MVNnum]; //邻接矩阵
int vertexnum, arcnum; //图当前的顶点数和边数
} AMGrapth;
//初始化
Status CtreatUDN(AMGrapth G, int size)
{
//无向网,
int i, j,k;
G.arcnum = G.vertexnum = size; //定义总顶点数和边数
for (i = 0; i < G.vertexnum; ++i)
{
G.vexs[i] = size >> i; //依次输入顶点的信息
}
/* i 为行 j 为列 G.arcs[行][列] */
for (i = 0; i < G.vertexnum; i++)
{
for (j = 0; j < G.vertexnum; ++j)
{
G.arcs[i][j] = MAXInt; //定义边的权值为最大值 如果是无向图,权值为都0
}
}
for ( k = 0; k < G.arcnum; ++k)
{
size = size >> k;
v1 = v2 = w = size;//输入一条边所依附的顶点及边的权值 如果是无向图, 权值都为 1
i = LocateVex(G,V1);
j = LocateVex(G,v2);//确定v1和v2在G中的位置
G.arcs[i][j] = w;//边的权值为w
G.arcs[j][i] = G.arcs[i][j];//置的对称边的权值为w 这一条代码去掉就是有向图的初始化
}
return OK;
}
int LocateVex(AMGrapth G,VerTexType u)
{
int i;
for ( i = 0; i < G.vertexnum; ++i)
{
if(u == G.vexs[i]) return i;
}
return -1;
}
优点:
缺点
顶点:按编号顺序将顶点存储在一维数组中[头结点:data(数据域);firstarc(指针边结点的地址)];
关联同一顶点的边(以顶点为尾的弧):用线性链表存储{表结点:adjvex(邻接点域,存放与Vi邻接的顶点在表头数组中的位置。);nextrac(链域,下一个边结点);info(存放权值或者其他信息)}
特点
有向图
存储形式
输入总边数和总顶点数
建立顶点表
创建链表
/* 邻接表 */
//无向图
int GnLocateVex(ALGraph G, VerTexType u);
typedef struct arcnode *arclink;
typedef struct arcnode
{
int adjvex; //该边所指向的顶点的位置
arclink nextrac; //指向下一条边的指针
char info; //和边相关的信息
} ArcNode;
typedef struct gnode
{
VerTexType data; //顶点表 顶点值
arclink firstarc; //指向第一条依附该顶点的边的指针
} GNode, AdjList[MVNnum]; //adjList表示邻接表的类型
//AdjList a ==== GNode v[MVNum]
typedef struct algraph
{
AdjList vertices; //vertices -- VerTex的复数
int vertexnum, arcnum; //图当前的顶点数和边数
} ALGraph;
//初始化
Status initALGraph(ALGraph G, int size)
{
int i, j, k;
G.arcnum = G.vertexnum = size; //输入总边数和总顶点数
for (i = 0; i < G.vertexnum; ++i)
{
G.vertices[i].data = size + i; //输入顶点值
G.vertices->firstarc = NULL; //初始化表头结点的指针域为null
}
for (k = 0; k < G.arcnum; ++k) //输入各边,构造邻接表
{
cin >> v1 >> v2; //输入一条边依附的两个顶点
i = GnLocateVex(G, v1); //获取对应的v1的下标
j = GnLocateVex(G, v2); //获取v2对应的下标
arclink p1; //生成一个新的边结点p1
p1->adjvex = j; //邻接点序号为j
p1->nextrac = G.vertices[i].firstarc;
G.vertices[i].firstarc = p1; //将新结点p1插入到vi的边表头部
arclink p2; //生成一个新的边结点p2
p2->adjvex = i; //邻接点序号为i
p2->nextrac = G.vertices[j].firstarc;
G.vertices[j].firstarc = p2; //将新结点p2插入到vj的边表头部
}
return OK;
}
int GnLocateVex(ALGraph G, VerTexType u)
{
int i;
for (i = 0; i < G.vertexnum; ++i)
{
if (u == G.vertices[i])
return i;
}
return -1;
}
存储特点
十字链表(Orthogonal List)是有向图的另一种链式存储结构。
我们也可以把它看成是将有向图的邻接表和逆邻接表结合起来形成的一种链表。
有向图中的每一条弧对应十字链表中的一个弧结点,同时有向图中的每个顶点在十字链表中对应有一个结点,叫做顶点结点。
顶点结点:data(数据域);firstin(第一条入弧);firstout(第一条出弧)。
弧结点:tailvex(弧尾位置);headvex(弧头位置,下一个边结点);hlink(弧头相同的下一条弧);tlink(弧尾相同的下一个弧)。
从已给的连通图中某一顶点出发,沿着一些边访问图中所有的顶点,且使每一个顶点仅被访问一次,就叫作图的遍历,它是图的基本运算
实质:找每一个顶点的邻接点的过程
深度优先搜索(Depth_First Search------DFS)(连通图的深度优先遍历类似于树的先序遍历)
步骤:
邻接矩阵(实现图的深度遍历–无向图)
算法
//邻接矩阵图的深度优先遍历实现方法
void DFS(AMGrapth G, int v)
{
int visited[MAXInt];
int count, i;
count << v;
visited[v] = true; //访问第v个顶点
for (i = 0; i < G.vertexnum; ++i) //依次检查邻接矩阵v所在的行
{
if ((G.arcs[v][i] != 0) && (!visited[i]))
{
DFS(G, i);
//i是v的邻接点,如果i未访问,则递归调用dfs
}
}
}
算法效率:
结论:
非连通图的遍历
广度优先搜索(Breadth_First Search-------BFS)
从图的某一个顶点出发,首先依次访问该结点的所有邻接点。再按这些顶点被先后访问的先后次序依次访问与它们相邻接的所有未被访问的结点
邻接表(实现图的广度优先)
算法
void BFS(ALGraph G, int v)
{
int visited[MAXInt];
int cout, w;
cout << v;
visited[v] = true; //访问第v个顶点
InitQueue(Q); //辅助队列Q初始化,置空
EnQueue(Q, v); //v进队
while (!QueueEmpty(Q)) //队列非空
{
DeQueue(Q, u); //队头元素出队并置为u
for (w = FirstAdjVex(G, u); w >= 0; w = NexrAdjVex(G, u, w))
{
if ((!visited[w])) //w为u的尚未访问的邻接顶点
{
cout << w;
visited[w] = true;
EnQueue(Q, w); //w进队
}
/* code */
}
/* code */
}
}
算法效率
DFS和BFS算法效率的比较
给定一个无向网络,在该网所有生成树中,使得各边权值之和最小的那棵生成树称为该网最小生成树,也叫最小代价生成树。
MST性质
:设N = (V,E)是一个连通网,U是顶点集V的一个非空子集。
若边(u,v)是一条具有最小权值的边,其中u∈U,v∈V-U,则必存在一棵包含边(u,v)的最小生成树
普利姆算法:
克鲁斯卡尔算法:
最小生成树不是唯一的
两种算法比较
算法名 | 普利姆算法 | 克鲁斯卡尔算法 |
---|---|---|
算法思想 | 选择点 | 选择边 |
时间复杂度 | O(n^2) (n为顶点数) | O(eloge) (e为边数) |
适应范围 | 稠密图 | 稀疏图 |
有向网
中A点(源点
)到达B点(终点
)的多条路径中,寻找一条各边权值之和最小
的路径,即最短路径
。
有向无环图:无环的有向图,简称DAG图
有向无环图通常用来描述一个工程或者一个系统的进行过程。(通常把计划、施工、生产、程序流程等当成是一个工程)
一个工程可以分为若干子工程,只要完成了这些子工程(活动),就可以导致整个工程的完成
AOV网(拓扑排序):
定义:
方法:
应用:
AOE网(关键路径):
用一个有向图表示一个工程的各子工程及其相互制约的关系,其中以弧表示活动,以顶点表示活动之间的优先制约关系,称这种有向图为边表示活动的网,简称AOE网
关键路径
确定关键路径
关建活动:关键路径上的活动,即l(i) == e(i),(即l(i)-e(i) == 0)的活动。
如何找(i) == e(i)的关键活动?
(1) e(i) = ve(j)
(2) l(i) =vl(k) -Wj,k
讨论:
关键路径的讨论
1、若网中有几条关键路径,则需加快同时在几条关键路径上的关键活动。
2、如果一个活动处于所有的关键路径上,那么提高这个活动的速度,就能缩短整个工程的完成时间。如:a1、a4
3、处于所有的关键路径上的活动完成时间不能缩短太多否则会使原来的关键路径变成不是关键路径。这时,必须重新寻找关键路径。如:a1由6天变成3天,就会改变关键路径。
范围:
顺序表的表示:
typedef int KeyType;
typedef struct elemtype *elink;
typedef struct elemtype
{
KeyType key; //关键字域
/* data */ //其他字域
} ElemType;
typedef struct stable
{
elink R; //表基址
int length; //表长
/* data */
} SSTable;
SSTable ST; //定义顺序表
//查找关键字
Status Search_Sep(SSTable ST, KeyType key)
{
int i;
for (i = ST.length; i >= 1; --i)
{
if (ST.R[i].key == key)
return i;
/* code */
}
return ERROR;
}
//哨兵:监视哨
Status Saerch_seq(SSTable ST, KeyType key)
{
int i;
ST.R[0].key = key;
for (i = ST.length; ST.R[i].key != key; --i);
return i;
}
时间复杂度:O(n)
空间复杂度:一个辅助空间O(1);
优点:算法简单,逻辑次序无要求,且不同存储结构均可用
缺点:ASL太长,时间效率太低
折半查找:每次将待查找记录所在区间缩小一半
/* 二分法 */
//折半算法(非递归算法)
Status Search_Bin(SSTable ST, KeyType key)
{
int low, hight, mid;
low = 0;
hight = ST.length;
while (low <= hight)
{
mid = (low + hight) / 2;
if (ST.R[mid].key == key)
return mid;
else if (ST.R[mid].key > key)
{
hight = mid - 1;
}
else
{
low = mid + 1;
}
}
return 0;
}
//递归算法
Status Search_BinDgui(SSTable ST, KeyType key, int low, int hight, int mid)
{
if (low <= hight)
return 0;
mid = (low + hight) / 2;
if (ST.R[mid].key == key)
return mid;
else if (ST.R[mid].key > key)
{
hight = mid + 1;
Search_Bin(ST, key, low, hight, mid);
}
else
{
low = mid + 1;
Search_Bin(ST, key, low, hight, mid);
}
}
时间复杂度O(n/2):
查找成功:
查找失败:
空间复杂度:
平均查找长度ASL:
优点:效率比顺序查找高
缺点:只适用于有序表,且限于顺序存储结构(对线性链式表无效)
顺序查找 | 折半查找 | 分块查找 | |
---|---|---|---|
ASL | 最大 | 最小 | 中间 |
表结构 | 有序表,序表 | 有序表 | 分块有序 |
存储结构 | 顺序表,线性链表 | 顺序表 | 顺序表,线性链表 |
定义:又称二叉搜索树,二叉查找树
二叉排序树或者是空树,或满足如下性质的二叉树:
性质:
中序遍历非空二叉排序树所得到的数据元素序列是一个按关键字排序的递增有序序列。
查找
查找算法
#include
#include
typedef int Status;
#define OK 1
#define ERROR 0
#define true 1
#define false 0
typedef int KeyType;
typedef struct InfoType
{
int info;
/* data */
} InfoType;
typedef struct elemtype
{
KeyType key; //关键字项
InfoType otherinfo; //其他数据域
/* data */
} ElemType;
typedef struct bstnode *bstlink;
typedef struct bstnode
{
ElemType data; //数据域
bstlink lchild; //左孩子指针
bstlink rchild; //右孩子指针
/* data */
} BSTNode;
bstlink SearchBST(bstlink T, KeyType key)
{
if ((!T) || key == T->data.key)
{
return T;
}
else if (key < T->data.key)
{
return SearchBST(T->lchild, key);
}
else
{
return SearchBST(T->rchild, key);
}
}
【算法思想】
若二叉排序树为空,则查找失败,返回空指针。
若二叉排序树非空,将给定值key与根结点的关键字
T—>data.key进行比较:
二叉排序树上查找某关键字等于给定值的结点过程,其实就是走了一条从根到该结点的路径
二叉排序树的平均查找长度
插入
若二叉排序树为空,则插入结点作为根结点插入到空树中否则,继续在其左、右子树上查找
生成
删除
定义
平衡因子
为了方便起见,给每个结点附加一个数字,给出该结点左子树右子树的高度差。这个数字称为结点的平衡因子(BF)。
平衡因子 = 结点左子树的高度 — 结点右子树的高度
根据平衡二叉树的定义,平衡二叉树上所有结点的平衡因子只能是 -1、0 和 1
对于一棵有n个结点的AVL树,其高度保持在O(log₂n)的数量级,ASL也保持在O(log₂n)量级
调整平衡二叉树
当我们在一棵平衡二叉排序树上插入一个结点时,可能导致失衡,即出现平衡因子绝对值大于1的结点
平衡调整四种类型
调整
记录的存储位置与关键字之间存在对应关系
对应关系 ------hash函数
Loc(i) = H(keyi)
构造好散列函数
制定一个好的解决冲突的方案
根据元素集合的特性构造
构造方法类型
p <= m 且为质数
散列表技术具有很好的平均性能,优于一些传统的技术
链地址法优于开地址法
除留余数法作散列表优于其它类型函数
序树为空,则插入结点作为根结点插入到空树中否则,继续在其左、右子树上查找
生成
删除
定义
平衡因子
为了方便起见,给每个结点附加一个数字,给出该结点左子树右子树的高度差。这个数字称为结点的平衡因子(BF)。
平衡因子 = 结点左子树的高度 — 结点右子树的高度
根据平衡二叉树的定义,平衡二叉树上所有结点的平衡因子只能是 -1、0 和 1
对于一棵有n个结点的AVL树,其高度保持在O(log₂n)的数量级,ASL也保持在O(log₂n)量级
调整平衡二叉树
当我们在一棵平衡二叉排序树上插入一个结点时,可能导致失衡,即出现平衡因子绝对值大于1的结点
平衡调整四种类型
调整
记录的存储位置与关键字之间存在对应关系
对应关系 ------hash函数
Loc(i) = H(keyi)
构造好散列函数
制定一个好的解决冲突的方案
根据元素集合的特性构造
构造方法类型
p <= m 且为质数