第一章:绪论

一:基本概念和术语

  • 数据:分为数值型数据与非数值型数据
    是能输入计算机且能被计算机处理的各种符号的集合
  • 数据元素:数据的基本单位
    数据元素也叫记录、结点、顶点
  • 数据项:构成数据元素的不可分割的最小单位

数据 > 数据元素 > 数据项

  • 数据对象:性质相同的数据元素的集合

数据元素与数据的关系:是集合的个体
数据项与数据的关系:是集合的子集

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

1.逻辑结构:数据元素之间的逻辑关系
2.物理结构/存储结构:数据元素机器关系在计算机内存中的表示(又称映像)
3.运算和实现:对数据元素可以试驾的操作以及这些操作在相应的存储结构上的实现

1.逻辑结构

  • 描述数据元素之间的逻辑关系
  • 与数据的存储无关,独立于计算机
  • 是从具体问题抽醒出来的数学模型

逻辑结构的种类

  • 划分方法一:

1.线性结构:
有且仅有一个开始和一个终端结点,并且所有结点都最多有一个直接前驱和一个直接后继
例如:线性表、栈、队列、串
2.非线性结构:
一个结点可能有多个直接前驱和直接后继
例如:树、图

  • 划分方法二:四类基本逻辑结构

1.集合结构:(数据元素之间)除了同属一个集合之外无任何其他关系
2.线性结构:一对一的线性关系
3.树形结构:一对多的层次关系
4.图状结构/网状结构:多对多的任意关系
image_61f8344a.png

2.存储结构

  • 数据元素及其关系在计算机存储器中的结构(存储方式)
  • 是数据结构在计算机中的表示

存储结构的种类

  • 四种基本的存储结构

1.顺序存储结构:

  • 用一组连续的存储单元依次存储数据元素,数据元素之间的逻辑关系由元素的存储位置来表示
  • C语言中用数组来实现顺序存储结构

2.链式存储结构

  • 用一组任意的存储单元存储数据结构,数据元素之间的逻辑关系用指针来表示
  • C语言中用指针(链表)来实现存储结构

3.索引存储结构

  • 在存储结点信息的同时还建立附加的索引表

4.散列存储结构

  • 根据结点的关键字直接计算出该结点的存储地址

3.逻辑结构与存储结构的关系

  • 存储结构是逻辑关系的映像与元素本身的映像
  • 逻辑结构是数据结构的抽象,存储结构是数据结构的实现
  • 两者综合起来建立了数据元素之间的结构关系

4.数据类型和抽象数据类型

1.数据类型

数据类型 = 值的集合 + 值集合上的一组操作

  • 高级语言中的数据类型明显地或隐含地规定了程序执行期间变量和表达的所有可能的取值范围,以及这些数值范围上所允许的操作
  • 作用:约束变量或常量的取值范围和操作
    2.抽象数据类型(ADT)
  • 指一个数学模型以及定义在此数学模型上的一组操作
  • 由用户定义,从问题抽象出数据模型(逻辑结构)
  • 还包括定义在数据模型上的一组抽象运算(相关操作)
  • 不考虑计算机内的具体存储结构与运算的具体实现算法

抽象数据类型的形式化定义:
抽象数据类型可用D、S、P三元组表示

  • D数据对象

  • S是D上的关系集

  • P是对D的基本操作集
    定义格式:
    ADT 抽象数据类型名{
    数据对象:<数据对象的定义>
    数据关系:<数据关系的定义>
    基本操作:<基本操作的定义>
    } ADT 抽象数据类型名

  • 数据对象、数据关系的定义用伪代码描述

  • 基本操作的定义格式为:
    基本操作名(参数表)
    初始条件:<初始条件描述>
    操作结果:<操作结果描述>

  • ADT定义举例:image_05e303b6.png

5.小结

image_7bd64b86.png
image_accbb055.png

二:抽象数据类型的表示与实现

  • 实现ADT“复数”
#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.事前估算:
    算法运行时间 = ∑每条语句的执行次数(又称语句频度) * 该语句执行一次所需时间
    image_5c0f68f1.png
  • 算法时间复杂度:
    算法中基本语句重复执行的次数是问题规模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”表示
    image_404984e0.png
    image_68e1994e.png
    image_72b6166e.png
  • 有些情况下,算法中基本操作重复执行的次数还随问题的输入数据集不同而不同
    image_fbedb869.png
  • 平均时间复杂度:在所有可能输入实例在等概率出现的情况下,算法的期望运行时间
    一般情况下总是考虑算法的最坏时间复杂度
  • 对于复杂的算法,可以将其分成几个容易估算的部分,然后利用 O 加法法则和乘法法则计算时间复杂度
    image_434503ed.png
  • 当n取得很大时,指数时间算法和多项式时间算法在所需的时间上非常悬殊image_40848984.png

时间复杂的按数量级递增的顺序为:
image_53e24c09.png

2.算法的空间效率

  • 空间复杂度:算法所需存储空间的度量
    S(n) = O(f(n)) n为问题的规模
  • 算法要占据的空间包括:
    算法本身要占据的空间,输入/输出、指令、常数、变量等
    算法要使用的辅助空间
    image_460df92e.png

3.设计好算法的过程

  • 抽象数据类型 = 数据的逻辑结构 + 抽象运算
  • 根据时间复杂度和空间复杂度选择最优算法

四:第一章小结

image_fc6eaa8a.png

第二章:线性表

补充:C/C++基础知识

1.链表相关知识:

[c++ 链表基础知识][c_]

2.动态内存申请相关知识:

[C/C++ 内存的动态申请与释放][C_C_]

一:线性表的定义和特点

线性表是具有相同特性的数据元素的一个有限序列
image_b0123ba2.png
顺序存储结构存在的问题:

  • 存储空间不灵活

  • 运算的空间复杂度高
    优点:

  • 存储密度大

  • 可以随机存取表中任一元素

缺点:

  • 在插入、删除某一元素时,需要移动大量元素
  • 浪费存储空间
  • 是静态存储,数据元素的个数不能自由扩充

二:线性表的顺序表示和实现

  • 抽象数据类型线性表的定义
    image_6d5a60c7.png
  • 顺序存储的定义:逻辑上相邻,物理上也相邻

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 //<math.h>中已有OVERFLOW,因此换一下
#define LIST_INIT_SIZE 100 //初始大小为100,可按需修改
#define LISTINCREMENT 10 //空间分配增量,课按需修改
typedef int Status; //函数调用状态
#define ELEMTYPE_IS_INT //数据类型
//#define ELEMTYPE_IS_DOUBLE
//#define ELEMTYPE_IS_CHAR_ARRAY
//#define ELEMTYPE_IS_CHAR_P
//#define ELEMTYPE_IS_STRUCT_STUDENT
//#define ELEMTYPE_IS_STRUCT_STUDENT_P
#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); //构造一个空的线性表L
Status DestroyList(sqlist* L); //销毁线性表L
Status ClearList(sqlist* L); //将线性表L置为空表
Status ListEmpty(sqlist L); //若L为空表返回TURE,否则返回FALSE
int ListLength(sqlist L); //返回L中数据元素个数
Status Getelem(sqlist L, int i, ElemType* e); //用e返回L中第i个数据元素的值
int LocateElem(sqlist L, ElemType e, Status(*compare)(ElemType e1, ElemType e2)); //返回L中第一个与e满足关系compare()的数据元素的位序,若不存在,返回0
Status PriorElem(sqlist L, ElemType cur_e, ElemType* pre_e, Status(*compare)(ElemType e1, ElemType e2)); //用pre_e返回cur_e的前驱
Status NextElem(sqlist L, ElemType cur_e, ElemType* next_e, Status(*compare)(ElemType e1, ElemType e2)); //用next_e返回cur_e的后驱
Status ListInsert(sqlist* L, int i, ElemType e); //在L的第i个位置插入元素e
Status ListDelete(sqlist* L, int i, ElemType* e); //删除L的第i个元素,并用e返回其值
Status ListTraverse(sqlist L, Status(*visit)(ElemType e)); //依次对L的每个数据元素调用visit()函数

2.liner_list_sq.c(具体函数的实现)

#include <stdio.h>
#include <stdlib.h> //malloc//realloc函数
#include <string.h> //strcpy/strcmp等函数
#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)
{
/*未执行InitList,直接执行本函数可能出错,因为指针初值未定*/
/*当类型是char *,struct student*时,要先释放二次申请空间*/
#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)
{
/*当类型是char *,struct student*时,要先释放二次申请空间*/
#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;
}
  • main中用于比较两值是否相等的函数
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;
}

image_82784056.png

/*插入元素*/
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]);
/*从最后一个开始到第i个元素一次往后移一格*/
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;
}

image_0f44c333.png

/*删除元素并返回其值*/
Status ListDelete(sqlist* L, int i, ElemType* e)
{
ElemType* p, * q;
if (i < 1 || i > L->length)
return ERROR;
p = &(L->elem[i - 1]); //指向第i个元素
/*取第i个元素的值放入i中*/
#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
/*从第i+1到最后,依次前移一格*/
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--; //长度-1
return OK;
}

image_44d1ad17.png

/*遍历线性表*/
Status ListTraverse(sqlist L, Status(*visit)(ElemType e))
{
extern int line_count; //main中定义的换行计数器
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;
}

三:线性表的链式表示和实现

单链表:结点只有一个指针域
双链表:结点有两个指针域
循环链表:首尾相接
image_1dcfc9d9.png

  • 头结点
    image_d6622602.png
头结点的好处:
  • 便于首元结点的处理
  • 便于空表和非空表的统一处理

头结点的数据域可以为空,也可与存放线性表长度等信息,但此节点不能计入链表长度值

四:双向链表

1.双向链表的插入

image_9cbe4f49.png

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

p->prior = s 必须在 p->prior->next = s 之后,其他顺序随意

2.双向链表的删除

image_886ddeb0.png

p->prior->next = p->next;
p->next->prior = p->prior;

五:单链表、循环链表、双向链表的时间效率比较

image_3a0d15b7.png

六:顺序表和链表的比较

  • 链式存储结构:
  • 优点:
    1.结点空间可以动态申请和释放
    2.插入和删除操作方便
  • 缺点:
    1.存储密度小
    2.进行查找操作时比较困难

image_1baf856b.png
image_956bf726.png

七:线性表的应用

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.有序表的合并

image_76e707cf.png

  • 用顺序表实现
void MergeList_sq(List La,List Lb, SqList &Lc)
{
pa = La.elem; //指向La的首元素
pb = Lb.elem; //指向Lb的首元素
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; //指向La的尾元素
pb_last = Lb.elem + Lb.length - 1; //指向Lb的尾元素
while(pa <= pa_last && pb <= pb_last)
{
if(*pa <= *pb)
*pc++ = *pa++;
else
*pc++ = *pb++;
}
while(pa <= pa_last) //若La还有剩余元素,插入Lc中
*pc++ = *pa++;
while(pb <= pb_last) //若Lb还有剩余元素,插入Lc中
*pc++ = *pb++;
}

该算法时间复杂度:O(ListLenth(La) + ListLengrh(Lb))
该算法空间复杂度:O(ListLenth(La) + ListLengrh(Lb))

  • 用链表实现
    image_dacc2d19.png
    image_99d0f0ed.png
    image_9a569e8f.png
    image_1d117b08.png
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.一元多项式的运算:加、减、乘

image_a6aff376.png

```
### 2.稀疏多项式的运算
![image_45f86a40.png](https://czrui99.oss-cn-chengdu.aliyuncs.com/image_45f86a40.png)
![image_be808582.png](https://www.liangtengyu.com:9998/images/image_be808582.png)
![image_cc8079b5.png](https://czrui99.oss-cn-chengdu.aliyuncs.com/image_cc8079b5.png)
![image_f707b34f.png](https://czrui99.oss-cn-chengdu.aliyuncs.com/image_f707b34f.png)
* 多项式的建立
```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]; //ISBN
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):从栈顶删除一个元素
    image_644dfd93.png
  • 示意图:
    image_c84f0981.png
  • 栈的应用:
    image_7d52650d.png

2.队列的定义和特点

队列(FIFO):先进先出

  • 队列的应用:
    image_91c32789.png

二:案例引入

1.进制转换

image_c622287e.png

2.括号匹配的检验

3.表达式的组成

image_2d1a2ae2.png
image_30ce19cf.png

三:栈的表示和实现

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;
}

image_76d200fe.png

2.KMP算法

[天勤公开课:KPM算法(易懂版)][KPM]
[KMP详解][KMP]

  • 利用已经部分匹配的结果而加快模式串的滑动速度
  • 主串S的指针i不必回溯,可提速到O(n + m)
    image_49ef8ab0.png
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]
image_aef22aac.pngimage_ab84b360.png

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;
}

image_4b849faa.png

将next[i]的值对应的位的值与i的值进行比较,若相等,nextval[i]=nextval【next[i]】;若不相等,则nextval[i]=next[i]
image_07a2073c.png

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
image_c723fdca.png

2.三角矩阵

对角线以下/上的元素都是同样的数
存储方法同对称矩阵
image_aef8d088.png

3.对角矩阵

所有非零元素都集中在以主对角线为中心的带状区域中image_7e55ca1f.pngimage_7ea9edbb.png

4.稀疏矩阵

矩阵中大部分都是零元素(95%)
三元组法:

  • 用三元组(i, j, aij)表示
  • 通常加一个“总体”信息(总行数, 总列数, 非零元素个数)

优点:

  • 非零元在表中按行序存储,便于进行按行顺序处理的运算

缺点:

  • 不能随机存储,若按行号存取某一行中的非零元,则需从头开始进行查找
    image_6d17d433.png
typedef struct{
int i, j; //非零元的行下标和;列下标
ElemType e;
}Triple;
typedef struct{
Triple data[MAXSIZE + 1]; //非零元三元组表,data[0]不用
int mu, nu, tu; //矩阵的行数、列数、非零元个数
}TSMatrix;

十字链表法:

  • 每个非零元素用一个结点表示
  • 结点除了(i, j, aij)外,还有right:用于连接同一行中的下个非零元素、down:用于连接同一列中的下个非零元素

优点:

  • 能灵活的插入因运算产生的新的非零元素,删除因运算产生的新的零元素
    image_4376af5c.png

二:广义表

  • 表头:第一个元素,表头可以是原子也可以是子表
  • 表尾:除表头之外的其他元素组成的表,表尾不是最后一个元素,而是一个子表
  • 长度:最外层所包含元素的个数
  • 深度:将广义表展开后所含括号的重数,原子的深度为0,空表的深度为1
    image_285195dd.pngimage_861cc0df.pngimage_1fd554ec.png

第六章:树和二叉树

一:定义

  • 结点之间有分支
  • 具有层次结构

树是n个结点的有限集

  • 若n=0,称为空数
  • 若n > 0,则满足:
    1.有且仅有一个特定的称为根的结点
    2.其余结点课分为m个互不相交的有限集,其中每一个集合本身又是一棵树,并称为根的子树
    image_23f13c6b.png

二:基本术语

  • 度:结点拥有的子树数
  • 度为0的结点称为叶或终端结点
  • 一个树的度为树内各结点的度的最大值
  • 孩子:结点的子树的根,相应地,该结点称为孩子的双亲
  • 祖先:从根到该结点所经分支上的所有结点
  • 子孙:以某结点为根的子树中的任一结点
  • 兄弟:同一个双亲的孩子
  • 层次:从根开始,根为第一层,根的孩子为第二层,以此类推
  • 堂兄弟:双亲在同一层的结点
  • 深度/高度:树中结点的最大层次
  • 有序树:树中结点的各子树从左到右是有次序的(不能互换)
  • 无序树:树中结点的各子树从左到右没有次序(可以互换)
  • 森林:m棵互不相交的树的集合image_e148d05b.png

三:二叉树

1.定义

每个结点最多有两颗子树(度不大于2),且二叉树的子树有左右之分,其次序不能任意颠倒

  • 二叉树结点的子树要区分左子树和右子树,即使只有一棵树也要进行区分,说明它是左子树还是右子树
    image_9ada10dc.pngimage_4bddcabc.pngimage_e039ca00.png

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
  • image_e7cef83e.png

3.存储结构

3.1 顺序结构

按二叉树的结点层次编号,依次存放二叉树中的数据元素,用"0"表示不存在的结点

适合完全二叉树

#define MAX_TREE_SIZE 100  //二叉树的最大结点数
typedef TElenmType sqBiTree[MAX_TREE_SIZE]; //0号单元存储根节点
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)
image_932ba0e9.png

4. 二叉树的层次遍历

从根结点开始,从上到下,从左到右依次访问
image_4708fd9b.png

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); //出队结点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);
}

六:线索二叉树

如果某个结点的左孩子为空,则将空的左孩子指针改为指向其前驱,右孩子同理
image_f27431b8.png
为了区分lchild和rchild是指向孩子的指针还是指向前驱/后驱的指针,对二叉链表中每个节点增设两个标志语ltag和rtag
image_41109034.png

typedef struct BiThrNode
{
int data;
int ltag, rtag;
struct BiTrNode *lchild, *rchild;
}BiThrNode, *BiThrTree;

image_a98d5977.png
image_8c9f306f.png
image_44330fc9.png

七:树与森林

森林:m(m >= 0)棵互不相交的树的集合

1.树的存储结构

1.1 双亲表示法

特点:找双亲容易,找孩子难
image_e5dc7bc3.png

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个元素的结构数组)存储
image_5461ea1e.png

//孩子结点结构
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 带双亲的孩子链表

image_3a3a94de.png

1.4 孩子兄弟表示法(二叉树表示法,二叉链表表示法)

用二叉链表做树的存储结构,链表中每个结点的两个指针域分别指向其第一个孩子和下一个兄弟结点
image_a8145e31.png

typedef struct CSNode
{
ElemType data;
struct CSNode *firstchild, *nextsibling;
}CSNode, *CSTree;

2.树与二叉树的转换

用二叉链表做媒介,对应关系唯一
image_018f8986.png
树变二叉树:兄弟相连留长子

  • 加线:兄弟之间加一条线
  • 减线:对每个结点,除了左孩子外,去除与其他孩子的关系
  • 旋转:以树的根节点为轴心,将树顺时针转45度
    image_f5e2ff78.png

二叉树变树:左孩右右连双亲,去掉原来右孩线

  • 加线:若p结点是双亲结点的左孩子,则将p的右孩子、右孩子的右孩子…沿分支找到所有右孩子,都与p的双亲连起来
  • 剪线:减去原二叉树中双亲与右孩子间的连线
  • 调整
    image_43121fc4.png

3.森林与二叉树的转换

森林变树:树变二叉根相连

  • 将各棵树都转为二叉树
  • 将每棵树的根节点用线相连
  • 以第一棵树的根结点作为二叉树的根,以根为轴心,顺时针旋转
    image_03d36b84.png

二叉树变森林:去掉全部右孩线,孤立二叉再还原

  • 将二叉树中根节点与其右孩子连线,及沿右分支搜索到的所有右孩子间连线全部去掉,使之变成孤立的二叉树
  • 将孤立的二叉树还原成树
    image_1eca3c5a.png

4.树与森林的遍历

4.1 树的遍历

image_45a2abf5.png

4.2 森林的遍历

将森林看作由三部分构成:

  • 1.森林中第一棵树的根节点
  • 2.森林中第一棵树的子树森林
  • 3.森林中其他树构成的森林
    image_c5f5e8a5.png
  • 先序遍历:
    按上面说的1、2、3部分的顺序遍历森林
    依次从左到右对森林中的每一棵树进行先根遍历
  • 中序遍历
    按上面说的2、1、3部分的顺序遍历森林
    依次从左到右对森林中的每一棵树进行后根遍历
    image_27244a03.png

八: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剩单根
    image_5552eda4.png

Huffman树的结点的度数为0或2,没有度为1的结点
包含n个叶子结点的Huffman树中共有2n-1个结点
image_27874898.png
算法实现:用顺序存储结构——一维结构数组

typedef struct
{
int weight;
int parent;
int lch, rch;
}HTNode, *HuffmanTree;

image_123d843b.png

void CreatHuffmanTree(HuffmanTree HT, int n)
{
int i;
if(n <= 1)
return;
int m = 2 * n = 1;
HT = new HTNode[m + 1]; //0号单元不用,HT[m]为根节点
for(i = 1; i <= m; i++) //初始化
{
HT[i].lch = 0;
HT[i].rch = 0;
HT[i].parent = 0;
}
for(i = 1; i < n; i++) //输入前n个元素的weight值
cin >> HT[i].weight;
for (i = n + 1; i <= m; i++) //合并产生n-1个结点——构造Huffman树
{
Select(HT, i - 1; s1; s2); //在HT[1..i-1]中选两个双亲域为0且权值最小的结点,并返回他们在HT中的序号s1、s2
HT[s1].parent = HT[s2].parent = i; //删除s1、s2
//设置左右孩子
HT[i].lch = s1;
HT[i].rch = s2;
HT[i].weight = HT[s1].weight + HT[s2].weight; //设置权值
}
}

3.Huffman编码

image_cc5e7bea.png
image_289d3d24.png

  • 1.统计字符集中每个字符在电文中出现的概率(概率越大,要求编码越短)
  • 2.利用Huffman树的特点:权越大的叶子离根越近。将每个字符的概率值作为权值,构造Huffman树。则概率越大的结点,路径越短
  • 3.在Huffman树的每个分支上标0或1:
    左分支标0,右分支标1
    把从根到每个叶子的路径上的标号连接起来,作为该叶子代表的字符的编码
    image_d80f90c4.png
    image_1720ddc7.png

Huffman编码是最优前缀码

void CreatHuffmanCode(HuffmanTree HT, HuffmanCode &HC, int n)
{
HC = new char*[n + 1]; //分配n个字符编码的头指针矢量
char* cd = new char[n]; //分配存放临时存放编码的动态数组空间
cd[n - 1] = '\0';
//逐字符求Huffman编码
for(i = 1; i <= n; i++)
{
int start = n - 1;
int c = i;
int f = HT[i].parent;
while(f != 0)
{
start--; //回溯一次start向前指一个位置
if(HT[f].lchild == c)
cd[start] = '0';
else //是右孩子
cd[start] = '1';
//继续向上回溯
c = f;
f = HF[f].parent;
}//end of while
HC[i] = new char[n - start]; //为第i个字符串编码分配空间
strcpy(HC[i], &cd[start]); //将求得的编码复制
}//end of for
delete[]cd;
}

4.文件的编码和解码

编码:

  • 1.输入各字符及其权值
  • 2.构造Huffman树
  • 3.进行Huffman编码
  • 4.查Huffman表得到各字符的Huffman编码

解码:

  • 1.构造Huffman树
  • 2.依次读入二进制码
  • 3.读入0则走向左孩子,读入1则走向右孩子
  • 4.一旦达到某叶子结点即可得到字符

第七章:图

一:图的定义和术语

image_6e6082f7.png

  • 完全图:任意两个点都有边相连
    image_7bd22e22.png
  • 稀疏图:有很少边/弧的图
  • 稠密图
  • 权:图中边/弧所具有的相关系数叫作权,表明从一个顶点到另一个顶点的距离或耗费
  • 网:边/弧带权的图
  • 邻接:有边/弧相连的两个顶点间的关系
    存在(Vi, Vj),则称Vi和Vj互为邻接点
    存在<Vi, Vj>,则称Vi邻接到Vj,Vj邻接于Vi
  • 关联(依附):边/弧与顶点间的关系
    存在(Vi, Vj)/<Vi, Vj>,则称该边/弧关联与Vi和 Vj
  • 顶点的度(TD):与该顶点相关联的边的数量
    在有向图中,顶点的度 = 该顶点的入度 + 出度
    入度(ID):以该顶点为终点的有向边的条数
    出度(OD):以该顶点为起点的有向边的条数
  • 路径:接续的边构成的顶点序列
  • 路径长度:路径上边或弧的数目/权值之和
  • 回路(环):第一个顶点和最后一个顶点相同的路径
  • 简单路径:除路径起点和终点可以相同外,其余点都不相同的路径
    image_26563503.png
  • 连通图:在无向图中,对任意两个顶点v、u都存在从v到u的路径,即为连通图
    image_c17cba7a.png
  • 强连通图:在有向图中,对任意两个顶点v、u都存在从v到u的路径,即为强连通图
    image_43670d9e.png
  • 子图:image_000a4144.png
  • 极大连通子图:该子图是G的子图,将G的任何不在该子图中的顶点加入后,子图不再连通
  • 连通分量:无向图的极大连通子图
  • 强连通分量:有向图的极大连通子图
  • 极小连通子图:该子图是G的子图,在该子图中删除任何一条边/弧,该子图不再连通
  • 生成树:包含无向图G所有顶点的极小连通子图
    image_98116873.png
  • 生成森林:对非连通图,由各个连通分量生成的树的集合

二:图的存储结构

1.数组(邻接矩阵)表示法

  • 建立一个顶点表(记录各顶点信息)和一个邻接矩阵(表示各个顶点之间的关系)
    image_8e6677c8.png

1.1 无向图的邻接矩阵

image_704ea6ef.png

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

1.2 有向图的邻接矩阵

image_1b6863fc.png

1.3 网的邻接矩阵

image_58407276.png

1.4邻接矩阵的建立

#define Maxlnt 999999  //表示无穷大
#define MVNum 100 //最大顶点数
typedef char VertexType //顶点数据类型为字符型
typedef int ArcType //边的权值类型为int型
typedef struct
{
VertxType vexs[MVNum]; //顶点表
ArcType arcs[MVNum][MVNum]; //邻接矩阵
int vexnum, arcnum; //图当前点数和边数
}AMGraph; //Adjacency Matrix Graph
//用邻接矩阵表示法创建无向网
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); //确定v1在G中的位置
j = LocateVEX(G, v2); //确定v2在G中的位置
G.arcs[i][j] = w; //邻接矩阵赋值
G.arcs[j][i] = G.arcs[i][j]; //无向图,反过来值相同
}
return OK;
}

1.5 邻接矩阵的优缺点

优点:

  • 直观,简单,好理解
  • 方便查找任意一对顶点是否存在边
  • 方便找任一顶点的所有邻接点
  • 方便计算任一顶点的度
    无向图:对应行或列非零元素个数
    有向图:对应行非零元素的个数是出度,对应列非零元素的个数是入度

缺点:

  • 不便于增加或删除顶点
  • 存稀疏图会有大量无效元素
  • 统计边数时比较浪费时间

2.链式(邻接表)表示法

image_b488b81d.png

2.1 无向图的邻接表

image_5e81698e.png

2.2 有向图的邻接表

image_cdc45871.png

2.3 图的邻接表存储表示

//顶点的结点结构
typedef struct VNode
{
VertexType data; //顶点信息
ArcNode* firstarc; //指向第一条依附该顶点的边的指针
}VNode, AdjList[MVNium]
//例如:VNode v[MVNum] 相当于 AdjList v
//弧/边的结点结构
typedef struct ArcNode
{
int adjvex; //该边所指向的顶点的位置
struct ArcNode* nextarc; //指向下一条边的指针
OtherInfo infi; //和边相关的信息
}ArcNode;
//图的结构定义
typedef struct
{
AdjList vertics; //顶点表
int vexnum, arcnum; //顶点数和边数
}ALGraph;

image_28dba5f3.png
image_d87bfb5f.png

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; //头插法,将*p1插入顶点Vi的边表头部
G.vertices[i].firstarc = p1;
ArcNode *p2 = new ArcNode; //生成边结点
p2->adjvex = i;
p2->nextarc = G.vertices[j].firstarc; //头插法,将*p2插入顶点Vj的边表头部
G.vertices[j].firstarc = p2;
}
return OK;
}

2.4 邻接表的特点

  • 方便找任一顶点的所有邻接点
  • 节约稀疏图的空间
  • 方便计算任一顶点的度

3.邻接矩阵与邻接表的关系

image_d770c05b.png

  • 联系:
    邻接表中每个链表对应着邻接矩阵中的一行,邻接表中的结点个数等于邻接矩阵一行中非零元素的个数
  • 区别:
    1.对任一确定的无向图,邻接矩阵唯一,邻接表不唯一
    2.邻接矩阵的空间复杂度为O(n2),邻接表的空间复杂度为O(n+e)
    3.邻接矩阵多用于稠密图,邻接表多用于稀疏图

4.十字链表——用于有向图

解决不方便求结点的度的问题
可以看作为有向图的邻接表和逆邻接表结合起来形成的一种链表
有向图的每一条弧对挺十字链表中的一个弧结点,每个顶点对应着十字链表中的顶点结点
image_b0518040.png

5.邻接多重表——用于无向图

解决每条边都要存储两边的问题
image_f1a3c31c.png

三:图的遍历

  • 定义:
    从已给的连通图中某一顶点出发,沿着一些边访遍图中所有顶点,且每个顶点只访问一次

1.深度优先搜索(DFS)

image_e86ba8ff.png
image_12e4e159.png

  • 邻接矩阵表示的无向图的深度遍历实现
    image_db6146fa.png
void DFS(AMGraph G,int v)
{
int w;
//访问第v个结点(起点)
cout << vl << endl;
visited[v] = ture;
//依次检查邻接矩阵v所在的行
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)

image_ec200a7e.png

  • 按广度优先非递归遍历连通图G
void BFS(Graph G, int v)
{
int w;
//访问第v个结点(起点)
cout << vl << endl;
visited[v] = ture;
//初始化队列
InitQueue(Q);
EnQueue(Q, v); //v进队
while(!QueueEmpty(Q)) //队列非空
{
DeQueue(Q, u); //队头元素出队并置为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); //w进队
}
}
}

邻接矩阵的时间复杂度:O(n2)
邻接表的时间复杂度:O(n+e)

四:图的应用

1.最小生成树

生成树:所有顶点由边连在一起,但没有回路

  • 生成树的顶点个数与图的顶点个数相同
  • 生成树是图的极小连通子图
  • 生成树去掉一条边则非联通,加上一条边必然形成回路
  • 生成树中任意两个顶点间路径唯一
  • 一个有n个顶点的连通图的生成树有n-1条边
  • 含n个顶点,n-1条边的图不一定是生成树
    image_ec829818.png
    最小生成树(Minimun Spanning Tree):给定一个无向网,该网的所有生成树中使各边权值之和最小的树为最小生成树
    image_fedefd23.png
  • MST性质
    image_06d88f57.pngimage_2d786099.png

1.1 Prim算法

image_359c29c5.png

时间复杂度:O(n2)
适合稠密图

1.2 Kruskal算法

image_553893c4.png

时间复杂度:O(eloge) e为边数
适合稀疏图

2.最短路径

image_795c8fa2.png

2.1 单源最短路径——Dijkstra算法

image_45e5d081.png
image_82b7bb07.png
image_b398bc8e.png

2.2 所有顶点间的最短路径——Floyd算法

image_802432b0.png

image_84a759ef.png

3.拓扑排序

  • 有向无环图(DAG):无环的有向图,通常用来描述一个工程或系统的进行过程
    image_99c640b1.png

AOV网:拓扑排序
用一个有向图表示一个工程的各子工程及其相互制约的关系,其中以顶点表示活动,弧表示活动之间的有限制约关系,称这种有向图为顶点表示活动的网
image_c4f86904.png

image_d73aa80d.png

image_0073b809.png
image_36ca44f0.png

拓扑排序的重要应用:检测AOV网中是否存在环

检测AOV网是否存在环的方法:对有向图构造其顶点的拓扑有序序列,若网中所有顶点都在它的拓扑有序序列中,则该AOV网必无环

4.关键路径

AOE网:关键路径
用一个有向图表示一个工程的各子工程及其相互制约的关系,其中以弧表示活动,顶点表示活动之间的开始或结束事件,称这种有向图为边表示活动的网
image_269355ae.png
image_16f9ffae.png
image_5501b860.png
image_bb9bed77.png
image_536479e8.png
image_431b62d5.png
image_513faae5.png