第一章:绪论
一:基本概念和术语
数据:分为数值型数据与非数值型数据
是能输入计算机且能被计算机处理的各种符号的集合
数据元素:数据的基本单位
数据元素也叫记录、结点、顶点
数据项:构成数据元素的不可分割的最小单位
数据 > 数据元素 > 数据项
数据元素与数据的关系:是集合的个体
数据项与数据的关系:是集合的子集
数据结构:相互之间存在一种或多种特定关系的数据元素的集合
1.逻辑结构:数据元素之间的逻辑关系
2.物理结构/存储结构:数据元素机器关系在计算机内存中的表示(又称映像)
3.运算和实现:对数据元素可以试驾的操作以及这些操作在相应的存储结构上的实现
1.逻辑结构
描述数据元素之间的逻辑关系
与数据的存储无关,独立于计算机
是从具体问题抽醒出来的数学模型
逻辑结构的种类
1.线性结构:
有且仅有一个开始和一个终端结点,并且所有结点都最多有一个直接前驱和一个直接后继
例如:线性表、栈、队列、串
2.非线性结构:
一个结点可能有多个直接前驱和直接后继
例如:树、图
1.集合结构:(数据元素之间)除了同属一个集合之外无任何其他关系
2.线性结构:一对一的线性关系
3.树形结构:一对多的层次关系
4.图状结构/网状结构:多对多的任意关系
2.存储结构
数据元素及其关系在计算机存储器中的结构(存储方式)
是数据结构在计算机中的表示
存储结构的种类
1.顺序存储结构:
用一组连续的存储单元依次存储数据元素,数据元素之间的逻辑关系由元素的存储位置来表示
C语言中用数组来实现顺序存储结构
2.链式存储结构
用一组任意的存储单元存储数据结构,数据元素之间的逻辑关系用指针来表示
C语言中用指针(链表)来实现存储结构
3.索引存储结构
4.散列存储结构
3.逻辑结构与存储结构的关系
存储结构是逻辑关系的映像与元素本身的映像
逻辑结构是数据结构的抽象,存储结构是数据结构的实现
两者综合起来建立了数据元素之间的结构关系
4.数据类型和抽象数据类型
1.数据类型
数据类型 = 值的集合 + 值集合上的一组操作
高级语言中的数据类型明显地或隐含地规定了程序执行期间变量和表达的所有可能的取值范围,以及这些数值范围上所允许的操作
作用:约束变量或常量的取值范围和操作
2.抽象数据类型(ADT)
指一个数学模型以及定义在此数学模型上的一组操作
由用户定义,从问题抽象出数据模型(逻辑结构)
还包括定义在数据模型上的一组抽象运算(相关操作)
不考虑计算机内的具体存储结构与运算的具体实现算法
抽象数据类型的形式化定义:
抽象数据类型可用D、S、P三元组表示
ADT定义举例:
5.小结
二:抽象数据类型的表示与实现
#include <iostream> using namespace std ; typedef struct { float realpart; float imagpart; }Complex; void assign (Complex* A, float real, float imag) ; void add (Complex* C, Complex A, Complex B) ; void minus (Complex* C, Complex A, Complex B) ;void mutiply (Complex* C, Complex A, Complex B) ;void divide (Complex* C, Complex A, Complex B) ;void assign (Complex* A, float real, float imag) { A->realpart = real; A->imagpart = imag; } void add (Complex* C, Complex A, Complex B) { C->realpart = A.realpart + B.realpart; C->imagpart = A.imagpart + B.imagpart; } void minus (Complex* C, Complex A, Complex B) { C->realpart = A.realpart - B.realpart; C->imagpart = A.imagpart - B.imagpart; } void mutiply (Complex* C, Complex A, Complex B) { C->realpart = A.realpart * B.realpart - A.imagpart * B.imagpart; C->imagpart = A.realpart * B.imagpart + A.imagpart * B.realpart; } void divide (Complex* C, Complex A, Complex B) { C->realpart = (A.realpart * B.realpart + A.imagpart * B.imagpart) / (B.realpart * B.realpart + B.imagpart * B.imagpart); C->imagpart = (B.realpart * A.imagpart - B.imagpart * A.realpart) / (B.realpart * B.realpart + B.imagpart * B.imagpart); }
三:算法和算法分析
算法的定义:解决问题的方法和步骤
算法的描述:自然语言、流程图、伪代码、程序代码
算法与程序:
算法考虑如何将输入转换成输出
程序是用某种设计语言对算法的具体实现
程序 = 数据结构 + 算法
数据结构通过算法实现操作
算法根据数据结构设计程序
算法特性
1.有穷性:步骤有穷、时间有穷
2.确定性:无二义性
3.可行性
4.输入:一个算法有零个或几个输入
5.输出:一个算法有一个或多个输出
算法设计的要求:
正确性、可读性、健壮性、高效性
算法的效率:时间效率、空间效率
1.算法的时间效率
算法时间效率的度量:事后统计(测算)、事前分析(估算)
1.事前估算:
算法运行时间 = ∑每条语句的执行次数(又称语句频度) * 该语句执行一次所需时间
算法时间复杂度:
算法中基本语句重复执行的次数是问题规模n的某个函数f(n),则算法的时间复杂度为:T(n) = O(f(n)) O是数量级的符号
基本语句:对算法运行时间贡献最大,执行次数最多
排序:n为记录数
矩阵:n为阶数
多项式:n为项数
集合:n为元素个数
树:n为结点个数
图:n为图的顶点或边数
定理1.1:若 f(n) 是关于n的幂函数,则 T(n) = O(n的最高次幂)
忽略低次幂项和高次幂项的系数
求时间复杂度的方法:
1.找出基本语句(语句频度最大)
2.计算基本语句的频度得到f(n)
3.取其数量级用符号“O”表示
有些情况下,算法中基本操作重复执行的次数还随问题的输入数据集不同而不同
平均时间复杂度:在所有可能输入实例在等概率出现的情况下,算法的期望运行时间
一般情况下总是考虑算法的最坏时间复杂度
对于复杂的算法,可以将其分成几个容易估算的部分,然后利用 O 加法法则和乘法法则计算时间复杂度
当n取得很大时,指数时间算法和多项式时间算法在所需的时间上非常悬殊
时间复杂的按数量级递增的顺序为:
2.算法的空间效率
空间复杂度:算法所需存储空间的度量
S(n) = O(f(n)) n为问题的规模
算法要占据的空间包括:
算法本身要占据的空间,输入/输出、指令、常数、变量等
算法要使用的辅助空间
3.设计好算法的过程
抽象数据类型 = 数据的逻辑结构 + 抽象运算
根据时间复杂度和空间复杂度选择最优算法
四:第一章小结
第二章:线性表
补充:C/C++基础知识
1.链表相关知识:
[c++ 链表基础知识][c_]
2.动态内存申请相关知识:
[C/C++ 内存的动态申请与释放][C_C_]
一:线性表的定义和特点
线性表是具有相同特性的数据元素的一个有限序列
顺序存储结构存在的问题:
存储空间不灵活
运算的空间复杂度高
优点:
存储密度大
可以随机存取表中任一元素
缺点:
在插入、删除某一元素时,需要移动大量元素
浪费存储空间
是静态存储,数据元素的个数不能自由扩充
二:线性表的顺序表示和实现
抽象数据类型线性表的定义
顺序存储的定义:逻辑上相邻,物理上也相邻
1.liner_list_sq.h(头文件)
#pragma once #define TRUE 1 #define FALSE 0 #define OK 1 #define ERROR 0 #define INFEASIBLE -1 #define LOVERFLOW -2 #define LIST_INIT_SIZE 100 #define LISTINCREMENT 10 typedef int Status; #define ELEMTYPE_IS_INT #ifdef ELEMTYPE_IS_DOUBLE typedef double ElemType;#elif defined (ELEMTYPE_IS_CHAR_ARRAY) typedef char ElemType[LISTINCREMENT];#elif defined (ELEMTYPE_IS_CHAR_P) typedef char * ElemType;#elif defined (ELEMTYPE_IS_STRUCT_STUDENT) typedef struct student { int num; char name[LISTINCREMENT]; char sex; float score; char addr[30 ]; }ElemType; #elif defined (ELEMTYPE_IS_STRUCT_STUDENT_P) typedef struct student { int num; char name[LISTINCREMENT]; char sex; float score; char addr[30 ]; }ET, * ElemType; #else typedef int ElemType; #endif typedef struct { ElemType* elem; int length; int listsize; }sqlist; Status InitList (sqlist* L) ; Status DestroyList (sqlist* L) ; Status ClearList (sqlist* L) ; Status ListEmpty (sqlist L) ; int ListLength (sqlist L) ; Status Getelem (sqlist L, int i, ElemType* e) ; int LocateElem (sqlist L, ElemType e, Status(*compare)(ElemType e1, ElemType e2)) ; Status PriorElem (sqlist L, ElemType cur_e, ElemType* pre_e, Status(*compare)(ElemType e1, ElemType e2)) ; Status NextElem (sqlist L, ElemType cur_e, ElemType* next_e, Status(*compare)(ElemType e1, ElemType e2)) ; Status ListInsert (sqlist* L, int i, ElemType e) ; Status ListDelete (sqlist* L, int i, ElemType* e) ; Status ListTraverse (sqlist L, Status(*visit)(ElemType e)) ;
2.liner_list_sq.c(具体函数的实现)
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <math.h> #include "liner_list_sq.h"
Status InitList (sqlist* L) { L->elem = (ElemType*)malloc (LIST_INIT_SIZE * sizeof (ElemType)); if (!L->elem) exit (LOVERFLOW); L->length = 0 ; L->listsize = LIST_INIT_SIZE; return OK; }
Status DestroyList (sqlist* L) { #if defined (ELEMTYPE_IS_CHAR_P) || defined (ELEMTYPE_IS_STRUCT_STUDENT_P) for (int i = 0 ; i < L->length; i++) free (L->elem[i]); #endif if (L->elem) free (L->elem); L->length = L->listsize = 0 ; return OK; }
Status ClearList (sqlist* L) { #if defined (ELEMTYPE_IS_CHAR_P) || defined (ELEMTYPE_IS_STRUCT_STUDENT_P) for (int i = 0 ; i < L->length; i++) free (L->elem[i]); #endif L->length = 0 ; return OK; }
Status ListEmpty (sqlist L) { if (L.length == 0 ) return TRUE; else return FALSE; } int ListLength (sqlist L) { return L.length; }
Status Getelem (sqlist L, int i, ElemType* e) { if (i<1 || i>L.length) return ERROR; #if defined (ELEMTYPE_IS_CHAR_ARRAY) || defined (ELEMTYPE_IS_CHAR_P) strcpy (*e, L.elem[i - 1 ]); #elif defined (ELEMTYPE_IS_STRUCT_STUDENT) memcpy (e, &(L.elem[i - 1 ]), sizeof (ElemType)); #elif defined (ELEMTYPE_IS_STRUCT_STUDENT_P) memcpy (*e, L.elem[i - 1 ], sizeof (ET)); #else * e = L.elem[i - 1 ]; #endif return OK; }
memcpy函数的使用:
void *memcpy(void *dest, const void *src, int n);
将从源地址开始的n个字节复制到目标地址中
整体内存拷贝,不论中间是否有尾零
内存理解同char型数组但无法保证尾零,因此不能用strcpy
int LocateElem (sqlist L, ElemType e, Status(*compare)(ElemType e1, ElemType e2)) { ElemType* p = L.elem; int i = 1 ; while (i <= L.length && (*compare)(*p++, e) == FALSE) i++; return (i <= L.length) ? i : 0 ; }
Status MyCompare (ElemType e1, ElemType e2) { #if defined (ELEMTYPE_IS_DOUBLE) if (fabs (e1 - e2) < 1e-6 ) #elif defined (ELEMTYPE_IS_CHAR_ARRAY) || defined (ELEMTYPE_IS_CHAR_P) if (strcmp (e1, e2) == 0 ) #elif defined (ELEMTYPE_IS_STRUCT_STUDENT) if (e1.num == e2.num) #elif defined (ELEMTYPE_IS_STRUCT_STUDENT_P) if (e1->num == e2->num) #else if (e1 == e2) #endif return TRUE; else return FALSE; }
Status PriorElem (sqlist L, ElemType cur_e, ElemType* pre_e, Status(*compare)(ElemType e1, ElemType e2)) { ElemType* p = L.elem; int i = 1 ; while (i <= L.length && (*compare)(*p, cur_e) == FALSE) { i++; p++; } if (i == 1 || i > L.length) return ERROR; #if defined (ELEMTYPE_IS_CHAR_ARRAY) || defined (ELEMTYPE_IS_CHAR_P) strcpy (*pre_e, *--p); #elif defined (ELEMTYPE_IS_STRUCT_STUDENT) memcpy (pre_e, --p, sizeof (ElemType)); #elif defined (ELEMTYPE_IS_STRUCT_STUDENT_P) memcpy (*pre_e, *--p, sizeof (ET)); #else * pre_e = *--p; #endif return OK; } Status NextElem (sqlist L, ElemType cur_e, ElemType* next_e, Status(*compare)(ElemType e1, ElemType e2)) { ElemType* p = L.elem; int i = 1 ; while (i <= L.length && (*compare)(*p, cur_e) == FALSE) { i++; p++; } if (i >= L.length) return ERROR; #if defined (ELEMTYPE_IS_CHAR_ARRAY) || defined (ELEMTYPE_IS_CHAR_P) strcpy (*next_e, *++p); #elif defined (ELEMTYPE_IS_STRUCT_STUDENT) memcpy (next_e, ++p, sizeof (ElemType)); #elif defined (ELEMTYPE_IS_STRUCT_STUDENT_P) memcpy (*next_e, *++p, sizeof (ET)); #else * next_e = *++p; #endif return OK; }
Status ListInsert (sqlist* L, int i, ElemType e) { ElemType* p, * q; if (i < 1 || i > L->length + 1 ) return ERROR; if (L->length >= L->listsize) { L->elem = (ElemType*)realloc (L->elem, (L->listsize + LISTINCREMENT) * sizeof (ElemType)); if (!L->elem) return LOVERFLOW; L->listsize += LISTINCREMENT; } q = &(L->elem[i - 1 ]); for (p = &(L->elem[L->length - 1 ]); p >= q; --p) #if defined ELEMTYPE_IS_CHAR_ARRAY strcpy (*(p + 1 ), *p); #elif defined ELEMTYPE_IS_STRUCT_STUDENT memcpy (p + 1 , p, sizeof (ElemType)); #else * (p + 1 ) = *p; #endif #if defined ELEMTYPE_IS_CHAR_ARRAY strcpy (*q, e); #elif defined ELEMTYPE_IS_CHAR_P L->elem[i - 1 ] = (ElemType)malloc ((strlen (e) + 1 ) * sizeof (char )); if (!L->elem[i - 1 ]) return LOVERFLOW strcpy (*q, e); #elif defined ELEMTYPE_IS_STRUCT_STUDENT memcpy (q, &e, sizeof (ElemType)); #elif defined ELEMTYPE_IS_STRUCT_STUDENT_P L->elem[i - 1 ] = (ElemType)malloc (sizeof (ET)); if (!L.elem[i - 1 ]) return LOVERFLOW memcpy (*q, e, sizeof (ET)); #else * q = e; #endif L->length++; return OK; }
Status ListDelete (sqlist* L, int i, ElemType* e) { ElemType* p, * q; if (i < 1 || i > L->length) return ERROR; p = &(L->elem[i - 1 ]); #if defined (ELEMTYPE_IS_CHAR_ARRAY) || defined (ELEMTYPE_IS_STRUCT_STUDENT) strcpy (*e, *p); #elif defined (ELEMTYPE_IS_STRUCT_STUDENT) memcpy (e, p, sizeof (ElemType)); #elif defined (ELEMTYPE_IS_STRUCT_STUDENT_P) memcpy (*e, *p, sizeof (ET)); #else * e = *p; #endif q = &(L->elem[L->length - 1 ]); #if defined (ELEMTYPE_IS_CHAR_P) || defined (ELEMTYPE_IS_STRUCT_STUDENT_P) free (*p); #endif for (++p; p <= q; ++p) { #if defined ELEMTYPE_IS_CHAR_ARRAY strcpy (*(p - 1 ), *p); #elif defined ELEMTYPE_IS_STRUCT_STUDENT memcpy ((p - 1 ), p, sizeof (ElemType)); #else * (p - 1 ) = *p; #endif } L->length--; return OK; }
Status ListTraverse (sqlist L, Status(*visit)(ElemType e)) { extern int line_count; ElemType* p = L.elem; int i = 1 ; line_count = 0 ; while (i <= L.length && (*visit)(*p++) == TRUE) i++; if (i <= L.length) return ERROR; printf ("\n" ); return OK; }
main中用于比较访问线性表某个元素的值的具体函数
Status MyVisit (ElemType e) { #if defined (ELEMTYPE_IS_DOUBLE) printf ("%5.1f->" , e); #elif defined (ELEMTYPE_IS_CHAR_ARRAY) || defined (ELEMTYPE_IS_CHAR_P) printf ("%s->" , e); #elif defined (ELEMTYPE_IS_STRUCT_STUDENT) printf ("%d-%s-%c-%f-%s->" , e.num, e.name, e.sex, e.score, e.addr); #elif defined (ELEMTYPE_IS_STRUCT_STUDENT_P) printf ("%3d->" , e); #else printf ("%3d->" , e); #endif if ((++line_count) % 10 == 0 ) printf ("\n" ); return OK; }
三:线性表的链式表示和实现
单链表:结点只有一个指针域
双链表:结点有两个指针域
循环链表:首尾相接
头结点
头结点的好处:
头结点的数据域可以为空,也可与存放线性表长度等信息,但此节点不能计入链表长度值
四:双向链表
1.双向链表的插入
s->prior = p->prior; p->prior->next = s; s->next = p; p->prior = s;
p->prior = s 必须在 p->prior->next = s 之后,其他顺序随意
2.双向链表的删除
p->prior->next = p->next; p->next->prior = p->prior;
五:单链表、循环链表、双向链表的时间效率比较
六:顺序表和链表的比较
链式存储结构:
优点:
1.结点空间可以动态申请和释放
2.插入和删除操作方便
缺点:
1.存储密度小
2.进行查找操作时比较困难
七:线性表的应用
1.线性表的合并(求并集)
void union_sq (List &La,List Lb) { La_len = ListLength(La); Lb_len = ListLength(Lb); for (i = 1 ; i <= Lb_len; i++) { GetElem(Lb, i, e); if (!LocateElem(La, e, equal)) ListInsert(La, ++La_len, e); } }
该算法的时间复杂度:O(ListLenth(La) * ListLengrh(Lb))
2.有序表的合并
void MergeList_sq (List La,List Lb, SqList &Lc) { pa = La.elem; pb = Lb.elem; Lc.listsize = Lc.length = La.length + Lb.length; pc = Lc.elem = (ElemType *)malloc (Lc.listsize * sizeof (ElemType)); if (!Lc.elem) exit (OVERFLOW); pa_last = La.elem + La.length - 1 ; pb_last = Lb.elem + Lb.length - 1 ; while (pa <= pa_last && pb <= pb_last) { if (*pa <= *pb) *pc++ = *pa++; else *pc++ = *pb++; } while (pa <= pa_last) *pc++ = *pa++; while (pb <= pb_last) *pc++ = *pb++; }
该算法时间复杂度:O(ListLenth(La) + ListLengrh(Lb))
该算法空间复杂度:O(ListLenth(La) + ListLengrh(Lb))
void MergeList_L (List &La,List &Lb, SqList &Lc) { ElemType *pa = La->next; ElemType *pb = Lb->next; ElemType *pc = Lc = 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?pb:pb; delete Lb; }
该算法时间复杂度:O(ListLenth(La) + ListLengrh(Lb))
该算法空间复杂度:O(1)
八:案例分析与实践
1.一元多项式的运算:加、减、乘
``` ### 2. 稀疏多项式的运算 ![image_45f86a40.png](https: ![image_be808582.png](https: ![image_cc8079b5.png](https: ![image_f707b34f.png](https: * 多项式的建立 ```c typedef struct PNode{ float coef; int expn; struct PNode *next; }PNode,*Polynomial;
void CreatPolyn (Polynomial &P, int n) { PNode* s, *pre, *q; P = new PNode; P->next = NULL ; for (int i = 1 ; i <= n; i++) { PNode* s = new PNode; cin >> s-> coef >> s->expn; pre = P; q = P->next; while (q && q->expn < s->expn) { pre = q; q = q->next; } s->next = q; pre->next = s; } }
3.图书馆信息管理系统
struct Book { char id[20 ]; char name[50 ]; int price; }; typedef struct { Book *elem; int length; }SqList; typdef struct LNnode { Book data; struct LNode *next ; }Lnode,*LinkList;
第三章:栈和队列
一:定义和特点
1.栈的定义和特点
栈(LIFO):后进先出
栈顶:Top
栈底:Base
入栈(PUSH):插入元素到栈顶
出栈(POP):从栈顶删除一个元素
示意图:
栈的应用:
2.队列的定义和特点
队列(FIFO):先进先出
队列的应用:
二:案例引入
1.进制转换
2.括号匹配的检验
3.表达式的组成
三:栈的表示和实现
1.顺序栈
空栈:base == top
栈满:top - base == stacksize
#define MAXSIZE 100 typedef struct { SElemType *base; SElemType *top; int stacksize; }SqStack;
Status InitStack (Sqstack &S) { S.base =(SElemType*)malloc (MAXSIZE * sizeof (SElemType)); if (!S.base) exit (OVERFLOW); S.top = S.base; S.stacksize = MAXSIZE; return OK; }
Status StackEmpty (SqStack S) { if (S.top == S.base) return TURE; else return FALSE; } int StackLength (SqStack S) { return S.top - S.base; } Status ClearStack (SqStack &S) { if (S.base) S.top = S.base; retuen OK; } Status DestroyStack (SqStack &S) { if (S.base){ free (S.base); S.stacksize = 0 ; S.base = S.top = NULL ; } return OK; } Status Push (SqStack &S, SElemType e) { if (S.top - S.base >= S.stacksize) { S.base = (SElemType*)realloc (S.base,(S.stacksize + STACKINCREMENT) * sizeof (SElemType)); if (!S.base) exit (OVERFLOW); s.top = s.base + S.stacksize; S.stacksize += STACKINCREMENT; } *S.top++ = e; return OK; } Status Pop (SqStack &S, SElemType &e) { if (S.top == S.base) return ERROR; e = *--S.top; return OK; }
2.链栈
typedef struct StackNode { SElemType data; struct StackNode *next ; }StackNode, *LinkStack; Status InitStack (LinkStack &S) { S = NULL ; return OK; } Status Push (LinkStack &S, SElemType e) { p = (StackNode*)malloc (sizeof (StackNode)); p->data = e; p->next = S; S = p; return OK; } Status Pop (LinkStack &S, SElemType &e) { if (S == NULL ) return ERROR; e = S->data; p = S; S = S->next; free (p); return Ok; }
四:队列的表示和实现
1.顺序队列(循环队列)
#define MAXQSIZE 100 typedef struct { QElemType *base; int front; int rear; }SqQueue;
Status InitQueue (SqQueue &Q) { Q.base = (QElemtype*)malloc (MAXQSIZE * sizeof (QElemType)); if (!Q.base) exit (OVERFLOW); Q.front = Q.rear = 0 ; return OK; } int QueueLength (SqQueue Q) { return ((Q.rear - Q.front + MAXQSIZE) % MAXQSIZE); } Status EnQueue (SqQueue &Q, QElemType e) { if ((Q.rear + 1 ) % MAXSIZE == Q.front) return ERROR; Q.base[Qrear] = e; Q.rear = (Q.rear + 1 ) % MAXQSIZE; return OK; } Status DeQueue (SqQueue &Q, QElemType &e) { if (Q.front == Q.rear) return ERROR; e = Q.base[Q.front]; Q.front = (Q.front + 1 ) % MAXQSIZE; return OK; }
2.链队列
#define MAXQSIZE 100 typedef struct QNode { QElemType data; struct QNode *next ; }QNode, *QueuePtr; typedef struct { QueuePtr front; QueuePtr rear; }LinkQueue;
Status InitQueue (LinkQueue &Q) { Q.front = Q.rear = (QueuePtr)malloc (sizeof (QNode)); if (!Q.front) exit (OVERFLOW); Q.front->next = NULL ; return OK; } Status DestroyQueue (LinkQueue &Q) { while (Q.front){ Q.rear = Q.front->next; free (Q.front); Q.front = Q.rear; } return OK; } Status EnQueue (LinkQueuue &Q, QElemType e) { p = (QueuePtr)malloc (sizeof (QNode)); if (!p) exit (OVERFLOW); p->data = e; p->next = NULL ; Q.rear->next = p; Q.rear = p; return OK; } Status DeQueue (LinkQueue &Q, QElemType &e) { if (Q.front == Q.rear) return ERROR; p = Q.front->next; e = p->data; Q.front->next = p->next; if (Q.rear == p) Q.rear = Q.front; free (p); return OK; }
第四章:串
一:串的顺序存储结构
#define MAXLEN 255 typedef struct { char ch[MAXLEN + 1 ]; int length; }SString;
二:串的链式存储结构–块链结构
#define CHUNKSIZE 80 typedef struct Chunk { char ch[CHUNKSIZE]; struct Chunk *next ; }Chunk; typedef struct { Chunk *head, *tail; int curlen; }LString;
三:串的模式匹配算法
目的:确定主串中所含子串第一次出现的位置
应用:搜索引擎、拼写检查、数据压缩
种类:BF算法、KMP算法
1.BF算法
int Index_BF (SString S, SString T) { int i = 1 , j = 1 ; 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 ; }
2.KMP算法
[天勤公开课:KPM算法(易懂版)][KPM]
[KMP详解][KMP]
利用已经部分匹配的结果而加快模式串的滑动速度
主串S的指针i不必回溯,可提速到O(n + m)
void get_next (SString T, int next[]) { int i = 1 , j = 0 ; next[1 ] = 0 ; while (i < T.length) { if (j == 0 || T.ch[i] == T.ch[j]) { i++; j++; next[i] = j; } else j = next[j]; } }
[KMP算法之求next数组代码讲解][KMP_next]
int Index_KPM (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 ; }
将next[i]的值对应的位的值与i的值进行比较,若相等,nextval[i]=nextval【next[i]】;若不相等,则nextval[i]=next[i]
void get_nextval (SString T, int nextval[]) { int i = 1 , j = 0 ; nextval[1 ] = 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 = next[j]; } }
第五章:数组和广义表
一:特殊矩阵的压缩存储
1.对称矩阵
aij = aji
只需存储上/下部分
将n * n个元素压缩为n * (n+1)/2个元素
以行序为主序将元素存储到一个一维数组 sa[n(n+1/2] 中。
位置:aij --> ∑(i - 1) + (j - 1) --> n(n-1)/2 + j - 1
2.三角矩阵
对角线以下/上的元素都是同样的数
存储方法同对称矩阵
3.对角矩阵
所有非零元素都集中在以主对角线为中心的带状区域中
4.稀疏矩阵
矩阵中大部分都是零元素(95%)
三元组法:
用三元组(i, j, aij)表示
通常加一个“总体”信息(总行数, 总列数, 非零元素个数)
优点:
非零元在表中按行序存储,便于进行按行顺序处理的运算
缺点:
不能随机存储,若按行号存取某一行中的非零元,则需从头开始进行查找
typedef struct { int i, j; ElemType e; }Triple; typedef struct { Triple data[MAXSIZE + 1 ]; int mu, nu, tu; }TSMatrix;
十字链表法:
每个非零元素用一个结点表示
结点除了(i, j, aij)外,还有right:用于连接同一行中的下个非零元素、down:用于连接同一列中的下个非零元素
优点:
能灵活的插入因运算产生的新的非零元素,删除因运算产生的新的零元素
二:广义表
表头:第一个元素,表头可以是原子也可以是子表
表尾:除表头之外的其他元素组成的表,表尾不是最后一个元素,而是一个子表
长度:最外层所包含元素的个数
深度:将广义表展开后所含括号的重数,原子的深度为0,空表的深度为1
第六章:树和二叉树
一:定义
树是n个结点的有限集
若n=0,称为空数
若n > 0,则满足:
1.有且仅有一个特定的称为根的结点
2.其余结点课分为m个互不相交的有限集,其中每一个集合本身又是一棵树,并称为根的子树
二:基本术语
度:结点拥有的子树数
度为0的结点称为叶或终端结点
一个树的度为树内各结点的度的最大值
孩子:结点的子树的根,相应地,该结点称为孩子的双亲
祖先:从根到该结点所经分支上的所有结点
子孙:以某结点为根的子树中的任一结点
兄弟:同一个双亲的孩子
层次:从根开始,根为第一层,根的孩子为第二层,以此类推
堂兄弟:双亲在同一层的结点
深度/高度:树中结点的最大层次
有序树:树中结点的各子树从左到右是有次序的(不能互换)
无序树:树中结点的各子树从左到右没有次序(可以互换)
森林:m棵互不相交的树的集合
三:二叉树
1.定义
每个结点最多有两颗子树(度不大于2),且二叉树的子树有左右之分,其次序不能任意颠倒
二叉树结点的子树要区分左子树和右子树,即使只有一棵树也要进行区分,说明它是左子树还是右子树
2.性质
二叉树的第i层上至多有2i-1个结点
深度为k的二叉树至多有2k-1个结点
若二叉树的叶子数为n0,度为2的结点数为n2,则n0=n2+1
总分支数=总结点数+1
满二叉树:
深度为k且有2k-1个结点的二叉树
完全二叉树:
深度为k,由n个结点的二叉树,其每一个结点都与深度为k的满二叉树中编号从1至n的结点一一对应(编号从上到下,从左到右)
完全二叉树的特点:
1.叶子结点只能在层次最大的两层上出现
2.对任一结点,其右子树的最大层次为i,则其左子树的最大层次必为i或i+1
具有n个结点的完全二叉树的深度为⌊log2n⌋+1
⌊⌋是向下取整符号
对一颗有n个结点的完全二叉树的结点按层序编号,则对任一结点i有:
1.如果i = 1,则结点i是二叉树的根,无双亲;若i > 1,则其双亲是结点 ⌊i/2⌋
2.如果2i > n,则结点i为叶子结点,无左孩子;否则,其左孩子为结点2i
3.如果2i+1 > n,则结点i无右孩子;否则,其右孩子为结点2i+1
3.存储结构
3.1 顺序结构
按二叉树的结点层次编号,依次存放二叉树中的数据元素,用"0"表示不存在的结点
适合完全二叉树
#define MAX_TREE_SIZE 100 typedef TElenmType sqBiTree[MAX_TREE_SIZE]; SqBiTree bt;
3.2 链式结构
typrdef struct BiTNode { TElemType data; struvt BiTNode *lchild, *rchild; }BiTNode, *BiTree;
四:遍历二叉树
1.先序遍历二叉树:根左右
Status PreOrderTraverse (BiTree T) { if (T == NULL ) return OK; else { visit(T); PreOrderTraverse(T->lchild); PreOrderTraverse(T->rchild); } }
2.中序遍历二叉树:左根右
Status InOrderTraverse (BiTree T) { if (T == NULL ) return OK; else { InOrderTraverse(T->lchild); visit(T); InOrderTraverse(T->rchild); } }
3.后续遍历二叉树:左右根
Status PostOrderTraverse (BiTree T) { if (T == NULL ) return OK; else { PostOrderTraverse(T->lchild); PostOrderTraverse(T->rchild); visit(T); } }
时间复杂度:O(n)
空间复杂度:O(n)
4. 二叉树的层次遍历
从根结点开始,从上到下,从左到右依次访问
typedef struct { BTNode data[MaxSize]; int fornt, rear; }SqQueue; void LevelOrder (BTNode *b) { BTNode *p; SqQueue *qu; InitQueue(qu); enQueue(qu, b); while (!QueueEmpty(qu)) { deQueue(qu, p); printf ("%c " , p->data); if (p->lchild != NULL ) enQueue(qu, p->lchild); if (p->rchild != NULL ) enQueue(qu, p->rchild); } }
五:二叉树遍历算法的应用
1.二叉树的建立
按先序遍历序列建立二叉树和二叉链表
因为只有先序序列建立的树不唯一,所以要补充空结点(这里用’#'代替)
Status CreatBiTree (BiTree& T) { cin >> ch; if (ch == '#' ) T = NULL ; else { if (!(T = new BiTree)) exit (OVERFLOW); T->data = ch; CreateBiTree(T->lchild); CreateBiTree(T->rchild); } return OK; }
2.复制二叉树
int Copt (BiTree T, BiTree& NewT) { if (T == NULL ) { NewT = NULL ; return 0 ; } else { NewT = new BiNode; NewT->data = T->data; Copy(T->lchild, NewT->lchild); Copy(T->rchild, NewT->rchild); } return 1 ; }
3.计算二叉树的深度
int Depth (BiTree T) { if (T == NULL ) return 0 ; else { int m, n; m = Depth(T->lchild); n = Depth(T->rchild); m > n? return (m + 1 ):return (n + 1 ); } }
3.计算二叉树结点个数
int NodeCount (BiTree T) { if (T == NULL ) return 0 ; else return NodeCount(T->lchild) + NCount(T->rchild) + 1 ; }
4.计算二叉树叶子结点数
int NodeCount (BiTree T) { if (T == NULL ) return 0 ; if (T->lchild == NULL && T->rchild == NULL ) return 1 ; else return NodeCount(T->lchild) + NCount(T->rchild); }
六:线索二叉树
如果某个结点的左孩子为空,则将空的左孩子指针改为指向其前驱,右孩子同理
为了区分lchild和rchild是指向孩子的指针还是指向前驱/后驱的指针,对二叉链表中每个节点增设两个标志语ltag和rtag
typedef struct BiThrNode { int data; int ltag, rtag; struct BiTrNode *lchild , *rchild ; }BiThrNode, *BiThrTree;
七:树与森林
森林:m(m >= 0)棵互不相交的树的集合
1.树的存储结构
1.1 双亲表示法
特点:找双亲容易,找孩子难
typedef struct PTNode { TElemType data; int parent; }PTNode; typedef struct { PTNode nodes[MAX_TREE_SIZE]; int r; int n; }PTree;
1.2 孩子链表
找孩子容易,找双亲难
把每个结点的孩子结点排列起来,看成线性表,用单链表存储,则n个结点有n个孩子链表(叶子结点为空表),而n个头指针又组成一个线性表,用顺序表(含n个元素的结构数组)存储
typedef struct CTNode { int child; struct CTNode *next ; }*ChildPtr; typedef struct { TElemType data; ChildPtr firstchild; }CTBox; typedef struct { CTBox nodes[MAX_TREE_SIZE]; int r; int n; }CTree;
1.3 带双亲的孩子链表
1.4 孩子兄弟表示法(二叉树表示法,二叉链表表示法)
用二叉链表做树的存储结构,链表中每个结点的两个指针域分别指向其第一个孩子和下一个兄弟结点
typedef struct CSNode { ElemType data; struct CSNode *firstchild , *nextsibling ; }CSNode, *CSTree;
2.树与二叉树的转换
用二叉链表做媒介,对应关系唯一
树变二叉树:兄弟相连留长子
加线:兄弟之间加一条线
减线:对每个结点,除了左孩子外,去除与其他孩子的关系
旋转:以树的根节点为轴心,将树顺时针转45度
二叉树变树:左孩右右连双亲,去掉原来右孩线
加线:若p结点是双亲结点的左孩子,则将p的右孩子、右孩子的右孩子…沿分支找到所有右孩子,都与p的双亲连起来
剪线:减去原二叉树中双亲与右孩子间的连线
调整
3.森林与二叉树的转换
森林变树:树变二叉根相连
将各棵树都转为二叉树
将每棵树的根节点用线相连
以第一棵树的根结点作为二叉树的根,以根为轴心,顺时针旋转
二叉树变森林:去掉全部右孩线,孤立二叉再还原
将二叉树中根节点与其右孩子连线,及沿右分支搜索到的所有右孩子间连线全部去掉,使之变成孤立的二叉树
将孤立的二叉树还原成树
4.树与森林的遍历
4.1 树的遍历
4.2 森林的遍历
将森林看作由三部分构成:
1.森林中第一棵树的根节点
2.森林中第一棵树的子树森林
3.森林中其他树构成的森林
先序遍历:
按上面说的1、2、3部分的顺序遍历森林
依次从左到右对森林中的每一棵树进行先根遍历
中序遍历
按上面说的2、1、3部分的顺序遍历森林
依次从左到右对森林中的每一棵树进行后根遍历
八:Huffman树及其应用
1.基本概念
路径:从树的一个结点到另一个结点间的分支构成这两个结点间的路径
结点的路径长度:两结点间路径上的分支数
树的路径长度(TL):从根结点到每一个结点的路径长度之和
结点数目相同的二叉树中,完全二叉树是路径长度最短的二叉树。
完全二叉树是路径长度最短的数,但路径长度最短的数不一定是二叉树
权:将树中结点赋给一个有着某种含义的值,则这个值叫该结点的权
结点的带权路径长度:从根结点到该结点之间的路径长度与该结点的权的乘积
树的带权路径长度(WPL):树中所有叶子结点的带权路径长度之和
Huffman树(最优二叉树):WPL最短的二叉树
满二叉树不一定是Huffman树
Huffman树中权值越大的叶子离根越近
具有相同权结点的Huffman树不唯一
2.Huffman树的构造算法(Huffman算法)
1.构造森林全是根:
根据n个给定的权值{W1, W2, …, Wn}构成n棵二叉树森林F = {T1, T2, …, Tn},其中Ti只有一个带权为Wi的根节点
2.选用两小造新树:
在F中选取两棵根结点的权值最小的树作为左右子树,且设置新的二叉树根结点的权值为其左右子树上根结点的权值之和
2.删除两小添新人:
在F中删除这两棵树,同时将新得到的二叉树加入森林
重复2、3剩单根
Huffman树的结点的度数为0或2,没有度为1的结点
包含n个叶子结点的Huffman树中共有2n-1个结点
算法实现:用顺序存储结构——一维结构数组
typedef struct { int weight; int parent; int lch, rch; }HTNode, *HuffmanTree;
void CreatHuffmanTree (HuffmanTree HT, int n) { int i; if (n <= 1 ) return ; int m = 2 * n = 1 ; HT = new HTNode[m + 1 ]; for (i = 1 ; i <= m; i++) { HT[i].lch = 0 ; HT[i].rch = 0 ; HT[i].parent = 0 ; } for (i = 1 ; i < n; i++) cin >> HT[i].weight; for (i = n + 1 ; i <= m; i++) { Select(HT, i - 1 ; s1; s2); HT[s1].parent = HT[s2].parent = i; HT[i].lch = s1; HT[i].rch = s2; HT[i].weight = HT[s1].weight + HT[s2].weight; } }
3.Huffman编码
1.统计字符集中每个字符在电文中出现的概率(概率越大,要求编码越短)
2.利用Huffman树的特点:权越大的叶子离根越近。将每个字符的概率值作为权值,构造Huffman树。则概率越大的结点,路径越短
3.在Huffman树的每个分支上标0或1:
左分支标0,右分支标1
把从根到每个叶子的路径上的标号连接起来,作为该叶子代表的字符的编码
Huffman编码是最优前缀码
void CreatHuffmanCode (HuffmanTree HT, HuffmanCode &HC, int n) { HC = new char *[n + 1 ]; char * cd = new char [n]; cd[n - 1 ] = '\0' ; for (i = 1 ; i <= n; i++) { int start = n - 1 ; int c = i; int f = HT[i].parent; while (f != 0 ) { start--; if (HT[f].lchild == c) cd[start] = '0' ; else cd[start] = '1' ; c = f; f = HF[f].parent; } HC[i] = new char [n - start]; strcpy (HC[i], &cd[start]); } delete[]cd; }
4.文件的编码和解码
编码:
1.输入各字符及其权值
2.构造Huffman树
3.进行Huffman编码
4.查Huffman表得到各字符的Huffman编码
解码:
1.构造Huffman树
2.依次读入二进制码
3.读入0则走向左孩子,读入1则走向右孩子
4.一旦达到某叶子结点即可得到字符
第七章:图
一:图的定义和术语
完全图:任意两个点都有边相连
稀疏图:有很少边/弧的图
稠密图
权:图中边/弧所具有的相关系数叫作权,表明从一个顶点到另一个顶点的距离或耗费
网:边/弧带权的图
邻接:有边/弧相连的两个顶点间的关系
存在(Vi, Vj),则称Vi和Vj互为邻接点
存在<Vi, Vj>,则称Vi邻接到Vj,Vj邻接于Vi
关联(依附):边/弧与顶点间的关系
存在(Vi, Vj)/<Vi, Vj>,则称该边/弧关联与Vi和 Vj
顶点的度(TD):与该顶点相关联的边的数量
在有向图中,顶点的度 = 该顶点的入度 + 出度
入度(ID):以该顶点为终点的有向边的条数
出度(OD):以该顶点为起点的有向边的条数
路径:接续的边构成的顶点序列
路径长度:路径上边或弧的数目/权值之和
回路(环):第一个顶点和最后一个顶点相同的路径
简单路径:除路径起点和终点可以相同外,其余点都不相同的路径
连通图:在无向图中,对任意两个顶点v、u都存在从v到u的路径,即为连通图
强连通图:在有向图中,对任意两个顶点v、u都存在从v到u的路径,即为强连通图
子图:
极大连通子图:该子图是G的子图,将G的任何不在该子图中的顶点加入后,子图不再连通
连通分量:无向图的极大连通子图
强连通分量:有向图的极大连通子图
极小连通子图:该子图是G的子图,在该子图中删除任何一条边/弧,该子图不再连通
生成树:包含无向图G所有顶点的极小连通子图
生成森林:对非连通图,由各个连通分量生成的树的集合
二:图的存储结构
1.数组(邻接矩阵)表示法
建立一个顶点表(记录各顶点信息)和一个邻接矩阵(表示各个顶点之间的关系)
1.1 无向图的邻接矩阵
特别:完全图的邻接矩阵中,对角元素为0,其余为1
1.2 有向图的邻接矩阵
1.3 网的邻接矩阵
1.4邻接矩阵的建立
#define Maxlnt 999999 #define MVNum 100 typedef char VertexType typedef int ArcType typedef struct { VertxType vexs[MVNum]; ArcType arcs[MVNum][MVNum]; int vexnum, arcnum; }AMGraph;
Status CreateUDN (AMGraph &G) { int i, j, k; VertexType v1, v2; ArcType w; cin >> G.vexnum >> G.arcnum; for (i = 0 ; i < G.vexnum; i++) cin >> G.vexs[i]; for (i = 0 ; i < G.vexnum; i++) for (j = 0 ; j < G.vexnum; j++) G.arcs[i][j] = MAXlnt; for (k = 0 ; k < G.arcnum; k++) { cin >> v1 >> v2 >> w; i = LocateVex(G, v1); j = LocateVEX(G, v2); G.arcs[i][j] = w; G.arcs[j][i] = G.arcs[i][j]; } return OK; }
1.5 邻接矩阵的优缺点
优点:
直观,简单,好理解
方便查找任意一对顶点是否存在边
方便找任一顶点的所有邻接点
方便计算任一顶点的度
无向图:对应行或列非零元素个数
有向图:对应行非零元素的个数是出度,对应列非零元素的个数是入度
缺点:
不便于增加或删除顶点
存稀疏图会有大量无效元素
统计边数时比较浪费时间
2.链式(邻接表)表示法
2.1 无向图的邻接表
2.2 有向图的邻接表
2.3 图的邻接表存储表示
typedef struct VNode { VertexType data; ArcNode* firstarc; }VNode, AdjList[MVNium] typedef struct ArcNode { int adjvex; struct ArcNode * nextarc ; OtherInfo infi; }ArcNode; typedef struct { AdjList vertics; int vexnum, arcnum; }ALGraph;
Status CreateYDG (ALGraph &G) { int i, j, k; VertexType v1, v2; cin >> G.vexnum >> G.arcnum; for (i = 0 ; i < G,vexnum; i++) { cin >> G.vertices[i].data; G.vertices[i].firstarc = NULL ; } for (k = 0 ; k < G.arcnum; k++) { cin >> v1 >> v2; i = LocateVex(G, v1); j = LocateVex(G, v2); ArcNode *p1 = new ArcNode; p1->adjvex = j; p1->nextarc = G.vertices[i].firstarc; G.vertices[i].firstarc = p1; ArcNode *p2 = new ArcNode; p2->adjvex = i; p2->nextarc = G.vertices[j].firstarc; G.vertices[j].firstarc = p2; } return OK; }
2.4 邻接表的特点
方便找任一顶点的所有邻接点
节约稀疏图的空间
方便计算任一顶点的度
3.邻接矩阵与邻接表的关系
联系:
邻接表中每个链表对应着邻接矩阵中的一行,邻接表中的结点个数等于邻接矩阵一行中非零元素的个数
区别:
1.对任一确定的无向图,邻接矩阵唯一,邻接表不唯一
2.邻接矩阵的空间复杂度为O(n2),邻接表的空间复杂度为O(n+e)
3.邻接矩阵多用于稠密图,邻接表多用于稀疏图
4.十字链表——用于有向图
解决不方便求结点的度的问题
可以看作为有向图的邻接表和逆邻接表结合起来形成的一种链表
有向图的每一条弧对挺十字链表中的一个弧结点,每个顶点对应着十字链表中的顶点结点
5.邻接多重表——用于无向图
解决每条边都要存储两边的问题
三:图的遍历
定义:
从已给的连通图中某一顶点出发,沿着一些边访遍图中所有顶点,且每个顶点只访问一次
1.深度优先搜索(DFS)
邻接矩阵表示的无向图的深度遍历实现
void DFS (AMGraph G,int v) { int w; cout << vl << endl ; visited[v] = ture; for (w = 0 ; w < G.vexnum; w++) if (G.arcs[v][w] != 0 && !visited[w]) DFS(G, w); }
邻接矩阵的时间复杂度:O(n2)
邻接表的时间复杂度:O(n+e)
2.广度优先搜索(BFS)
void BFS (Graph G, int v) { int w; cout << vl << endl ; visited[v] = ture; InitQueue(Q); EnQueue(Q, v); while (!QueueEmpty(Q)) { DeQueue(Q, u); for (w = FirstAdjVex(G, u); w >= 0 ; w = NextAdjVex(G, u, w)) if (!visited[w]) { cout << w << endl ; visited[w] = true ; EnQueue(Q, w); } } }
邻接矩阵的时间复杂度:O(n2)
邻接表的时间复杂度:O(n+e)
四:图的应用
1.最小生成树
生成树:所有顶点由边连在一起,但没有回路
生成树的顶点个数与图的顶点个数相同
生成树是图的极小连通子图
生成树去掉一条边则非联通,加上一条边必然形成回路
生成树中任意两个顶点间路径唯一
一个有n个顶点的连通图的生成树有n-1条边
含n个顶点,n-1条边的图不一定是生成树
最小生成树(Minimun Spanning Tree):给定一个无向网,该网的所有生成树中使各边权值之和最小的树为最小生成树
MST性质
1.1 Prim算法
时间复杂度:O(n2)
适合稠密图
1.2 Kruskal算法
时间复杂度:O(eloge) e为边数
适合稀疏图
2.最短路径
2.1 单源最短路径——Dijkstra算法
2.2 所有顶点间的最短路径——Floyd算法
3.拓扑排序
有向无环图(DAG):无环的有向图,通常用来描述一个工程或系统的进行过程
AOV网:拓扑排序
用一个有向图表示一个工程的各子工程及其相互制约的关系,其中以顶点表示活动,弧表示活动之间的有限制约关系,称这种有向图为顶点表示活动的网
拓扑排序的重要应用:检测AOV网中是否存在环
检测AOV网是否存在环的方法:对有向图构造其顶点的拓扑有序序列,若网中所有顶点都在它的拓扑有序序列中,则该AOV网必无环
4.关键路径
AOE网:关键路径
用一个有向图表示一个工程的各子工程及其相互制约的关系,其中以弧表示活动,顶点表示活动之间的开始或结束事件,称这种有向图为边表示活动的网