数据结构
第一章:绪论
一:基本概念和术语
- 数据:分为数值型数据与非数值型数据
是能输入计算机且能被计算机处理的各种符号的集合 - 数据元素:数据的基本单位
数据元素也叫记录、结点、顶点
- 数据项:构成数据元素的不可分割的最小单位
数据 > 数据元素 > 数据项
- 数据对象:性质相同的数据元素的集合
数据元素与数据的关系:是集合的个体
数据项与数据的关系:是集合的子集
- 数据结构:相互之间存在一种或多种特定关系的数据元素的集合
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三元组表示
- D数据对象
- S是D上的关系集
- P是对D的基本操作集
定义格式:
ADT 抽象数据类型名{
数据对象:<数据对象的定义>
数据关系:<数据关系的定义>
基本操作:<基本操作的定义>
} ADT 抽象数据类型名
- 数据对象、数据关系的定义用伪代码描述
- 基本操作的定义格式为:
基本操作名(参数表)
初始条件:<初始条件描述>
操作结果:<操作结果描述>
- ADT定义举例:

5.小结


二:抽象数据类型的表示与实现
- 实现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.事前估算:
算法运行时间 = ∑每条语句的执行次数(又称语句频度) * 该语句执行一次所需时间

- 算法时间复杂度:
算法中基本语句重复执行的次数是问题规模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.链表相关知识:
2.动态内存申请相关知识:
一:线性表的定义和特点
线性表是具有相同特性的数据元素的一个有限序列

顺序存储结构存在的问题:
- 存储空间不灵活
- 运算的空间复杂度高
优点:
- 存储密度大
- 可以随机存取表中任一元素
缺点:
- 在插入、删除某一元素时,需要移动大量元素
- 浪费存储空间
- 是静态存储,数据元素的个数不能自由扩充
二:线性表的顺序表示和实现
- 抽象数据类型线性表的定义

- 顺序存储的定义:逻辑上相邻,物理上也相邻
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;
}

/*插入元素*/
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;
}

/*删除元素并返回其值*/
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;
}

/*遍历线性表*/
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;
}
三:线性表的链式表示和实现
单链表:结点只有一个指针域
双链表:结点有两个指针域
循环链表:首尾相接
- 头结点

头结点的好处:
- 便于首元结点的处理
- 便于空表和非空表的统一处理
头结点的数据域可以为空,也可与存放线性表长度等信息,但此节点不能计入链表长度值
四:双向链表
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; //指向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))
- 用链表实现




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.稀疏多项式的运算




- 多项式的建立
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):从栈顶删除一个元素

-
示意图:

-
栈的应用:

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算法
- 利用已经部分匹配的结果而加快模式串的滑动速度
- 主串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];
}
}
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]; //非零元三元组表,data[0]不用
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]; //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)

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); //出队结点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]; //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编码


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


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.一旦达到某叶子结点即可得到字符
第七章:图
一:图的定义和术语

-
完全图:任意两个点都有边相连

-
稀疏图:有很少边/弧的图
-
稠密图
-
权:图中边/弧所具有的相关系数叫作权,表明从一个顶点到另一个顶点的距离或耗费
-
网:边/弧带权的图
-
邻接:有边/弧相连的两个顶点间的关系
存在(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 //边的权值类型为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.链式(邻接表)表示法

2.1 无向图的邻接表

2.2 有向图的邻接表

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;


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.邻接矩阵与邻接表的关系

- 联系:
邻接表中每个链表对应着邻接矩阵中的一行,邻接表中的结点个数等于邻接矩阵一行中非零元素的个数 - 区别:
1.对任一确定的无向图,邻接矩阵唯一,邻接表不唯一
2.邻接矩阵的空间复杂度为O(n2),邻接表的空间复杂度为O(n+e)
3.邻接矩阵多用于稠密图,邻接表多用于稀疏图
4.十字链表——用于有向图
解决不方便求结点的度的问题
可以看作为有向图的邻接表和逆邻接表结合起来形成的一种链表
有向图的每一条弧对挺十字链表中的一个弧结点,每个顶点对应着十字链表中的顶点结点

5.邻接多重表——用于无向图
解决每条边都要存储两边的问题

三:图的遍历
- 定义:
从已给的连通图中某一顶点出发,沿着一些边访遍图中所有顶点,且每个顶点只访问一次
1.深度优先搜索(DFS)


- 邻接矩阵表示的无向图的深度遍历实现

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)

- 按广度优先非递归遍历连通图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条边的图不一定是生成树

最小生成树(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网必无环
4.关键路径
AOE网:关键路径
用一个有向图表示一个工程的各子工程及其相互制约的关系,其中以弧表示活动,顶点表示活动之间的开始或结束事件,称这种有向图为边表示活动的网







第八章:查找
一:基本概念
- 查找:
根据给定的某个值,在查找表中确定一个与其关键字等于给定值的数据元素(或记录) - 关键字:
用来表示一个数据元素(或记录)的某个数据项的值
主关键字:
可以唯一地表示一个记录的关键字
次关键字:
用以识别若干记录的关键字 - 查找表:
由同一类型的数据元素(或记录)构成的集合。由于集合中的数据元素之间存在着松散的关系,因此查找表是一种应用灵便的结构
静态查找表:
做查询、检索操作的查找表
动态查找表:
做插入、删除操作的查找表 - 对查找表常进行的几个操作:
1.查询某个特定的数据元素是否在查找表中
2.检索某个特定的数据元素的各种属性
3.如果查询结果为不存在,则在查找表中插入一个数据元素
4.如果查询结果为存在,则删除查找表中的某个数据元素 - 查找算法的评价标准:
- 关键字的平均比较次数,也叫平均查找长度(ASL)

二:线性表的查找
1.顺序(线性)查找
优点:
算法简单,逻辑次序无要求,不同存储结构均适用
缺点:
ASL太长,时间效率低
查找范围:
- 顺序表/线性表表示的静态查找表
表内元素之间无序
//数据元素类型定义
typedef struct{
KeyType key; //关键字域
... //其他域
}ElemType;
typedef struct{
ElemType *R; //表基址
int length; //表长
}SSTable; //Sequential Search Table
SSTable ST; //定义顺序表ST
//在ST中查找值为key的值
int Search_Seq(SSTable ST, KeyType key){
//此查找表为从1开始,0号位置为空,可按需更改
for (int i = 1; i <= ST.length; i++)
if(ST.R[i].key == key)
return i;
return 0;
//其他形式
/*for(i = ST.length; ST.R[i].key != key && i > 0; i--)
;
return i*/
}
改进:
把待查关键字key存入表头(哨兵/监视哨),从后往前逐个比较,可以免去查找过程中每一步都要检查是否查找完毕的步骤
int Search_Seq(SSTable ST, KeyType key){
ST.R[0].key = key;
for(i = ST.length; ST.R[i].key != key; i--)
;
return i
}



2.二分查找
仅适用于有序表,且仅限于顺序存储结构

int Seaarch_Bin(SSTable ST, KeyType key)
{
int low = 1, high = ST.length; //初始化
while(low <= high)
{
int mid = (low + high) / 2;
if(ST.R[mid].key == key)
return mid;
else if(key < ST.R[mid].key)
high = mid - 1;
else
low = mid + 1;
}
return 0; //查找不到
}


3.分块(索引顺序)查找



三:树表的查找

1.二叉排序树(Binary Sort Tree)
1.1BST的定义
- 若其左子树非空,则左子树上所有结点的值均小于根节点的值
- 若其右子树非空,则右子树上所有结点的值均大于根节点的值
- 其左右子树本身又各是一颗二叉排序树
- 二叉排序树可以是空树
- 中序遍历二叉排列树,得到的数据元素序列是一个递增有序的序列
1.2 BST的存储结构
typedef struct
{
KeyType key; //关键字
InfoType otherinfo; //其他数据域
}ElemType;
typedef struct BSTNode
{
ElemType data; //数据域
struct BSTNode *lchild, *rchild; //左右孩子指针
}BSTNode, *BSTree;
BSTree T; //定义二叉排序树T
1.3 BST的查找

BSTree SearchBST(BSTree T, KeyType key)
{
if((!T) || key == T->data.Tree)
return T;
else if(key < T->data.key)
return SearchBST(T->lchild, key); //在左子树继续查找
else
return SearchBST(T->rchild, key); //在右子树继续查找
}



1.4 BST的插入和生成



1.5 BST的删除
- 被删除的是叶子结点:
直接删去该结点 - 被删除的结点只有左子树或只有右子树:
用其左/右子树替换它 - 被删除的结点既有左子树又有右子树:
用中序前趋值替换它,再删除该前趋结点。前趋是左子树中最大结点
或用后继替换它,再删除该后继结点。后继是右子树中最小结点

2.二叉平衡树(Adelson-Velskii and Landis)
2.1 AVL的定义
平衡二叉树是具有以下性质的二叉排序树
- 左子树与右子树的高度之差的绝对值小于等于一
- 左子树和右子树也是平衡二叉排序树
- 平衡二叉树可以是空树
平衡因子 = 结点左子树高度 - 结点右子树高度
AVL的平衡因子只能是-1,0,1
2.2 失衡二叉排序树的分析与调整

降低高度,保持AVL的性质





四:哈希表的查找
基本思想:记录的存储位置与关键字之间存在对应关系(hash函数)
- 优点:查找效率快O(1)
- 缺点:空间效率低


1.构造散列函数的方法
构造好的散列函数:
- 所选函数尽可能简单,提高转换速度读
- 所选函数对关键码计算出的地址,应在散列地址集中致均匀分布,减少空间浪费
构造散列函数考虑的因素:
- 关键字长度
- 散列表的大小
- 关键字的分布情况
- 查找频率



2.处理冲突的方法
制定一个好的解决冲突的方案:
- 查找时,如果从散列函数中计算出的地址中查不到关键码,应依据解决冲突的规则,有规律地查询其他相关单元
- 开放定址法(开地址法)





- 链地址法(拉链法)
优点:
- 非同义词不会冲突,无“聚集”现象
- 适合表长不确定的情况(链表动态申请)


- 再散列法(双散列函数法)
- 建立一个公共溢出区
3.散列表的查找





结论
- 散列表技术具有很好的平均性能,优于一些传统的技术
- 链地址法优于开地址法
- 除留余数法做散列函数优于其他类型函数
第九章:排序
1.分类
按数据存储介质:
- 内部排序:数据量不大、数据在内存,无需内外存交换数据
- 外部排序:数据量较大、数据在外存(文件排序),外存排序时,要将数据分批调入内存来排序,中间结果还要及时放入外存,比较复杂
按比较器个数:
- 串行排序:单处理机(同一时刻比较一对元素)
- 并行排序:多处理机(同一时刻比较多对元素)
按主要操作:
- 比较排序:插入排序、交换排序、选择排序、归并排序
- 基数排序:不比较元素大小,仅根据元素本身的取值确定其有序位置
按辅助空间:
- 原地排序:辅助空间用量为O(1),与参加排序的数据量大小无关
- 非原地排序:辅助空间用量超过O(1),与参加排序的数据量大小有关
按稳定性:
- 稳定排序:能使任何数值相等的元素,排序后相对次序不变
- 非稳定排序:数值相等的元素,排序后相对次序变化
按自然性:
- 自然排序:输入数据越有序,排序的速度越快
- 非自然排序:输入数据越有序,排序的速度越慢
2.存储结构——记录序列以顺序表存储
#define MAXSIZE 20 //记录最大个数
typedef int KetType; //关键字类型
//定义每个记录(数据元素)的结构
Typedef struct{
KeyType key; //关键字
InfoType otherinfo; //其他数据项
}RedType; //Record Type
//定义顺序表的结构
typedef struct{
RedType r[MAXSIZE + 1]; //存储顺序表的向量,r[0]一般做哨兵或缓冲区
int length; //顺序表长度
}SqList;
3.插入排序
每步将一个待排序的对象,按其关键码大小,插入到前面已经排好序的一组对象的适当位置上,直到对象全部插入为止

3.1 直接插入排序

void InsertSort(SqList &L){
int i, j;
for(i = 2; i < L.length; i++){
if(L.r[i].key < L.[r - 1].key){
L.r[0] = L.r[i]; //复制为哨兵
for(j = i - 1; L.r[0].key; j--){
L.r[j + 1] = L.[j]; //记录后移
}
L.r[j + 1] = L.r[0]; //插入到正确位置
}
}
}



时间复杂度:
- 最好情况:O(n)
- 最坏情况:O(n2)
- 平均情况:O(n2)
空间复杂度:O(1)
是一种稳定的排序方法
3.2 折半插入排序

void BLinsertSort(SqList &L){
int i, j;
int low, high, mid;
for(i = 2; i <= L.length; i++){
L.r[0] = L.r[i]; //插入当前元素到哨兵位置
low = 1;
high = i - 1;
while(low <= high){ //用二分查找法查找插入位置
mid = (low + high) / 2;
if (L.r[0].key < L.r[mid].key)
high = mid - 1;
else
low = mid + 1;
}//循环结束,high+1为插入位置
for(j = i - 1; j >= high + 1; j--)
L.r[j + 1] = L.r[j]; //移动元素
L.r[high + 1] = L.r[0]; //插入正确位置
}
}


3.3 希尔排序
特点:
- 一次移动,移动位置比较大,跳跃式地接近排序后的最终位置
- 最后一次只需少量移动
- 增量必须递减,最后一次必须是1
- 增量序列应该是互质的




void ShellSort(SqList &L, int dlta[], int t){
//按增量序列dlta[0..t-1]对顺序表L作希尔排序
for(int k = 0; k < t; k++)
ShellInsert(L, dlta[k]); //一趟增量为dlta[k]的插入排序
}
void ShellInsert(SqList &L, int dk){
//对顺序表L进行一趟增量为dk的shell排序,dk为增长因子
int i, j;
for(i = dk + 1; i < L.length; i++){
if(L.r[i].key < L.[i - dk].key){
L.r[0] = L.r[i]; //复制为哨兵
for(j = i - dk; j > 0 && (L.r[0].key < L.r[j].key); j -= dk){
L.r[j + dk] = L.[j]; //记录后移
}
L.r[j + dk] = L.r[0]; //插入到正确位置
}
}
}



时间复杂度:
- 最好情况:O(n)
- 最坏情况:O(n2)
- 平均情况:O(n1.3)
空间复杂度:O(1)
是一种不稳定的排序方法
4.交换排序

4.1 冒泡排序

n个记录需要比较n-1次
第m次需要比较n-m次
void BubbleSort(SqList &L){
int i, j, m;
RedType temp; //交换时临时储存
bool flag = 1; //标记是否有交换
for(m = 1; m < L.length - 1 && flag; m++){
flag = 0;
for(j = 1; j <= L.length - m; j++){
if(L.r[j].key > L.[j + 1].key){ //交换
flag = 1;
temp = L.r[j];
L.r[j] = L.r[j + 1];
L.r[j + 1] = temp;
}
}
}
}

时间复杂度:
- 最好情况:O(n)
- 最坏情况:O(n2)
- 平均情况:O(n2)
空间复杂度:O(1)
是一种稳定的排序方法
4.2 快速排序


void QSort(SqList &L, int low, int high){
if(low >= high)
return;
int pivotloc = Partition(L, low, high); //将L一份为二
QSort(L,low,pivotloc - 1); //对低子表递归排序
QSort(L, pivotloc + 1, high); //对高子表递归排序
}
int Partition(SqList &L, int low, int high){
L.r[0] = L.r[low]; //复制到哨兵位置
KeyType pivotkey = L.r[low].key;
while(low < high){
while(low < high && L.r[high].key >= pivotkey)
high--; //从后往前找小的
while(low < high && L.r[high].key <= pivotkey)
low++; //从前往后找大的
}
L.r[low] = L.r[0];
return low;
}


时间复杂度:
- 最好情况:O(nlogn)
- 最坏情况:O(n2)
- 平均情况:O(nlogn)
空间复杂度:O(nlogn)
是一种不稳定的排序方法
快速排序时,越乱越好,基本有序的不适合用快速排序
5.选择排序
5.1 简单选择排序

void SelectSort(SqList &K){
int i, j, k;
ElemType temp;
for(i = 1; i < L.length; i++){
k = i;
for(j = i + 1; j <= L.length; j++)
if(L.r[j].key < L.r[k].key)
k = j;
if(k != i){
temp = L.r[i];
L.r[i] = L.r[k];
L.r[k] = temp;
}
}
}

时间复杂度:
- 最好情况:O(n2)
- 最坏情况:O(n2)
- 平均情况:O(n2)
空间复杂度:O(1)
是一种不稳定的排序方法
5.2 堆排序
- 堆的定义



- 在输出堆顶元素后,调整其余元素为一个新的堆
筛选:
- 输出堆顶后,用堆中最后一个元素代替之
- 将根节点与左、右子树的根节点值比较,并与其中较小/大的交换
- 重复上述操作,直到叶子结点
void HeapAdjust(Elem R[], int s, int m){
//调整R[s]的关键字,使R[s..m]成为一个大根堆
rc = R[s];
for(j = 2 * s; j <= m; j *= 2){
if(j < m && R[j] < R[j + 1])
j++; //j为关键字较大的数据元素下标
if(rc >= R[j])
break;
R[s] = R[j];
s = j; //记录位置
}
R[s] = rc; //插入
}
- 建立一个堆



for(int i = n / 2; i >= 1; i--)
HeadAdjust(R, i, n);
- 实现堆排序
void HeapSort(Elen R[]){
int i;
for(int i = n / 2; i >= 1; i--) //建立初始堆
HeadAdjust(R, i, n);
for(i = n; i > 1; i--){
Swap(R[1], R[i]); //根与最后一个元素交换
HeapAdjust(R, 1, i - 1); //剩下的重新建堆
}
}


时间复杂度:O(nlogn)
空间复杂度:O(n2)
是一种不稳定的排序方法
6.归并排序
将多个有序子序列归并成一个有序序列

需要进行⌈log2n⌉次

时间复杂度:O(nlogn)
空间复杂度:O(n)
是一种稳定的排序方法
7.基数排序(桶/箱排序)
基本思想:分配 + 收集




8.总结
时间复杂度
- O(nlogn):快速排序、堆排序、归并排序
- O(n2):直接插入排序、冒泡排序、简单选择排序
- O(n):基数排序
- 当待排序序列为有序是:用直接插入排序、冒泡排序时间复杂度为O(n),而快速排序退化为O(n2)
- 简单选择排序、堆排序、归并排序的时间性能不随序列分布二改变
空间复杂度:
- O(1):所有简单排序(直接插入排序、冒泡排序、简单选择排序)和堆排序
- O(logn):快速排序
- O(n):归并排序

本文深入探讨了数据结构中的线性表,包括顺序存储和链式存储的实现,以及它们在插入、删除操作中的效率对比。接着介绍了栈和队列的概念,以及它们在实际问题中的应用,如进制转换和括号匹配。此外,详细阐述了树和二叉树的定义、术语、遍历方法以及它们在构建Huffman树和计算路径长度中的作用。最后,概述了图的定义、存储结构和遍历方法,以及它们在最小生成树和最短路径计算中的应用。


















1961

被折叠的 条评论
为什么被折叠?



