数据结构C语言版本(中)

简介: 数据结构C语言版本(中)

第四章 串

串:限定数据元素类型的线性表。

应用实例:

编辑软件(本质上是字符串处理)

信息检索、病毒查找(字符串比较)

第一节 逻辑结构


一、定义

串是由字符组成的线性表。

STRING=(D,S,P)

D = {ai| ai∈CHARACTER(字符集), i=0,1,2,…,n-1}

ASCII码串: CHARACTER为ASCII码字符集。

位串:CHARACTER={0,1}

S = {<ai-1,ai>| ai-1,ai∈D, i=1,2,…,n-1}

"a0a1……an-1"的长度为n。

空串:长度为0。

子串:串中任意个连续字符组成的子序列。

两串相等:两串长度相等,对应的每个字符相等。

例:根据字符串比较运算的定义,完善该函数:

int strcmp(char s[ ], char t[ ])

{ int i;

for (i=0; s[i] && t[i]; i++)

if (s[i]!=t[i]) ;

; }


二、基本运算

6730d2cccc7d43f7beaff43e06bebb14.png

赋值 StrAssign(&S,T)

判断 StrEmpty(S) StrComp(S,T)

求长度 StrLen(S)

联接 Concat(&T,S1,S2)

求子串 SubStr(&Sub,S,pos,len)

子串定位Index(S,T,pos)

替换 Replace(&S,T,V)

插入 StrInsert(&S,pos,T)

删除 StrDelete(&S,pos,len)

释放 DestroyString(&S)


第二节 存储结构

一、顺序存储

串中相邻的字符存放在内存的相邻单元中。

560af4054a9e46fa8f7afa76653cb98e.pnge81cfcbf49594d7da2e32e4ced10591c.png


优点:访问子串方便,

缺点:空间大小不灵活,插入、删除费时。


二、链式存储

如何设置结点的大小?

一个结点存储1个字符:非紧缩格式。

空间浪费,操作简单。

一个结点存储多个字符:紧缩格式。

空间效率高,操作复杂。

b45ece852abf4180bca5f11d01d17841.png

如何在紧缩格式的链串中插入、删除?


第三节 模式匹配

模式匹配Index(S, T, pos)

主串S中寻找模式串T的起始位置。

pos是主串中进行模式匹配的起始位置。

Index(“abcdef”,“cde”):2

Index(“abcdef”,“ab”) :0

Index(“abcdef”,“ad”) :-1


一、BF算法(Brute-Force)

主 串S=“s0s1…sm-1”

模式串T=“t0t1…tn-1”

匹配过程:

e507f6d96c9b4bfaa21ad457ed4f3102.png

匹配结果:

91bffdf8d82a492bbb5f854b876d2e9d.png

int index_BF(char s[],char t[])

{ int i=0,j=0;

while(s[i] && t[j])

if(s[i]==t[j]) { i++; j++; }

else { i=i-j+1; j=0; }

if(t[j]==0) return(i-j);

else return(-1);

}

问题分析:

S=“aaaaaaaaaaaaaaab” 长度为m

T=“aaaab” 长度为n

回溯次数=m-n

最差时间复杂度:O((m-n)n) ≈ O(nm)

再例:S=“abcabcabcde”

T=“abcabcd”


二、KMP算法(Knuth等3人)

fa9b89b677a4423eb80a006ce32f9a9d.png


1、推导

琢磨模式串的内在规律,使得:

在si<>tj时,i不回溯,j适当回溯,再重新比较。

当si<>tj时,i不变,j=k。 求k=? (k<j)

此时有: “t0t1…tj-1”=“si-j…si-1”

61f01eefa5c34dd3aa4eae9b752939af.png

显然,k应具有性质:

“t0t1…tk-1”=“si-k…si-1”=“tj-ktj-k+1…tj-1”

即:“t0t1…tk-1”=“tj-ktj-k+1…tj-1”

即:当tj与si失配时,由于tj的前k个字符等于模式串的开头k个字符,所以可将si与tK比较。

为达到更高效率,k越大越好。(k<j)

2、next[]

每个tj与对应一个k值,所以设立next[]存储所有k值。

计算方法:

1、j=0时:next[j]=-1;

2、存在"t0t1…tk-1"=“tj-ktj-k+1…tj-1”,(k<j):

例如:“t0”=“tj-1” “t0t1”=“tj-2tj-1” “t0t1t2”=“tj-3tj-2tj-1”

next[j]=max(k)。

3、否则:next[j]=0;

3、KMP程序

//预先求出next[]

int index_kmp(char s[],char t[],int next[])

{int i=0, j=0;

while(s[i] && t[j])

if(j==-1 || s[i]==t[j]) { i++; j++; }

else j=next[j];

if(t[j]0) return(i-j);

else return(-1);

}

体会j-1

体会j=next[j]

例1: a b c a b c d

-1 0 0 0 1 2 3

a5980b60c7b147f9a43aa7c9329e77bd.png

例2: a a a a b

-1 0 1 2 3

5d27e845271342e9831aaccc616d89f1.png

例:设主串s=“abcabcabd”,模式串p=“abcabd”,按KMP算法进行模式匹配,当"s0s1s2s3s4"=“p0p1p2p3p4”,且s5≠p5时,应进行 比较。

A、s5和p2 B、s5和p3 C、s1和p0 D、s8和p5


第四节 串操作实例

一、文本编辑软件的数据结构

1、文本编辑软件

数据对象:字符串。

040ccb66b1124b02b5d3f134e5c8d550.png

基本操作:插入、删除字符。

简易文本编辑器的功能菜单:

①末尾添加/在某行前插入一行;

②删除指定行;

③在某行中插入字符串;

④在某行中删除某子串;

⑤查找/替换子串;

⑥打开/保存文件;

⑦显示文件内容(可以简化)


2、数据结构的设计

如何减少时间/空间复杂度?

方案①:整个文件是一个顺序/链式字符串。

方案②:每行作为一个顺序/链式字符串,

行表:所有行结构的头结点组成的线性表。

行表的存储结构:顺序/链式

顺序结构:字符数/行、行数有上限

链式结构:存储空间浪费严重。

紧缩格式?


二、多模式匹配问题:网络搜索引擎、病毒搜索

查找对象:一个大字符串S

若干模式串:一个字符串集合Keywords

S中存在多个包含Keywords中所有关键字的匹配子串。

目标:求出包含所有匹配子串的最短匹配子串。

上机:


1、模式匹配

例1、子串替换

main()

{ char s[M]=“abcdeabcdef”,t[M]=“bcde”;

char v[M]=“12”; // “1234” “123456” “”

substr(s,t,v); puts(s);

}

将s[]中的t[]用v[]替换

void substr(char s[],char t[],char v[])

{ int s_len,t_len,v_len,s_i,t_i,v_i,gap,i,j;

s_len=strlen(s); t_len=strlen(t); v_len=strlen(v);

gap=v_len-t_len;

s_i=0;

while( s[s_i] )

{ for(t_i=0; s[s_i] && t[t_i]; ) // 定位

if(s[s_i]t[t_i]) { s_i++; t_i++; }

else { s_i=s_i-t_i+1; t_i=0; }

if(t[t_i]‘\0’) // 替换

{ if(gap>0)  // 扩充s串

for(i=s_len-1; i>=s_i; i–) s[i+gap]=s[i];

if(gap<0)  // 缩减s串

for(i=s_i; i<=s_len-1; i++) s[i+gap]=s[i];

for(i=s_i-t_len,j=0; j<v_len; i++,j++)s[i]=v[j];

s_len=s_len+gap;

s_i=s_i+gap;

}

}

s[s_len]=‘\0’;

}

例2、在结点大小为1的链串上,实现子串定位算法。若匹配成功,则返回起始结点地址,否则返回空指针。

typedef struct lstring

{ char data;

struct lstring *next;

}LString;


LString *index_BF(LString *s, LString *t)

{ LString *sp, *tp, *sstart;

sp=s; tp=t; sstart=s;

while(sp && tp)

{ if(sp->data==tp->data)

{ sp=sp->next; tp=tp->next; }

else { sstart=sstart->next; sp=sstart; tp=t; }

}

if(!tp) return(sstart);

else return(NULL);

}


第五章 数组和广义表

继续增加线性表结构的内涵。


第一节 数组的逻辑结构

1_Array=(D, S, P) 定长线性表

D = {ai| ai∈ElemSet, i=0,1,2,…, n-1}

S = {<ai-1,ai>| ai-1,ai∈D, i=1,2,…,n-1}

2_Array=(D, S, P) 定长线性表,每个元素又是定长线性表。

2fce225c76be4b81b5631b22a7a03524.png

D = {ai,j| ai,j∈ElemSet, i=0,1,…,n1-1; j=0,1,…,n2-1}

S = {ROW,COL}

ROW={<ai,j-1, ai,j>| i=0,1,…,n1-1; j=1,…,n2-1}

COL={<ai-1,j, ai,j>| i=1,…,n1-1; j=0,1,…,n2-1}

3_Array=(D, S, P) 增加层的关系

二维数组:分量为一维数组的一维数组。

三维数组:分量为二维数组的一维数组。

数组一旦被定义,它的维数和维界就不再改变。

基本操作:取值、赋值。


第二节 数组结构的顺序存储结构

数组结构是多维的,内存地址是一维的。

二维数组a[n1][n2]的存储方式:

5574ce9399e64c848711d09872f50e0a.png

f10c321db8814ae682167ae785f4f754.png

a5446121179945b581ea19012a7bc82d.png


1、初始化数组结构

Status Array_Init(Array &A,int dim,int bounds[])

{ A.dim=dim;

A.bounds= (int )malloc(dimsizeof(int));

A.constants=(int )malloc(dimsizeof(int));

if(!A.bounds || !A.constants) return(OVERFLOW);

for(i=0;i<dim;i++) A.bounds[i]=bounds[i];

for(A.constants[dim-1]=1,i=dim-2; i>=0; i–)

A.constants[i]=A.bounds[i+1]*A.constants[i+1];

//开辟数组空间

elemtotal=A.bounds[0]*A.constants[0];

A.base=(ElemType )malloc(elemtotalsizeof(ElemType));

if(!A.base) return(OVERFLOW);

return(OK);

}


2、取值

ElemType Array_GetValue(Array A,int dim_i[])

{ int offset;

for(offset=0,i=0; i<A.dim; i++)

offset+=A.constants[i] * dim_i[i];

return(*(A.base+offset));

}


main()

{ Array A; ElemType e;

int bounds[4]={3, 4, 5, 6};

int dim[4]={2,1,3,4};

Array_Init(A, 4, bounds);

e = Array_GetValue(A,dim);

}


第三节 矩阵的压缩存储

矩阵的用途:

大型方程组求解、高次方程求解

真正有用的计算是矩阵的计算。

从空间/时间复杂度出发,讨论矩阵的存储结构。


一、特殊矩阵

利用矩阵的特征,尽量减少数据的存储量。


1、对称矩阵 aij=aji

59c8b4c3d13d4e0facf1ba5f666164b6.png

NxN矩阵的存储结构:S[N(N+1)/2]。

以行主序的方式存储下三角元素:

若i>=j: k=i*(i+1)/2+j

若i< j: k=j*(j+1)/2+i

/* 取值 /

int getValue(int i,int j)

{ if(i>=j) return(s[i(i+1)/2+j])

else return(s[j*(j+1)/2+i]);

}

/* 赋值 /

void SetValue(int i,int j,int e)

{ if(i>=j) s[i(i+1)/2+j]=e;

else s[j*(j+1)/2+i]=e;

}


2、上/下三角矩阵

类似对称矩阵。只是某个三角区域的所有元素为0,或某个固定值。

例:设N阶对称矩阵A存储为一维数组S[N(N+1)/2],以行主序的方式存储A的下三角,则元素A[3][5]的存储位置是?


3、小结

练习的意义:培养摸索数据规律的感觉。


二、稀疏矩阵之三元组表

稀疏因子=t/(m*n)

稀疏矩阵:稀疏因子<=0.05的大型矩阵。(非0数很少,且无规律)


1、三元组的顺序表

f04b2d98e8934a5391651f52a1ff538b.png

约定:①三元组中的e必须不等于0。

②非0元素按行主序排列。

优点:节省空间。

缺点:按行号、列号存取元素,费时。


2、转置运算 M=>T (以T为中心,按需点菜)

faf5f165e4794b2787d3375ae04ee0c5.png

f4a1dfe6642f4d90a2a8e408ef65fa6b.png

c7afe004118c4096b500dc51f5684831.png

void TSMatrix_Transpose(TSMatrix M,TSMatrix &T)

{ int k,col,i;

T.mu=M.nu; T.nu=M.mu; T.tu=M.tu;

for(k=0,col=0; col<M.nu; col++) //找M中所有列

for(i=0; i<M.tu; i++)

if(M.data[i].j==col)

{ T.data[k].i=M.data[i].j;

T.data[k].j=M.data[i].i;

T.data[k].e=M.data[i].e; k++;

}

}

时间复杂度:

若M有n列,有t个非0数:为O(nt)。

若t≈mn: 为O(nmn)。


3、快速转置运算 M=>T (以M为中心,按位就座)

辅助数据结构:

num[]:M中每列(T中每行)的三元组个数。

=》pos[]:T中每行第一个三元组的下标。

pos[0]=0;

pos[j]=pos[j-1]+num[j-1]; j∈[1,M.nu-1]

ebb1aacf0f0946c79cd3ca8c01289e3c.png

905aa542635c476f9ac637bab2f6560d.png

void TSMatrix_FastTranspose(TSMatrix M,TSMatrix &T)

{ int i,j,k,num[MAX],pos[MAX];

T.mu=M.nu; T.nu=M.mu; T.tu=M.tu;

//统计M中每列的三元组个数

for(i=0;i<M.nu;i++) num[i]=0;

for(i=0;i<M.tu;i++) num[M.data[i].j]++;

//计算pos数组

for(pos[0]=0,i=1;i<M.nu;i++)

pos[i]=pos[i-1]+num[i-1];

//快速转置

for(i=0;i<M.tu;i++)

{ col=M.data[i].j; loc=pos[col];

T.data[loc].i=M.data[i].j;

T.data[loc].j=M.data[i].i;

T.data[loc].e=M.data[i].e; pos[col]++;

}

}

时间复杂度:

若M有n列,有t个非0数:循环次数:n+t+n+t

若t≈mn: 为O(mn)。

空间复杂度:2个数组(n+n个单元),O(n)。

人生的启迪:

了解、把握已有的财富,比眼望着目标更有效!


4、三元组表小结

3b990de761854e14bf5305968bf65fd7.png

如何快速定位呢?==>十字链表。


三、稀疏矩阵之十字链表

十字链表的存储结构:真正等价于矩阵的逻辑结构。


1、十字链表

597ab58f78bc41f9960177ad7db3ddc8.png

图示:4x5矩阵的十字链表,含非0数:

(1,1:1) (3,1:2) (3,4:3)

用right构成行链表,用down构成列链表。

da70c8eab306477b91f2c9710f124edc.png


2、建立、打印十字链表的算法

按行主序打印矩阵M

void CrossList_Print(CrossList M)

{ OLNODE *p; int i;

printf(“%d,%d,%d\n”,M.mu,M.nu,M.tu);

for(i=0; i<M.mu; i++)

for(p=M.rhead[i].right; p!=NULL; p=p->right)

printf(“%d,%d:%d\n”,p->i,p->j,p->e);

}

根据三元组顺序表T,建立十字链表M

void CrossList_Create(CrossList &M, TSMatrix T)

{ int i;

CrossList_Init(M,T.mu,T.nu);

for(i=0; i<T.tu; i++) // 插入各个三元组结点

CrossList_Insert(M, T.data[i]);

}

初始化十字链表结构M

void CrossList_Init(CrossList &M,int mu,int nu)

{ int i;

M.mu=mu; M.nu=nu; M.tu=0;

//初始化行头结点表、列头结点表

M.rhead=(OLNODE )malloc(M.musizeof(OLNODE));

for(i=0; i<M.mu; i++) M.rhead[i].right=NULL;

M.chead=(OLNODE )malloc(M.nusizeof(OLNODE))

for(i=0; i<M.nu; i++) M.chead[i].down=NULL;

}

根据三元组Item,新建结点,并插入十字链表M中

void CrossList_Insert(CrossList &M, Triple Item)

{ OLNODE *newp,*p,pre;

// 插入行链表中

for(pre=&M.rhead[Item.i],p=pre->right;p; pre=p,p=p->right)

if(p->j >= Item.j) break; // 定位

if(pNULL || p->j > Item.j)

{ newp=(OLNODE *)malloc(sizeof(OLNODE));

newp->i=Item.i; newp->j=Item.j; newp->e=Item.e;

newp->right=p; pre->right=newp; M.tu++;

}

else // 遇到同类项

{ p->e+=Item.e;

if(p->e0) pre->right=p->right;

}

// 将new结点插入列链表中

for(pre=&M.chead[Item.j],p=pre->down; p; pre=p,p=p->down)

if(p->i >= Item.i) break; // 定位

if(pNULL || p->i > Item.i)

{ newp->down=p; pre->down=newp; }

else // 遇到同类项

if(p->e0) { pre->down=p->down; free§; M.tu–}

}


第四节 广义表的逻辑结构

一、定义

线性表的扩展,每个元素的结构不等。

GList={a1,a2,…,an | ai∈AtomSet或ai∈GList}

ai∈AtomSet:ai为原子

ai∈Glist :ai为子表

867dff20e99e4d4fb3ec5c337b4629f3.png

数据关系:顺序关系、层次关系。

a1为表头(head)元素, 其余为表尾(tail)。

应用实例:Lisp语言、前缀表达式

(setq x (+ 4 (- a b)) y (+ 3 4) )

表头:函数名; 表尾:参数表


二、基本操作

求长度:GList_Length(L)

求深度: GList_Depth(L) 表的最大层次数

原子深度=0,表深度=max{GList_Depth(ai)}+1

例:求长度、深度:

(a), (), ((a),a), (((a),a),(a),a)

遍历表:Glist_Traverse(L)

复制表:Glist_Copy(L,&T)

取表头:GList_Head(L)

取表尾:GList_Tail(L)

取第i个元素:GList_nth(L,i)


第五节 广义表的存储结构

一、不规范的结构图

9a3236f4a1bd40a19ff9a8ac698b0f3d.png


二、广义表的存储结构

二种结点:表结点、原子结点

f51e59d624ce4481a6462c6acc73a777.png

结构约定:所有广义表带特殊头结点(相当于括号)。

能简化算法。

绘制存储结构:

ce3d864c4d1540799fd400a66b58044e.png


第六节 m元多项式的存储

43fa2a5f25e646089c7621684150f8c1.png


绘制存储结构

F(x,y,z)=((x12+2x6)y3+(3x15)y2)z20 + ((x18+6x3)y4+2y)z10 + 15

第七节 广义表的递归算法

typedef char AtomType;

typedef enum {ATOM,LIST} ElemTag;

typedef struct GLNode

{ ElemTag tag;

union

{ AtomType atom;

struct GLNode *child;

}UNION;

struct GLNode *next;

}GLNODE;


一、遍历广义表,打印层次括号

void GList_Traverse(GLNODE *L)

{ GLNODE *p;

putch(‘(’);

for(p=L->UNION.child; p; p=p->next)

if(p->tag==ATOM) printf(“%c “,p->UNION.atom);

else // 特殊头结点等价于”( )”

{ putch(‘(’);

GList_Traverse(p->UNION.child);

putch(‘)’);

}

putch(‘)’);

}


二、求表深度

int GList_Depth(GLNODE *L)

{ GLNODE *p;

int depth1, max=0;

if(L->tag==ATOM) return(0);

for(p=L->UNION.child; p; p=p->next)

{ depth1=GList_Depth§;

if(depth1>max) max=depth1;

}

return(max+1);

}


第六章 树

第一节 树的定义

一、逻辑结构

树结构:层次关系、线性关系。

例1:资源管理器;

例2:个人照片管理;

树的逻辑结构图:

3ce1990830944fa6a03145eaebc48d63.png


通俗定义:

树是n(n>0)个结点的有限集,在任意一棵树中:
1)有且只有一个特定的根(root)结点;

2)当n>1时,其余结点可分为m(m>0)个互不相交的有限子集T1,T2…Tm,每个子集是一棵树,称为子树。

形式定义:

Tree=(D,S,P)

D={ai | ai∈ElemSet,i=1,2,…,n}

二元关系S:

当n=1时,S=φ;

当n>1时:

afe6f43e3d7c4b708dbbb0803efc92dd.png

d09aeaf95924461eb3825c804bc9e31f.png


二、术语

20e76c03b52d4c869ca4b11d51fde2f5.png


三、基本逻辑操作

c11de12ba1334b87a955ab4b90b75689.png


第二节 二叉树

一、定义

形式定义:参考树的定义。

根结点、左子树、右子树。

二叉树的逻辑结构图:

8437571bb4d0476fa49b527677ee9f1f.png

f20308f74b814b5699269f53b9f33fb6.png


二、二叉树的形态

具有3个结点的二叉树,有哪些形态?

23fdaf35306f493898aefbf18cbd8725.png

具有n个结点的二叉树,有多少种形态?

例:n个结点的相似树的个数

int TCount(int n)

{ int left,count=0;

if(n0 || n1) return(1);

for(left=n-1; left>=0; left–)

count+=TCount(left)*TCount(n-1-left);

return(count);

}

计算:TCount(5)=?


三、性质

基本常识:关系数=结点数-1


性质1:若一个二叉树,叶子结点数为n0,度为2的结点数为n2,

则n0=n2+1。

例:若一棵树中度为1的结点有N1个,度为2的结点有N2个,……,度为m的结点有Nm个,则该树的叶结点有 (N1+2N2+…+mNm+1)-(N1+N2+…+Nm) 个。


性质2:二叉树的第i层上的结点数最多为2i-1。

性质3:深度为k的二叉树中结点总数最多为2k-1。

满二叉树: 深度为K的二叉树。共有2k-1个结点。

完全二叉树:深度为K的二叉树,前k-1层是满二叉树,

第k层的结点从左至右依次排列。

8017773b50544bc3b8e80376a4555aca.pnge01a745374df4f77a293ae13378a5894.png


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

性质5:有n个结点的完全二叉树,结点从0顺序标号,则:

02764ab599ca4390b8aa8b279da4c6e9.png

1、若i>0,i的双亲结点是(i-1)/2。

2、若2i+1<n,i的左孩子是2i+1;若2i+1>=n,i无左孩子。

3、若2i+2<n,i的右孩子是2i+2;若2i+2>=n,i无右孩子。


四、存储结构

1、顺序结构

依照满二叉树的结点顺序,存放各个结点。

存储位置暗藏树的关系。

例:将68个结点的完全二叉树,按顺序存储结构存于数组A[0…100]中,叶子结点的最小编号是 。

A、32 B、33 C、34 D、35

例:具有101个结点的完全二叉树,其度为1的结点有 个。

分析:

满/完全二叉树:存储效率最高,插入、删除方便。

非完全二叉树 :

9d325d4fc5554ef1b8973fec13b96a88.png

9dda10a6f6c14eec9084685abff8b31c.png

不存在的结点,用特殊符号标识。

空间浪费大,不灵活。

退化的二叉树:

1b76ef0c0c5d4fedbb4df6022fcf6a22.png

链式结构(二叉链表结构)

64940ad2cfbd4e908e0bb57560ecbbed.png


第三节 遍历二叉树

遍历:每个结点访问且只访问一次。


一、深度遍历(递归):先序、中序、后序

二叉树 = 根结点 + 左子树 + 右子树

将树的遍历转变为子树的遍历。

9dac3e17375b428c901062728fdf24c1.png

遍历序列与二叉树不是一一对应的。

例:若前序序列为123,对应的二叉树有5种。

09c6dab446864e4bb22a9e1a611154e2.png

思考:试找出分别满足下列条件的所有二叉树

①先序序列和中序序列相同

②中序序列和后序序列相同

③先序序列和后序序列相同

void BiT_PreOrder(BiTNODE *root)

{①if(root==NULL) return;

②printf(“%d,”,root->data);

③BiT_PreOrder(root->lchild);

④BiT_PreOrder(root->rchild);

}

跟踪程序:观察调用栈Ctrl-F3。

c14727cef21b4f17828800353893e28c.png

逻辑清晰,效率不高。

时间复杂度:结点数为n, O(n)

空间复杂度:栈的深度=树的深度k, O(k)


二、层次(广度)遍历二叉树

1、算法

①将根指针加入队列;

②取队首元素,设为p;访问结点p;

③若p存在左孩子,将其加入队列;

若*p存在右孩子,将其加入队列;

④若队列不空,则循环②③,否则完成。

void BiT_TravelByLevel(BiTNODE *root)

{ BiTNODE p; Queue Q; / Q为指针队列 /

if(root==NULL) return;

Queue_Init(Q); / 初始化队列 /

Queue_Enter(Q, root); / 将root进队列 /

while(!Queue_Empty(Q))

{ p=Queue_Leave(Q); / 出队列的值赋给p /

printf(“%d,”, p->data); / 每个元素出队列一次 */

if(p->lchild) Queue_Enter(Q,p->lchild);

if(p->rchild) Queue_Enter(Q,p->rchild);

}

}

时间复杂度:结点数为n, O(n)

空间复杂度:各层结点数的最大值

辅助程序

typedef struct queuenode

{ ElemType data;

struct Node *link;

}QueueNode;

typedef struct

{ QueueNode *front,*rear; }LinkQueue


三、遍历算法的应用

1、统计结点的个数

深度遍历模式

int BiT_Count(BiTNODE *root)

{ int left,right;

if(root==NULL) return(0);

left= BiT_Count(root->lchild);

right=BiT_Count(root->rchild);

return(1+left+right);

}


统计叶子结点的个数

int BiT_Count(BiTNODE *root)

{ int left,right;

if(rootNULL) return(0);

if(root->lchildNULL && root->rchild==NULL) return(1);

left= BiT_Count(root->lchild);

right=BiT_Count(root->rchild);

return(left+right);

}


层次遍历模式

int BiT_TravelByLevel(BiTNODE *root)

{ BiTNODE *p; int count=0;

Queue Q;

Queue_Init(Q);

Queue_Enter(Q, root);

while(!Queue_Empty(Q))

{ p=Queue_Leave(Q);

count++;

if(p->lchild) Q_Enter(Q,p->lchild);

if(p->rchild) Q_Enter(Q,p->rchild);

}

return(count);

}


思考:下列二叉树计数算法是否有错?

int count(BiTNODE *root)

{ int s=0;

if(root)

{ s++;

count(t->lchild);

count(t->rchild);

return(s);

}

}


2、求二叉树的深度

int BiT_Depth(BiTNODE *root)

{ int left,right;

if(root==NULL) return(0);

left =BiT_Depth(root->lchild)

right=BiT_Depth(root->rchild))

return(max(left,right)+1);

}

比较:广义表的深度算法


3、交换树中每个结点的左右子树

深度遍历模式

void BiT_Exchange(BiTNODE *root)

{ BiTNODE *p;

if(root==NULL) return;

BiT_Exchange(root->lchild);

BiT_Exchange(root->rchild);

p=root->lchild; root->lchild=root->rchild; root->rchild=p;

}


层次遍历模式

4、释放二叉树中的所有结点空间

深度遍历模式

void BiT_Free(BiTNODE root)

{ if(root==NULL) return;

BiT_Free(root->lchild); / 释放左子树 /

BiT_Free(root->rchild); / 释放右子树 /

free(root); / 按后序次序 */

}


层次遍历模式

void BiT_TravelByLevel(BiTNODE *root)

{ BiTNODE *p;

Queue Q;

Queue_Init(Q);

Queue_Enter(Q, root);

while(!Queue_Empty(Q))

{ p=Queue_Leave(Q);

if(p->lchild) Q_Enter(Q,p->lchild);

if(p->rchild) Q_Enter(Q,p->rchild);

free§;

}

}


5、在二叉树中查找关键值为key的结点

深度遍历模式

BiTNODE *BiT_Search(BiTNODE *root, int key)

{ BiTNODE *p;

if(rootNULL) return(NULL);

if(root->datakey) return(root);

p=BiT_Search(root->lchild, key);

if§ return§;

return( BiT_Search(root->rchild, key) );

}


层次遍历模式

BiTNODE *BiT_Search(BiTNODE *root, int key)

{ BiTNODE *p;

Queue Q;

Queue_Init(Q);

Queue_Enter(Q, root);

while(!Queue_Empty(Q))

{ p=Queue_Leave(Q);

if(p->data==key) return§;

if(p->lchild) Q_Enter(Q,p->lchild);

if(p->rchild) Q_Enter(Q,p->rchild);

}

return(NULL);

}


6、带空指针标记的先序序列=>二叉树

深度遍历模式

2797420a2af34dbe9281e06242df9ddd.png

int i=0; //全局变量

main()

{ BiTNode root=BiT_Create("ABDECF**"); }


BiTNODE *BiT_Create(char s[])

{ BiTNODE root; char ch;

ch=s[i]; i++;

if(ch=='') return(NULL);

root=(BiTNODE )malloc(sizeof(BiTNODE));

root->data=ch;

root->lchild=BiT_Create(s); / 建左子树 /

root->rchild=BiT_Create(s); / 建右子树 */

return(root);

}


7、先序序列+中序序列=>二叉树

①讨论

二叉树和其遍历序列是否一一对应?

先序+中序=>二叉树

示例:先序: A B H F D E C K G

中序: H B D F A E K C G

cdb8b83fe7fb45f09d78927b800df183.png

递归问题:分解策略,最终小问题的解决方案。

②算法

大小问题必须具有完全相同的形式。

BiTNODE *premid(char pre[],char mid[],int pre_i,int mid_i,int n)

{ int i; BiTNODE root;

if(n==0) return(NULL); / 最终解决方案 */

root=(BiTNODE )malloc(sizeof(BiTNODE));

root->data=pre[pre_i];

while(i=0; i<n; i++) / 在中序序列中定位根结点 */

if(pre[pre_i]==mid[mid_i+i]) break;

root->lchild=premid(pre,mid, pre_i+1, mid_i, i);

root->rchild=premid(pre,mid, pre_i+i+1,mid_i+i+1,n-i-1);

return(root);

}

后序+中序=>二叉树 OK

先序+后序=>二叉树 NO


8、实例:再论表达式的存储结构

字符串存储:识别运算符、运算数;比较优先级

=》效率低

①二叉树存储形式

运 算 符:分支结点;

运算对象:叶子结点;

左操作数:左子树;

右操作数:右子树;

例:a+b*c-d

②表达式树的结构

typedef enum {OP,OPD} TAG;

typedef struct node

{ TAG tag;

union

{ char op;

float data;

}UNION;

struct node *lchild,rchild;

}TNode;

③遍历的意义:求值

先序:前缀表达式

中序:中缀表达式

后序:后缀表达式

一次建树,多次高效计算、转换方便

float value(TNode root)

{ float x,y;

if(root->tag==OPD) return(root->UNION.data);

x=value(root->lch);

y=value(root->rch);

return(compute(root->UNION.op,x,y));

}

float compute(char op,float opd1,float opd2)

{ switch (op)

{ case ‘+’: return(opd1+opd2);

case ‘-’: return(opd1-opd2);

case '': return(opd1opd2);

case ‘/’: return(opd1/opd2);

}

}

作业与上机:

1、根据含空标志的先序序列,建立二叉树

2、根据先序序列、中序序列,建立二叉树

3、深度、层次遍历二叉树

4、统计结点的个数、二叉树的深度

5、在二叉查找关键值为x的结点


第四节 线索二叉树

如何快捷地找出结点的前驱、后继?

如何保存遍历过程得到的先后次序?

①每个结点增加前驱域、后继域。

②利用结点的空链域存储前驱域和后继域。


一、建立线索树

647c57feaa344d8e9d915cee84ccafb2.png


1、算法思路:

设root中所有结点的LTag、Rtag为LINK

①p遍历各结点,pre指向上一个访问的结点。

②每次指针变化前,线索化p的前驱,*prev的后继。


2、递归算法

BiTNODE *pre; //全局变量

void BiThrTree_MidOrderThread(BiTNODE *root)

{ if(rootNULL) return;

*pre=NULL;

BiThrTree_MidOrderThreading(root);

pre->rtag=THREAD; //最后遍历的是最右下的叶子

}

void BiThrTree_MidOrderThreading(BiTNODE *p)

{ if(pNULL) return;

if(p->LTagLINK)

BiThrTree_MidOrderThreading(p->lchild);

if(p->lchildNULL)

{ p->LTag=THREAD; p->lchild=pre; }

if(pre!=NULL && pre->rchildNULL)

{ pre->RTag=THREAD; pre->rchild=p; }

pre=p;

if(p->RTagLINK)

BiThrTree_MidOrderThreading(p->rchild);

}


二、检索结点(中序线索树)

如何在中序线索树中,找到指定结点*p的前驱、后继?

1、求*p的后继

若p->RTag=THREAD:后继为p->rchild

若p->RTag=LINK :后继为p的右子树中最左下结点

BiThrNode * FindNext(BiThrNode *p)

{ if(p->rtagTHREAD) return(p->rchild);

for(p=p->rchild; p->ltagLINK; p=p->lchild);

return§;

}


2、求*p的前驱

若p->LTag=THREAD:前驱为p->lchild

若p->LTag=LINK :前驱为p的左子树中最右下结点

BiThrNode * FindPrev(BiThrNode *p)

{ if(p->ltagTHREAD) return(p->lchild);

for(p=p->lchild; p->rtagLINK; p=p->rchild);

return§;

}


3、中序线索树中的遍历

// 不使用栈的中序遍历

void MidBiThrTree_Travel(BiThrNode *root)

{ BiThrNode p;

for(p=root; p->ltag==LINK; p=p->lchild); / 找到起点 */

for(; p; p=FindNext§) printf(“%d,”,p->data);

}

// 不使用栈的反中序遍历

void MidBiThrTree_ReverseTravel(BiThrNode *root)

{ BiThrNode p;

for(p=root; p->rtag==LINK; p=p->rchild); / 找到终点 */

for(; p; p=FindPrev§) printf(“%d,”,p->data);

}


第五节 树和森林

树逻辑结构=》二叉树存储结构、算法

=》树存储结构、算法。

一、树的存储结构


1、双亲表示法

能否采用动态链表结构?

87c44174dcb1432196a27bcc41a0164f.png

57518b91f4154c7381e456fb69c63d6e.png

ffb54d7f2cb64294b29dca916c49983b.png


二、树与二叉树的转换

1、树结构与二叉树结构的一一对应关系

对树采用孩子兄弟法存储,得到目标二叉树。

源树与目标二叉树一一对应。

目标二叉树:根结点无右子树;

2、树操作与二叉树操作的一一对应关系

①基本操作:树的遍历

7b3b17a920f446d5ad3f4c27aec4f6b1.png

③应用问题:

计算树中每个结点的度?

已知树的存储结构为二叉链表结构,每个结点包含data、firstchild、nextsibling、degree四个域,要求将每个结点度数存入相应结点的degree域中。

// 遍历二叉树的套路

void Tree_Degree(CSNODE *root)

{ CSNODE *p;

if(rootNULL) return;

root->degree=0;

for(p=root->firstchild; p; p=p->nextsibling)

root->degree++;

Tree_Degree(root->firstchild);

Tree_Degree(root->nextsibling);

}

计算原树的叶子数?

// 遍历线性表的套路

int Tree_Leaf(CSNODE *root)

{ CSNODE *p;

int count=0;

if(rootNULL) return(0);

if(root->firstchildNULL) return(1);

for(p=root->firstchild; p; p=p->nextsibling)

count=count + Tree_Leaf§;

return(count);

}

计算原树的深度?

// 遍历线性表的套路

int Tree_Depth(CSNODE *root)

{ CSNODE *p;

int depth1,maxdepth=0;

if(rootNULL) return(0);

for(p=root->firstchild; p; p=p->nextsibling)

{ depth1=Tree_Depth§;

if(depth1>maxdepth) maxdepth=depth1;

}

return(maxdepth+1);

}


三、森林与树的转换

1、森林结构与二叉树结构的一一对应关系

孩子兄弟法:将所有树的根结点视为兄弟。

9c47697e35c847cab98315df59adf6b9.png

ab6e92a9e4aa4262b566ab1b6055e645.png

c36de21cbfb44cda84177cd72122cc53.png


第六节 哈夫曼树及其应用

应用实例:文件压缩、解压。


一、术语

路径 :连接结点之间的若干分支。

路径长度:路径的分支数。

结点路径长度:从根到该结点的路径长度。

树的路径长度:树中所有叶子结点的路径长度之和。

结点带权路径长度:结点路径长度与结点权的积。

树的带权路径长度:树中所有叶子结点的带权路径长度之和。

哈夫曼树(最优二叉树):

在叶子结点确定的前提下,带权路径长度最小的二叉树。

05fecad594f34c62a96bef14c28b5a3e.png


二、构造哈夫曼树的算法

已知:N个结点的权值为W1,……,WN,构造算法如下:

①根据(W1,……,WN)构成森林F=(T1,……,TN),每棵二叉树Ti有且只有一个根结点,权为Wi,左右子树为空;

②在F中选取二棵根的权值最小的树Ti、Tj,权为Wi、Wj,作为左右子树合成一棵新树Tk,Tk的根的权Wk=Wi+Wj;

③在F中删除Ti、Tj,加入Tk;

④重复②③,循环N-1次,F中只剩一棵树,即为哈夫曼树。


示例:0.4, 0.2, 0.1, 0.5

示例:0.22, 0.3, 0.4, 0.28


三、哈夫曼编码

1、信息编码的目标

源文件 =》 目标文件

编码目标: 目标文件长度尽可能短。

2、编码方法

统计编码原理:①基于概率分布特性;②编码长度不固定。

频率高的符号用较少的位。

问题1:

A的编码可以为"1"吗?B的编码可以为"11"吗?

前缀编码:任何符号的编码都不是另一符号编码的前缀。

统计编码必须是前缀编码!

问题2:编码的效率?

设 S1, S2, … , Sn是被编码的一组符号,

P1, P2, … , Pn是各自出现的概率,

L1, L2, … , Ln是码长,

则平均码长:见下 4f43e62cab2646af8487fdcad5d07c66.png


3、哈夫曼树中的哈夫曼编码

哈夫曼树的意义:

叶子:待编码的符号;

权值:待编码的符号的出现频率

叶子的路径:符号的哈夫曼编码

记:左分支=0,右分支=1。

哈夫曼编码的性质:

①前缀编码:各个叶子的路径决不会相互包含。

 ②高效编码:树的带权路径长度=平均码长


4、压缩/解压软件的思路

设压缩单位为字节,

压缩模块的设计思路:

①统计源文件:0,1,……,255的出现次数=》“频率表”;

②根据“频率表”构造哈夫曼树;

③读取各字符的哈夫曼编码;

④将“频率表”写入压缩文件头;(文件头的格式设计?)

⑤翻译源文件,写入压缩文件。

解压模块的设计思路:

①读压缩文件头,得到“频率表”;

②根据“频率表”重构哈夫曼树;

③读压缩文件中的"0/1"串,从根搜索至叶子,将叶子对应的字符写入“解压文件”;

④重复③,直至译完压缩文件。

四、哈夫曼算法:建树、计算编码

存储结构:顺序?链式?

若结点数确定:采用静态链表。

若结点数不定:采用动态链表。

#define N 4 /* 需编码的字符数 /

#define M 7 / 哈夫曼树的结点数=2N-1 */

typedef struct //每个字符及相应权值

{ char ch;

float weight;

}Char_Weight;


typedef struct

{ char ch;

float weight;

int parent,lchild,rchild; //父结点,左、右孩子

}HuffNode;


typedef struct //每个字符的编码结构

{ int start;

int bits[N];

}HuffCode;


main()

{ Char_Weight W[N];

HuffNode HT[M]; HuffCode HC[N];

ReadWeight(W); // 读入权值

Huffman_Create(HT,W); //建树HT[M]

Huffman_Coding(HT,HC); //读编码HC[N]

}


void Huffman_Create(HuffNode HT[],Char_Weight W[])

{ for(i=0;i<N;i++) // 初始化N个树的森林

{ HT[i].ch= W[i].ch; // 为将来译码作准备

HT[i].parent=HT[i].lchild=HT[i].rchild=-1;

HT[i].weight=W[i].weight;

}

for(i=N;i<M;i++) // 合并树

{ select(HT,i-1,&n1,&n2); // n1最小,n2次小

// 可采用优先队列实现

HT[n1].parent=HT[n2].parent=i;

HT[i].parent=-1; HT[i].lchild=n1; HT[i].rchild=n2;

HT[i].weight=HT[n1].weight+HT[n2].weight;

}

}

示例:a:0.4, b:0.2, c:0.1, d:0.5

46ecd322ff9041509e0db0861fc34525.png

void HuffmanCoding(HuffNode HT[], HuffCode HC[])

{ int i,j, p, parent;

for(i=0; i<N; i++)

{ p=i; parent=HT[p].parent;

// 只有根结点的parent==-1

for(j=N-1; parent!=-1; j–)

{ if(HT[parent].lchild==p)

HT[i].bits[j]=0; //左子树为0

else HT[i].bits[j]=1; //右子树为1

p=parent; parent=HT[parent].parent;

}

HC[i].start=j+1;

}

}


五、哈夫曼算法:译码(密文=》明文)

void HuffmanDeCoding(HuffNode HT[],char source[],char dest[])

{ int i, p=M-1, k=0; // p为根结点下标,k是明文中字符数

for(i=0; source[i]; i++)

{ if(source[i]‘0’) p=HT[p].lchild;

else p=HT[p].rchild;

if(HT[p].lchild-1 && HT[p].lchild==-1) // 遇到叶子

{ dest[k++]=HT[p].ch; p=M-1; } // 翻译字符,从头再来

}

dest[k]=‘\0’;

}

附:哈夫曼树的在编程中的一个应用:

根据数据情况,改善条件判断的执行效率

if(x<60) grade=“不及格”; 0.05

else if(x<70) grade=“及格”; 0.15

else if(x<80) grade=“中等”; 0.40

else if(x<90) grade=“良好”; 0.30

else grade=“优秀”; 0.10


if(x<80)

if(x<70)

if(x<60) grade=“不及格”; 0.05

else grade=“及格”; 0.15

else grade=“中等”; 0.40

else

if(x<90) grade=“良好”; 0.30

else grade=“优秀”; 0.10


相关文章
|
22天前
|
C语言
【数据结构】栈和队列(c语言实现)(附源码)
本文介绍了栈和队列两种数据结构。栈是一种只能在一端进行插入和删除操作的线性表,遵循“先进后出”原则;队列则在一端插入、另一端删除,遵循“先进先出”原则。文章详细讲解了栈和队列的结构定义、方法声明及实现,并提供了完整的代码示例。栈和队列在实际应用中非常广泛,如二叉树的层序遍历和快速排序的非递归实现等。
109 9
|
21天前
|
存储 搜索推荐 算法
【数据结构】树型结构详解 + 堆的实现(c语言)(附源码)
本文介绍了树和二叉树的基本概念及结构,重点讲解了堆这一重要的数据结构。堆是一种特殊的完全二叉树,常用于实现优先队列和高效的排序算法(如堆排序)。文章详细描述了堆的性质、存储方式及其实现方法,包括插入、删除和取堆顶数据等操作的具体实现。通过这些内容,读者可以全面了解堆的原理和应用。
60 16
|
21天前
|
C语言
【数据结构】二叉树(c语言)(附源码)
本文介绍了如何使用链式结构实现二叉树的基本功能,包括前序、中序、后序和层序遍历,统计节点个数和树的高度,查找节点,判断是否为完全二叉树,以及销毁二叉树。通过手动创建一棵二叉树,详细讲解了每个功能的实现方法和代码示例,帮助读者深入理解递归和数据结构的应用。
72 8
|
24天前
|
存储 C语言
【数据结构】手把手教你单链表(c语言)(附源码)
本文介绍了单链表的基本概念、结构定义及其实现方法。单链表是一种内存地址不连续但逻辑顺序连续的数据结构,每个节点包含数据域和指针域。文章详细讲解了单链表的常见操作,如头插、尾插、头删、尾删、查找、指定位置插入和删除等,并提供了完整的C语言代码示例。通过学习单链表,可以更好地理解数据结构的底层逻辑,提高编程能力。
50 4
|
25天前
|
存储 C语言
【数据结构】顺序表(c语言实现)(附源码)
本文介绍了线性表和顺序表的基本概念及其实现。线性表是一种有限序列,常见的线性表有顺序表、链表、栈、队列等。顺序表是一种基于连续内存地址存储数据的数据结构,其底层逻辑是数组。文章详细讲解了静态顺序表和动态顺序表的区别,并重点介绍了动态顺序表的实现,包括初始化、销毁、打印、增删查改等操作。最后,文章总结了顺序表的时间复杂度和局限性,并预告了后续关于链表的内容。
56 3
|
25天前
|
存储 算法 C语言
C语言数据结构(2)
【10月更文挑战第21天】
|
24天前
|
C语言
【数据结构】双向带头循环链表(c语言)(附源码)
本文介绍了双向带头循环链表的概念和实现。双向带头循环链表具有三个关键点:双向、带头和循环。与单链表相比,它的头插、尾插、头删、尾删等操作的时间复杂度均为O(1),提高了运行效率。文章详细讲解了链表的结构定义、方法声明和实现,包括创建新节点、初始化、打印、判断是否为空、插入和删除节点等操作。最后提供了完整的代码示例。
40 0
|
13天前
|
存储 算法
非递归实现后序遍历时,如何避免栈溢出?
后序遍历的递归实现和非递归实现各有优缺点,在实际应用中需要根据具体的问题需求、二叉树的特点以及性能和空间的限制等因素来选择合适的实现方式。
21 1
|
16天前
|
存储 算法 Java
数据结构的栈
栈作为一种简单而高效的数据结构,在计算机科学和软件开发中有着广泛的应用。通过合理地使用栈,可以有效地解决许多与数据存储和操作相关的问题。
|
19天前
|
存储 JavaScript 前端开发
执行上下文和执行栈
执行上下文是JavaScript运行代码时的环境,每个执行上下文都有自己的变量对象、作用域链和this值。执行栈用于管理函数调用,每当调用一个函数,就会在栈中添加一个新的执行上下文。
下一篇
无影云桌面