数据结构课程学习笔记

整理一下上数据结构课记录的笔记。

第一章 绪论

1.1 数据结构的基本概念

1.2 算法的基本概念

1.2.1 时间复杂度

事前预估算法时间开销T(n)与问题规模n的关系。分析算法操作的执行次数x和问题模型n的关系x=f(n)。

常见数量级关系:(常对幂指阶)
O ( 1 ) < O ( log ⁡ 2 n ) < O ( n ) < O ( n l o g 2 n ) < O ( n 2 ) < O ( n 3 ) < O ( 2 n ) < O ( n ! ) < O ( n n ) O(1)O(1)<O(log2n)<O(n)<O(nlog2n)<O(n2)<O(n3)<O(2n)<O(n!)<O(nn)

1.2.2 空间复杂度

无论问题规模怎么变,算法运行所需的内存空间都是固定的常量。

第二章 线性表

2.1 线性表的基本概念

零个或多个数据元素组成的有序序列。 特性:有序,类型相同,有限。

2.1.1 数学定义

线性表是具有相同类型的n(n≥0)个数据元素的有限序列(a1,a2,a3,…,an-1,an),ai是表项,n是表长度。存在前驱后继。

2.1.2 线性表的操作

  • 创建线性表
  • 销毁线性表
  • 清空线性表
  • 将元素插入线性表
  • 将元素从线性表中删除
  • 获取线性表中某个位置的元素
  • 获取线性表的长度

线性表的抽象数据类型定义:

ADT线性表(List)
    
Data
  线性表的数据对象集合为{a1,a2,a3,...,an-1,an},其中,除第一个元素a1外,每个元素有且只有一个直接前驱元素,直接后继元素同理,数据元素之间的关系是一一对应的。
    
Operation(操作)

//初始化,建立一个空的线性表L
InitList(*L);

//若线性表为空,返回true,否则返回false
ListEmpty(L);

//将线性表清空
ClearList(*L);

//将线性表L中的第i个位置的元素返回给e
GetElem(L,i,*e);

//在线性表L中查找与给定值e相等的元素,如果查找成功,返回该元素在表中序号表示成功,否则返回0表示失败
LocateElem(L,e)

//在线性表L中的第i个位置插入新元素e
ListInsert(*L,i,e);

//删除线性表L中的第i个位置元素,并用e返回其值
ListDelete(*L,i,*e);

//返回线性表L的元素个数
ListLength(L);;

//销毁线性表
DestroyList(*L);

endADT

2.2 线性表的存储方式

如上图,线性表的存储细分以下两种:

1.将所有数据一次存储在连续的整块屋里空间中,叫做顺序存储结构

2.数据分散的存储在物理空间中,通过一根线保存着它们之间的逻辑关系,链式存储结构

2.3 顺序表

顺序表不仅存在“一对一”的数据关系,对数据的物理存储结构也有要求。顺序表存储结构时,会提前申请一整块足够大小的屋里空间,然后将数据依次存储起来,存储时做到数据元素之间不留一丝细缝

2.3.1 顺序表的初始化

顺序表需要实时记录一下两项数据:

1.顺序表申请的存储容量;

2.顺序表的长度,也就是表中存储数据元素的个数;

提示:正常状态下,顺序表申请的存储容量要大于顺序表的长度

因此,需要自定义顺序表

typedef struct Table{
    int *head;//声明了一个名为head的长度不确定的数组
    int length;//记录当前顺序表的长度
    int size;//记录顺序表分配的存储容量
}table;

注意:head是未初始化的动态数组。

初步建立一个顺序表,需要如下工作:

  • 给head动态数据申请足够大小的物理空间
  • 给size和length赋初始值
#define Size 5;//对Size进行宏定义,表示顺序表申请空间的大小
table InitTable(){
    table t;
    t.head = (int*)malloc(Size *sizeof(int));//构造空的顺序表,动态申请存储空间
    if(!t.head){//申请失败,作为提示直接退出程序
        printf("初始化失败");
        exit(0);
    }
    t.length = 0;//空表的长度初始化为0
    t.size = Size;//空表的初始化存储空间为Size
    return t;
}

2.3.2 顺序表的插入元素

  • 插入位置不合理,抛出异常
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
#include 
using namespace std;

typedef int Status;

// 初始条件:顺序线性表L已存在,1<=i<=ListLength(L).
// 操作结果:在L中第i个位置之前插入新的数据元素e,L长度+1
Status ListInsert(List *L, int i, ElemType e)
{
    int k;

    if (L->length == MAXSIZE) //顺序线性表已经满了
    {
        return ERROR;
    }

    if (i < 1 || i > L->length + 1) //当i不在范围内时
    {
        return ERROR
    }

    if (i <= L->length) //若插入数据位置不在表尾,i=length+1就是在表尾,直接添加就可以
    {
        // 将要插入位置后数据元素向后移动一位
        for (k = L->length - 1; k >= i - 1; k--)
        {
            L->data[k + 1] = L->data[k];
        }
    }

    L->data[i - 1] = e; //将新元素插入
    L->length++;

    return OK;
}

2.3.3 顺序表的删除元素

  • 删除位置不合理,抛出异常
  • 取出删除元素
  • 从删除元素位置开始遍历到最后一个元素位置,分别将他们向前移动一个位置
  • 表长-1
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
#include 
using namespace std;

typedef int Status;

Status ListDelete(List *L, int i, ElemType *e)
{
    int k;

    if (L->length == 0)
    {
        return ERROR;
    }

    if (i < 1 || i > L->length)
    {
        return ERROR;
    }

    *e = L->data[i - 1];

    if (i < L->length)
    {
        for (k = i; k < L->length; k++)
        {
            L->data[k - 1] = L->data[k];
        }
    }
    L->length--;
    return OK;
}

2.3.4 顺序表的查找元素

#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
#include 
using namespace std;

typedef int Status;

// Status是函数的类型,其值是函数结果状态代码,如OK等。
// 初始条件:顺序线性表L已存在, 1 <= i <= ListLength(L)
// 操作结果:用e返回L中第i个数据元素的值

Status GetElem(List L, int i, ElemType *e)
{
    if (L->length == 0 || i < 1 || i > L->length)
    {
        return ERROR;
    }

    *e = L->data[i - 1];
    return OK;
}

2.3.5 顺序表的更改元素

2.4 单链表

2.4.1 获取链表第i个数据

  • 声明一个节点p指向链表第一个节点,初始化j从1开始
  • 当j
  • 若到链表末尾p为空,则说明第i个元素不存在
  • 否则查找成功,返回节点p的数据

2.5 双向链表

第三章 栈和队列

3.1 什么是栈

栈就是一种只能从表的一段存储数据且遵循“先进后出”原则的线性存储结构。压栈(push),弹栈(pop)。

栈遵循先进后出,栈存储结构的实现有以下两种方式:

  • 顺序栈
  • 链栈

3.2 顺序栈及基本操作

3.2.1 顺序栈的定义

typedef struct{   
    int data[maxsize];    //定义一个数组大小为maxsize的数组,用来存放栈中数据元素    
    int top;              //栈顶指针                 
}SqStack;				  //顺序栈定义

3.2.2 顺序栈的操作

可以参考顺序栈的操作C语言版

对于顺序栈,一共有4个要素,包括两个特殊状态和两个操作

特殊状态:

1)栈空状态:st.top == -1,也有的用st.top = 0表示,这时候栈顶位置为0;

2)栈满状态:st.top == maxsize - 1表示栈满,maxsize是栈中最大元素个数,maxsize-1为栈满时栈顶元素在数组中的位置,因为数组位置是从0开始的。

操作:

进栈和出栈

以下是数组实现:

/*源代码*/
#include 
#include 

#define MAXSIZE 100
typedef int DataType;

typedef struct
{
    DataType stack[MAXSIZE];
    int top;  
}SeqStack;

/*初始化栈,把栈初始化为空,把栈顶指针置为-1*/
void InitStack(SeqStack * s)
{
    s->top = -1;
}

/*判空操作,当栈顶指针为top 为-1,栈为空*/
int StackEmpty(SeqStack s)
{
    if (s.top == -1)
    {
        return 1;
    }
    return 0;
}

/*入栈操作,栈顶指针top++,然后将data值压入栈中*/
int StackPush(SeqStack* s, DataType data)
{
    if (s->top == MAXSIZE)
    {
        printf("栈已满,不能入栈!\n");
        return 0;
    }
    else
    {
        s->top++;
        s->stack[s->top] = data;
        return 1;
    }
}

/*取栈顶元素,将栈顶元素输出*/
int StackGetTop(SeqStack s)
{
    if (s.top==-1)
    {
        printf("栈为空!\n");
        return 0;
    }
    else
    {
        printf("%d\n", s.stack[s.top]);
        s.top--;
    }
}
/*输入要入栈的元素以 -1 作为结束标志*/

void StackInput(SeqStack* s)
{
    int data;
    scanf_s("%d", &data);
    while (data != -1)
    {
        StackPush(s, data);
        scanf_s("%d", &data);
    }
}

/*打印栈中元素*/
void Display(SeqStack s)
{
    int i;

    for (i = s.top; i >= 0; i--)
    {
        printf("%-3d", s.stack[i]);
    }
    printf("\n");
}
/*出栈操作,将栈顶指针top-- */
void StackPop(SeqStack* s)
{
    if (s->top==-1)
    {
        printf("栈为空!\n");
    }
    else
    {
        s->top--;
    }
}

/*返回栈的长度,栈的长度就是栈中元素的个数*/
int StackLength(SeqStack s)
{
     s.top + 1;
     printf("%d\n", s.top + 1);
     return 0;
}

/*清空栈,清空栈与初始化栈的操作一样,只需将栈顶指针置-1即可*/
void StackClear(SeqStack* s)
{
    s->top = -1;
}
int main()
{
    SeqStack s;
    InitStack(&s);

    printf("输入入栈元素:\n");
    StackInput(&s);
    printf("栈中元素为:\n");
    Display(s);
    printf("栈顶元素为:\n");
    StackGetTop(s);
    printf("栈中元素个数为:\n");
    StackLength(s);
    printf("退一次栈!\n");
    StackPop(&s);
    printf("栈中元素为:\n");
    Display(s);
    StackClear(&s);

    system("pause");
    return 0;
}

以下是定义结构体实现:

#include
#include
#include
#include
#include
using namespace std;
#define Status int
#define SElemType int
#define MaxSize 100
//栈数据结构
typedef struct Stack
{
    SElemType *base;//栈底指针 不变
    SElemType *top;//栈顶指针 一直在栈顶元素上一个位置
    int stacksize;//栈可用的最大容量
}SqStack;
//**************************************基本操作函数************************************//
//初始化函数
Status InitStack(SqStack &s)
{
    s.base=new SElemType[MaxSize];//动态分配最大容量
    if(!s.base)
    {
        printf("分配失败\n");
        return 0;
    }
    s.top=s.base;//栈顶指针与栈底相同 王道上top起初在base下面,感觉很别扭,top应该高于或等于base
    s.stacksize=MaxSize;
    return 1;
}
//入栈
Status Push(SqStack &s,SElemType e)
{
    if(s.top-s.base==s.stacksize) return 0;//栈满
    *(s.top++)=e;//先入栈,栈顶指针再上移 注意与王道上的不同,具体问题具体分析
    return 1;	
}
//出栈 用e返回值
Status Pop(SqStack &s,SElemType &e)
{
    if(s.top==s.base) return 0;//栈空
    e=*--s.top;//先减减 指向栈顶元素,再给e
    return 1;	
}
//得到栈顶元素,不修改指针
bool GetTop(SqStack s,SElemType &e) //严蔚敏版59页有问题,应该用e去获得,函数返回bool类型去判断
{
    if(s.top==s.base) return false;//栈空			
    else e=*--s.top;
    return true;

}
//********************************功能实现函数**************************************//
//菜单
void menu()
{
    printf("********1.入栈      2.出栈*********\n");
    printf("********3.取栈顶    4.退出*********\n");
}
//入栈功能函数 调用Push函数
void PushToStack(SqStack &s)
{
    int n;SElemType e;int flag;
    printf("请输入入栈元素个数(>=1):\n");
    scanf("%d",&n);
    for(int i=0;i=1):\n");
    scanf("%d",&n);
    for(int i=0;i

3.3 链栈及基本操作

把栈顶放在单链表的表头,用链表来存储栈的数据结构称为链栈。

链栈节点定义如下:

typedef struct StackNode{
    ElemType data;
    struct StackNode *next;
}StackNode,*LinkStack;

3.3.1 链栈的操作

链栈也有四个元素,包括两个状态和两个操作

状态:

1)栈空:StakNode->next == NULL,即栈没有后继节点时,栈为空

2)栈满:如果存储空间无限大,没有这种情况。

操作:

进栈是头插法建立链表的插入方法,出栈就是单链表的删除操作

以上是链栈的插入操作

以上是链栈的删除操作

  • 链栈初始化
Status InitStack(LinkStack &S){
    //构造一个空栈S,栈顶指针置空
    S=NULL;
    return OK;
}
  • 进栈
Status Push(LinkStack &S,SElemType e){
    //栈顶插入元素e
    p=new StackNode;    //创建新节点
    p->data=e;          //将新节点数据域置为e
    p->next=S;          //将新结点与头结点建立逻辑关系
    S=p;                //更新头结点的指向
    return OK;
}
  • 出栈
Status Pop(LinkStack &S,SElemType &e){
    //删除S的栈顶元素,用e返回值
    if(S==NULL) 
        return ERROR;     //栈空
    e=S->data;            //将栈顶元素赋给e
    p=S;                  //用p临时保存栈顶元素空间,以备释放
    S=S->next;            //修改栈顶指针
    delete p;             //释放原栈顶元素的空间
    return OK;
}
  • 取栈顶元素
SElemType GetTop(LinkStack S){
    if(S!=NULL)
        return S->data;
}
#include
#include

typedef struct LinkedStackNode{
    int data;
    LinkedStackNode *next;
}LinkedStackNode,*LinkedStack;

//初始化栈
int InitLinkedStack(LinkedStack &L){
    L=(LinkedStackNode*)malloc(sizeof(LinkedStackNode));
    L->next=NULL;
    return 1;
}

//销毁栈
int DestroyList(LinkedStack &L){
    LinkedStack temp;
    while(L){
        temp=L->next;
        free(temp);
        L=temp;
    }
    return 1;
}

//判断栈空
int LinkedStackEmpty(LinkedStack L){
    if(!L->next){
        printf("LinkedStack is Empty\n");
        return 1;
    }
    else{
        printf("LinkedStack isn't Empty\n");
        return 0;
    }
}

//入栈
int Push(LinkedStack &L,int e){
    LinkedStack temp;
    temp=(LinkedStackNode*)malloc(sizeof(LinkedStackNode));//创建临时结点
    temp->data=e;//将e赋值给temp的数据域
    temp->next=L->next;//temp结点指向下一结点
    L->next=temp;//更改L结点的指向temp结点
    return 1;
}

//出栈
int Pop(LinkedStack &L,int &e){
    LinkedStack temp;
    temp=L->next;
    e=temp->data;
    L->next=temp->next;
    free(temp);
    return 1;
}

//获取栈顶元素
int GetElem(LinkedStack L,int &e){
    return e=L->next->data;
}

int main() {              //记几个测试代码,可随意修改
    int n,i,e;
    LinkedStackNode *top;
    InitLinkedStack(top);
    LinkedStackEmpty(top);
    scanf("%d",&n);
    for(i=1;i<=n;i++){
        Push(top, i);
    }
    Pop(top,e);
    printf("%d\n",e);
    GetElem(top, e);
    printf("%d\n",e);
    DestroyList(top);
    return 0;
}

3.4 什么是队列

与栈不同的是,队列的两端都“开口”,要求数据只能从一端进,从另一端出。

1234

通常,称进数据的一端叫做“队尾”,出数据的一端叫做“队头”,添加数据叫做入队,出队列叫做出队

队列遵循先进先出的原则,队列存储结构的实现有以下两种方式:

  • 顺序队列:在顺序表的基础上实现队列的结构
  • 链队列:在链表的基础上实现队列结构

3.4.1 队列的顺序存储

为了满足顺序队列中数据从队尾进,队头出且先进先出的要求,我们还需要定义两个指针(top 和 rear)分别用于指向顺序队列中的队头元素和队尾元素。

由于顺序队列初始状态没有存储任何元素,因此 top 指针和 rear 指针重合,且由于顺序队列底层实现靠的是数组,因此 toprear 实际上是两个变量,它的值分别是队头元素和队尾元素所在数组位置的下标。

用顺序表来存储队列元素的数据结构称为队列的顺序存储结构,定义如下:

typedef struct{
    int data[maxsize];	//定义数组
    int front;			//队首指针
    int rear;			//队尾指针
}SqQueue;				//顺序队列定义

3.4.2 队列的链式存储结构

只需创建两个指针(命名为 top 和 rear)分别指向链表中队列的队头元素和队尾元素。

如图所示为链式队列的初始状态,此时队列中没有存储任何数据元素,因此 top 和 rear 指针都同时指向头节点。

  • 链式队列的结构定义
//节点结构
typedef struct QNode{
    int data;				//数据域
    struct QNode *next;		//指针域
}QNode;
//队列的链表结构
typedef struct{
    QNode *front;		//队头指针
    QNode *rear;		//队尾指针
}LiQueue;
  • 链队的初始化
void initQueue(LiQueue *&lqu){
    lqu = (LiQueue*)malloc(sizeof(LiQueue));
    lqu->front = lqu->real = NULL;
}
  • 链队的入队操作:如下图,表示链队列依次入队{1,2,3}三个元素

如上图所示,当有新的数据入队时,要以下3步操作

  1. 将该数据元素用节点包裹,例如新节点名称为 elem;
  2. 与 rear 指针指向的节点建立逻辑关系,即执行 rear->next=elem;
  3. 最后移动 rear 指针指向该新节点,即 rear=elem;
  • 链队的出队操作:当有元素出队时,按照“先进先出”的原则,只需要将存储该数据的结点以及它之前入队的元素结点按照顺序依次出队。出队的过程就是从链表头一次删除首节点的过程,现在我们需要将上图{1,2}两个元素出队,如下图

所以我们知道,需要以下3步操作

  1. 通过 top 指针直接找到队头节点,创建一个新指针 p 指向此即将出队的节点;
  2. 将 p 节点(即要出队的队头节点)从链表中摘除;
  3. 释放节点 p,回收其所占的内存空间;

第四章 串、数组和广义表

4.4 数组

4.4.1 数组的类型定义

数组是由类型相同的数据元素构成的有序集合,一维数组可以看成是一个线性表,二维数组可以看成数据元素是线性表的线性表。

4.4.2 数组的顺序存储

在Basic、Pascal、Java和C语言中,用的是以行序为主序的存储结构,而在FORTRAN语言中,用的是以列序为主序的存储结构。

假设每个数据元素占L个存储单元,则二维数组A[0…m-1,0…n-1](即下标从0开始,共有m行n列)中任一元素aij的存储位置可由下式确定

L O C ( i , j ) = L O C ( 0 , 0 ) + ( n × i + j ) L LOC(i,j)=LOC(0,0)+(n{\times}i+j)L LOC(i,j)=LOC(0,0)+(n×i+j)L

式中,LOC(i,j)是aij的存储位置;LOC(0,0)是a00的存储位置,即二维数组A的起始存储位置也称为基地址或基址。

得到一般推广情况,可以得到n维数组A[0…b1,0…b2,…,0…bn-1]的数据元素存储位置的计算公式:
L O C ( j 1 , j 2 , . . . , j n ) = L O C ( 0 , 0 , . . . , 0 ) + ( b 2 × . . . b n × j 1 + b 3 × . . . × b n × j 2 + . . . + b n × j n − 1 + j n ) L = L O C ( 0 , 0 , . . . , 0 ) + ( ∑ i = 1 n − 1 j i ∏ k = i + 1 n b k + j n ) L \begin{aligned}LOC(j_1,j_2,...,j_n)&=LOC(0,0,...,0)+(b_2{\times}...b_n{\times}j_1+b_3{\times}...{\times}b_n{\times}j_2+...+b_n{\times}j_{n-1}+j_n)L\\&=LOC(0,0,...,0)+\left( \sum_{i=1}^{n-1}{j_i}\prod_{k=i+1}^{n}{b_k}+j_n \right)L \end{aligned} LOC(j1,j2,...,jn)=LOC(0,0,...,0)+(b2×...bn×j1+b3×...×bn×j2+...+bn×jn1+jn)L=LOC(0,0,...,0)+(i=1n1jik=i+1nbk+jn)L
可缩写成
L O C ( j 1 , j 2 , . . . , j n ) = L O C ( 0 , 0 , . . . , 0 ) + ∑ i = 1 n c i j i LOC(j_1,j_2,...,j_n)=LOC(0,0,...,0)+\sum_{i=1}^{n}c_ij_i LOC(j1,j2,...,jn)=LOC(0,0,...,0)+i=1nciji
其中,cn=L,ci-1=bi*ci,1

4.4.3 特殊矩阵的压缩存储

1 对称矩阵

[特点]  在n*n的矩阵a中,满足以下性质:
a i j = a j i ( a ≤ i , j ≤ n ) a_{ij}=a_{ji}(a{\le}i,j{\le}n) aij=aji(ai,jn)
[存储方法]  只存储下(或者上)三角(包括主对角线)的数据元素。共占用n(n+1)/2个元素空间。

image-20210408172627868
k = { i ( i − 1 ) 2 + j − 1 ,   i ≥ j j ( j − 1 ) 2 + i − 1 ,   j < i k=\begin{cases} \frac{i(i-1)}{2}+j-1, i{\ge}j\\ \frac{j(j-1)}{2}+i-1, j{\lt}i \end{cases} k={2i(i1)+j1, ij2j(j1)+i1, j<i
2 三角矩阵

[特点]  对角线以下(或者以上)的数据元素(不包括对角线)全部为常数c。

image-20210408174126047

[存储方法]  重复元素c共享一个元素存储空间,共占用n(n+1)/2+1个元素空间:sa[k]和矩阵元aij之间的关系:

(1)上三角矩阵:
k = { ( i − 1 ) × ( 2 n − i + 2 ) 2 + j − i         i ≤ j n ( n + 1 ) 2                         i > j k=\begin{cases} \frac{(i-1)\times(2n-i+2)}{2}+j-i    i{\le}j\\ \frac{n(n+1)}{2}            i{\gt}j \end{cases} k={2(i1)×(2ni+2)+ji    ij2n(n+1)            i>j

(2)下三角矩阵:
k = { i ( i − 1 ) 2 + j − 1         i ≥ j n ( n + 1 ) 2                 i < j k=\begin{cases} \frac{i(i-1)}{2}+j-1    i{\ge}j\\ \frac{n(n+1)}{2}        i{\lt}j \end{cases} k={2i(i1)+j1    ij2n(n+1)        i<j
3 对角矩阵

[特点]  在n*n的方阵中,非零元素集中在主对角线及其两侧共L(奇数)条对角线的带状区域内 — L对角矩阵。

[存储方法]

4 稀疏矩阵

[特点]  大多数元素为0

[常用存储方法]  只记录每一非零元素(i,j,aij)

第五章 树和二叉树

5.1 树和二叉树的定义

5.1.1 树的定义

树是n(n>=0)个结点的有限集,它或为空树(n=0);或为非空树,对于非空树T:

(1)有且仅有一个称之为根的结点;

(2)除根节点意外的其余结点可分为m(m>0)个互不相交的有限集T1,T2,…,Tm,其中每一个集合本身又是一棵树,并且称为根的子树。

5.1.2 树的基本术语

如图5.1(b):

(1)结点:数中的一个独立单元。包含一个数据元素及若干指向其子树的分支,如图的A、B、C、D等。

(2)结点的度:结点拥有的子树数。如,A的度是3,C的度是1,F的度是0。

(3)树的度:树内各结点度的最大值

(4)叶子:度为0的结点称为叶子或终端结点。比如,K、L、F、G、M、I、J都是树的叶子。

(5)非终端结点:度不为0的结点称为非终端结点或分支节点。除根结点之外,也叫内部节点。

(6)双亲和孩子:结点的子树的根称为该结点的孩子,相应地,该结点称为孩子的双亲。如,B的双亲为A,B的孩子有E和F。

(7)层次:结点的层次从根开始定义,根为第一层,根的孩子为第二层等等。

(8)树的深度:树中结点的最大层次称为树的深度或高度,上图树的深度为4。

5.1.3 二叉树的定义

二叉树(Binary Tree)是n(n>=0)个结点所构成的集合,它或为空树(n=0);或为非空树,对于非空树T:

(1)有且仅有一个称之为根的结点;

(2)除根结点之外的其余结点分为两个互不相交的子集T1和T2,分别称为T的左子树和右子树,且T1和T2本身又都是二叉树。

二叉树的基本特点

  • 结点的度都<=2
  • 有序树(子树有序,不能颠倒)

5.2 二叉树的性质和存储结构

5.2.1 二叉树的性质

性质1 在二叉树的第i层上至多有2i-1个节点(i>=1)。

性质2 深度为k的二叉树至多有2k-1个节点(k>=1)。

性质3 对任何一个二叉树T,如果其终端节点数为n0,度为2的节点数为n2,则n0=n2+1。

特殊的二叉树

(1)满二叉树:一个深度为k且有2k-1个结点的二叉树。

(2)完全二叉树:深度为k的,有n个节点的二叉树,当且仅当其没一个节点斗鱼深度为k的满二叉树中编号从1至n的结点一一对应(且最后一层叶子不满,全部集中在左边)

性质4 具有n个结点的完全二叉树的深度为:
⌊ log ⁡ 2 n ⌋ + 1 \left \lfloor \log_2n\right \rfloor+1 log2n+1

⌊ x ⌋ 取 不 大 于 x 的 最 大 整 数 , ⌈ x ⌉ 取 不 小 于 x 的 最 小 整 数 。 \left \lfloor x\right \rfloor取不大于x的最大整数,\left \lceil x \right \rceil取不小于x的最小整数。 xxxx

性质5 如果对一棵有n个结点的完全二叉树的结点按层序编号(从第1层到第[log2n]+1层,每层从左到右),则对任一结点i(1<=i<=n),有:

(1)如果i=1,则结点i无双亲,是二叉树的根;如果i>1,则其双亲是结点[i/2]。

(2)如果2i>n,则结点i无左孩子(结点i为叶子结点);若2i==n,其左孩子是结点2i(i为最后一个非叶子结点)。

(3)如果2i+1>n,则结点i无右孩子;若2i+1==n ,其右孩子是结点2i+1(i为最后一个非叶子结点)。

5.2.2 二叉树的存储结构

1 顺序存储结构

2 链式存储结构

image-20210407170302561

如图 1 所示,此为一棵普通的二叉树,若将其采用链式存储,则只需从树的根节点开始,将各个节点及其左右孩子使用链表存储即可。因此,图 1 对应的链式存储结构如图 2 所示:

image-20210407170347593

由图 2 可知,采用链式存储二叉树时,其节点结构由 3 部分构成(如图 3 所示):

  • 指向左孩子节点的指针(Lchild);
  • 节点存储的数据(data);
  • 指向右孩子节点的指针(Rchild);

image-20210407170412791

表示该节点结构的 C 语言代码为:

typedef struct BiTNode{  
    TElemType data;//数据域   
    struct BiTNode *lchild,*rchild;//左右孩子指针    
    struct BiTNode *parent;//可以有可以没有
}BiTNode,*BiTree;

5.3 遍历二叉树和线索二叉树

5.3.1 遍历二叉树及其应用

二叉树的遍历

二叉树由3个基本单元组成:根结点、左子树、右子树。因此可以分为三个遍历部分。可以参考具体链接。

  • DLR——先序遍历,先根再左再右

  • LDR——中序遍历,先左再根再右

  • LRD——后序遍历,先左再右再根

image-20210407174035666

先序遍历

先序遍历:访问根节点,访问当前节点的左子树;若当前节点无左子树,则访问当前节点的右子树。

image-20210422090614930

Status PreOrder Traverse(BiTree T){
    if(T==NULL)
        return OK;
    else{
        cout<data;
        PreOrderTraverse(T->lchild);
        PreOrderTraverse(T->rchild);
    }
}

中序遍历

中序遍历:访问当前节点的左子树;访问根节点;访问当前节点的右子树。

image-20210422090651292

中序遍历的递归算法

void InOrderTraverse(BiTree T){
    if(T){
        InOrderTraverse(T->lchild);
        cout<data;
        InorderTraverse(T->rchild);
    }
}

中序遍历的非递归算法

①初始化一个空栈S,指针p指向根结点

②申请一个结点空间q,用来存放栈顶弹出的元素

③当p非空或者栈S非空时,循环执行以下操作:

  • 如果p非空,则将p进栈,p指向该结点的左孩子;
  • 如果p为空,则弹出栈顶元素并访问,将p指向该结点的右孩子。
void InOrderTraverse(BiTree T){
    InitStack(S);
    p=T;
    q=new BiTNode;
    while(p||!StackEmpty(S)){
        if(p){//p非空
            Push(S,p);//根指针进栈
            p=p->lchild;//根指针进栈,遍历左子树 
        }else{//p空
            Pop(S,q);//退栈
            cout<data;//访问根结点
            p=q->rchild;//遍历右子树
        }
    }
}

后序遍历

后序遍历:从根节点出发,依次遍历各节点的左右子树,直到当前节点左右子树遍历完成后,才访问该节点元素。

image-20210422090949365

层序遍历

层次遍历:从上往下一层一层遍历。

image-20210422091009192

void LevelTreaverse(BiTree T){
    queue <BiNode *> que;
    if(!T)
        return;
    que.push(T);
    while(!que.empty()){
        BiNode *p = que.front();
        que.pop();
        cout<<p->data;
        if(p->lchild!=NULL)
            que.push(p->lchild);
        if(p->rchild!=NULL)
			que.push(p->rchild);v
    }
}
二叉树的遍历应用
  • 先序遍历的顺序建立二叉链表

1.扫描序列,读入字符ch。

2.如果ch是一个“#”字符,则表明该二叉树为空树,即T为NULL;否则执行以下操作:

①申请一个节点空间T;

②将ch赋给T->data;

③递归创建T的左子树;

④递归创建T的右子树;

void CreatBiTree(BiTree &T){
    //按先序次序输入二叉树中的结点的值,创建二叉链表表示二叉树T
    cin>>ch;
    if(ch=='#')
        T=NULL; //递归结束,建立空树
    else{//递归创建二叉树
        T=new BiTNode;//生成根节点
        T->data=ch;//根结点数据域为ch
        CreateBiTree(T->lchild);//递归创建左子树
        CreateBiTree(T->rchild);//递归创建右子树
        
    }
}
  • 复制二叉树

①若二叉树不空,则首先复制根结点(相当于先序遍历算法中访问根节点的语句)

②然后分别复制二叉树根结点的左子树和右子树(相当于先序遍历中递归遍历左子树和右子树的语句)

void Copy(BiTree T, BiTree &NewT){
    if(T==NULL){
        NewT=NULL;
        return;
    }
    else{
        NewT=new BiTNode;
        NewT->data=T->data;
        Copy(T->lchild,NewT->lchild);
        Copy(T->rchild,NewT->rchild);
    }
}
  • 计算二叉树深度

如果是空树,递归结束,深度为0,否则

  • 递归计算左子树的深度为m;
  • 递归计算右子树的深度为n;
  • 如果m>n,二叉树的深度为m+1,否则n+1。
int Depth(BiTree T)
{//计算二叉树T的深度
    if(T==NULL)
        return 0;//如果是空树,深度为0, 递归结束
    else
    {
        m=Depth(T->lchild);//递归计算左子树的深度记为m
        n=Depth(T->rchild);//递归计算右子树的深度记为n
        if(m>n) return(m+l);//二叉树的深度为m与n的较大者加1
        else return(n+l);
    }
}                                             
  • 统计二叉树中结点的个数

如果是空树,则结点个数为0,;苟泽,结点个数为左子树的结点个数加上右子树的结点个数再加上 1

int NodeCount(BiTree T){
    //统计二叉树T中结点的个数
    if (T==NULL) return O; //如果是空树,则结点个数为0, 递归结束
    else return NodeCount (T->lchild) +Node Count (T->rchild) + 1;//否则结点个数为左子树的结点个数+右子树的结点个数+l
}
  • 计算二叉树叶子结点的数
int LeadCount(BiTree T){
    if(T==NULL) 	//如果是空树返回0
        return 0;
    if (T->lchild == NULL && T->rchild == NULL)
        return 1; //如果是叶子结点返回1
    else return LeafCount(T->lchild) + LeafCount(T->rchild);
}

在n个结点的二叉链表中,有n+1个空指针域

5.3.2 线索二叉树

遍历二叉树是以一定规则将二叉树中的结点排列成一个线性序列,得到二叉树中结点的先序序列、中序序列或后序序列。普通二叉树只能找到结点的左右孩子信息,而该结点的直接前驱和直接后继只能在遍历过程中获得。若将某种遍历序列某个结点的前驱和后继预存起来,则从第一个结点开始就能很快“顺藤摸瓜”而遍历整个树。

如何保存这类信息?
两 种 解 决 方 法 = { 增 加 两 个 域 : f w d 和 b w d ;       使 得 结 构 的 存 储 密 度 大 大 降 低 利 用 空 链 域 ( n + 1 个 空 链 域 )                     两种解决方法=\begin{cases} 增加两个域:fwd和bwd;   使得结构的存储密度大大降低\\ \\ 利用空链域(n+1个空链域)           \end{cases} =fwdbwd;   使n+1          
所以基于某种遍历规则:
1)若结点有左子树,则lchild指向其左孩子;
否则, lchild指向其直接前驱(即线索);

2)若结点有右子树,则rchild指向其右孩子;
否则, rchild指向其直接后继(即线索) 。

为了避免混淆,增加两个标志域

image-20210414173918248

其中:
L T a g = { 0   l c h i l d 域 指 示 结 点 的 左 孩 子   1   l c h i l d 域 指 示 结 点 的 前 驱                     LTag=\begin{cases} 0 lchild域指示结点的左孩子 \\ 1 lchild域指示结点的前驱           \end{cases} LTag={0 lchild 1 lchild          

R T a g = { 0   l c h i l d 域 指 示 结 点 的 左 孩 子   1   l c h i l d 域 指 示 结 点 的 后 继                     RTag=\begin{cases} 0 lchild域指示结点的左孩子 \\ 1 lchild域指示结点的后继           \end{cases} RTag={0 lchild 1 lchild          

二叉树二叉线索类型定义如下:

typedef struct BiThrNode{
    TElemType data;
    struct BiThrNode *lchild,*rchild;
    int LTag,RTag;
}BiThrNode,*BiThrTree;

以结点p为根的子树中序线索化

[算法步骤]

①如果p非空,左子树递归线索化

②如果p的左孩子为空,则给p加上左线索,将其LTag置为1,让p的左孩子指针指向pre(前驱);否则将p的LTag置为0

③如果pre的右孩子为空,则给pre加上右线索,将其RTag置为1,让pre的右孩子指针指向p(后继);否则将pre的RTag置为0

④将pre指向刚访问过的结点p,即pre=p

⑤右子树递归线索化

[算法描述]

void InThreading(BiThrTree p){
    //pre是全局变量,初始化时右孩子指针为空,便于在树的最左点开始建线索
    if(p){
        InThreading(p->lchild);//左子树递归线索化
        if(!p->lchild)//p的左孩子为空
        {
            p->LTag=1;//给p加上左线索
            p->lchild=pre;//p的左孩子指针指向pre(前驱)
        }
        else
            p->LTag=0;
        if(!pre->rchild){//pre的右孩子为空
            pre->RTag=1;//给pre加上右线索
            pre00>rchild=p;//pre的右孩子指针指向p(后继)
        }
        else 
            pre->RTag=0;
        pre=p;//保持pre指向p的前驱
        InThreading(p->rchild);//右子树递归线索化
    }
}

5.4 树与森林

5.4.1 树的存储结构

1.双亲表示法:以一组连续空间存储树的结点,同时在结点中附设一个指针,存放双亲结点在链表中的位置。

双亲实现树定义:

#define MAX_TREE_SIZE 100   //最大结点个数
typedef struct PTNode{          //树结点定义
    TElemType     data;                 
    int      parent;                        
} PTNode;

typedef struct PTNode{          //树定义
    PTNode   nodes[MAX_TREE_SIZE];                 
    int     r,   n;        //根的位置和结点数                       
} PTree;

2.孩子表示法

(1)多重链表法(两种)

(2)孩子链表法:将每个结点的孩子结点排列起来,看成一个线性表,且以单链表作存储结构,n个结点有n个孩子链表,n个头指针组成线性表。

3.孩子兄弟表示法:每个节点除值域外,还包括两个指针,分别指向该节点的第一个孩子和下一个兄弟。

结点结构:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gXdghJ3E-1649591834336)(https://cdn.jsdelivr.net/gh/qnjy/images/data/1618446095-image-20210415082134980.png)]

结构定义:

typedef char ElemType;

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

5.4.2 森林和二叉树的转换

1.树—>二叉树

方法(树的孩子兄弟表示法):

①加线:树中所有相邻兄弟间加一连线;
②抹线:对树中的每个结点,只保留其与第一个孩子间的连线,删去它与其他孩子间连线;
③旋转:以树根为轴心,将整棵树顺时针旋转45度,使之结构层次分明。

任何一棵树和树对应的二叉树,其根结点的右子树必空。

示例:

image-20210422093615176

特点:

  • 左分支-----父子关系 右分支—兄弟关系

  • 根没有兄弟,所以一棵树转换后的二叉树一定只有左子树

2.森林—>二叉树

方法:

①将森林中每棵树转换成相应的二叉树;
②第一棵二叉树不动,从第二棵二叉树开始,依次把后一棵二叉树的根结点作为前一棵二叉树根结点的右孩子,当所有二叉树连起来后,此时,所得二叉树即是森林转换得到的。

示例:

3.二叉树—>树

①若某结点的左孩子结点存在,将左孩子结点的右孩子结点、右孩子结点的右孩子结点……都作为该结点的孩子结点,将该结点与这些右孩子结点用线连接起来;

②删除原二叉树中所有结点与其右孩子结点的连线;

③整理(1)和(2)两步得到的树,使之结构层次分明。

4.二叉树—>森林

①先把每个结点与右孩子结点的连线删除,得到分离的二叉树;

②把分离后的每棵二叉树转换为树;

③整理第(2)步得到的树,使之规范,这样得到森林。

特点:根没有右孩子,则转换成的是树,否则转换成的是森林。

5.4.3 森林和树的遍历

1.树的遍历

  • 先根遍历:若树不空,则先访问根结点,然后依次从左到右先根遍历根的各棵子树。

  • 后根遍历:若树不空,则先依次从左到右后根遍历根的
    各棵子树,然后访问根结点;

2.森林的遍历

  • 先序遍历森林:

若森林非空,则可按下述规则遍历:

①访问森林中第一棵树的根结点;

②先序遍历第一棵树的根结点的子树森林;

③先序遍历除去第一棵树之后剩余的树构成的森林。

  • 中序遍历森林

若森林非空,则可按下述规则遍历:

①中序遍历森林中第一棵树的根结点的子树森林;

②访问第一棵树的根结点;

③中序遍历除去第一棵树之后剩余的树构成的森林。

5.5 哈夫曼树及其应用

5.5.1 哈夫曼树的基本概念

哈夫曼树又称最优树,是一类带权路径长度最短的树。

(1)路径:从树中一个结点到另一个结点之间的分支构成这两个结点之间的路径。

(2)路径长度:路径上的分支数目称作路径长度。

(3)树的路径长度:从树根到每一结点的路径长度之和。

(4):赋予某个实体的一个量,是对实体的某个或某些属性的数值化描述。在数据结构中,实体有结点(元素)和边(关系)两大类, 所以对应有结点权和边权。结点权或边权具体代表什么意义,由具体情况决定。如果在一棵树中的结点上带有权值,则对应的就有带权树等概念。

(5)结点的带权路径长度:从该结点到树根之间的路径长度与结点上权的乘积。

(6)树的带权路径长度:树中所有叶子结点的带权路径长度之和,通常记作 W P L = ∑ k = 1 n w k l k WPL=\sum_{k=1}^{n}{w_k}{l_k} WPL=k=1nwklk

(7)哈夫曼树:假设有m个权值{ w 1 , w 2 , ⋅ ⋅ ⋅ , w m w_1,w_2,···,w_m w1,w2,,wm},可以构造一颗含有n个叶子结点的二叉树,每个叶子结点的权为 w i w_i wi,则其中带权路径长度WPL最小的二叉树称作最优二叉树或哈夫曼树。

5.5.2 哈夫曼树的构造算法

1.哈夫曼树的构造过程

2.哈夫曼算法的实现

由于哈夫曼树中没有度为1的结点,则一颗有n个叶子结点的哈夫曼树共有2n-1个结点。

哈夫曼树的存储表示:

typedef struct{
    int weight;					//结点的权值
    int parent,lchild,rchild;	//结点的双亲、左孩子、右孩子
}*HuffmanTree;					//动态分配数组存储哈夫曼树

为了实现方便,数组0号单元不使用,从1号单元开始使用,所以数组大小为2n。叶子结点存储在前面部分1~n个位置,后面的n-1个位置存储其余非叶子结点。

构造哈夫曼树算法实现可以分为两部分:

①初始化:首先动态申请2n个单元;然后循环2n-1次,从1号单元开始,依次将1至2n-1所有单元中的双亲、左孩子、右孩子的下标都初始化为0,;最后循环n次,输入前n个单元中叶子结点的权值。

②创建树:循环n-1次,通过n01次的选择、删除与合并来创建哈夫曼树。选择是从当前森林中选择双亲为0且权值最小的两个树根节点s1和s2;删除是指将结点s1和s2的双亲改为非0;合并就是将s1和s2的权值和作为一个新结点的权值依次存入到数组的第n+1之后的单元中,同时记录这个节点做孩子的下标为s1,右孩子的下标为s2。

第六章 图

6.1 图的定义和基本术语

6.1.1 图的定义

图由两个集合V和E组成,记为G=(V,E),其中V是顶点的有穷非空集合,E是V中顶点欧对的有穷集合,这些顶点偶对称为边。V(G)和E(G)通常分别表示图G的顶点集合和边集合。

无向图:每条边都没有方向

数据结构课程学习笔记_第1张图片

有向图:每条边都有方向

image-20210421175614149

完全图:任意两点都有一条边相连

6.1.2 图的基本术语

**顶点的度:**与该顶点相关联的边的数目,记为TD(v)。

在有向图中,顶点的度等于该顶点的入度与出度之和。

顶点v的入度是以v为终点的有向边的条数,记做ID(v),

顶点v的出度是以v为始点的有向边的条数,记做OD(v)

  • 问:当有向图仅有1个顶点的入度为0,其余顶点的入度均为1,此时是何结构?
  • 答:是一棵树;一颗有向树。

**路径:**连续的边构成的顶点序列。

**路径长度:**路径上边或弧的数目/权值之和。

**回路(环):**第一个顶点和最后一个顶点相同的路径。

**简单路径:**路径中各顶点均不相同的路径。

**简单回路(简单环):**除路径起点和终点相同外,其余顶点均不相同的路径。

数据结构课程学习笔记_第2张图片


**连通图(强连通图):**在无(有)向图G=( V, {E} )中,若对任何两个顶点 v、u 都存在从v 到 u 的路径,则称G是连通图(强连通图)。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BGedkaUb-1649591834339)(https://cdn.jsdelivr.net/gh/qnjy/images/data/1619599384-image-20210428164304480.png)]

**连通分量:**无向图G 的极大连通子图称为G的连通分量。极大连通子图意思是:该子图是 G 连通子图,将G 的任何不在该子图中的顶点加入,子图不再连通。

image-20210428164349664

**强连通分量:**有向图G 的极大强连通子图称为G的强连通分量。极大强连通子图意思是:该子图是G的强连通子图,将D的任何不在该子图中的顶点加入,子图不再是强连通的。

image-20210428164441752

**子图:**设有两个图G=(V,{E})、G1=(V1,{E1}),若V1 V,E1  E,则称 G1是G的子图。例:(b)、© 是 (a) 的子图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cHt2eidz-1649591834340)(https://cdn.jsdelivr.net/gh/qnjy/images/data/1619599589-image-20210428164628941.png)]

**极小连通子图:**该子图是G 的连通子图,在该子图中删除任何一条边,子图不再连通。(n-1条边)

**生成树:**包含无向图G 所有顶点的极小连通子图。

**生成森林:**对非连通图,各个连通分量的生成树构成的集合。

image-20210428164740239

6.2 图的存储结构

图没有顺序存储结构,但可以借助二维数组来表示元素之间的关系,即邻接矩阵表示法。另一方面,图的链式存储有邻接表、十字链表和邻接多重表。

6.2.1 邻接矩阵

顺序表(邻接矩阵)表示法

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

②设图A=(V,E)有n个顶点,则图的邻接矩阵是一个二维数组A.Edge[n][n]定义为:
A . E d g e [ i ] [ j ] = { 1 ,     如 果   < i , j > ∈ E   或 者   ( i , j ) ∈ E 0 ,     反 之 A.Edge[i][j]=\begin{cases}1,  如果 {\in}E 或者 (i,j){\in}E\\ 0,  反之 \end{cases} A.Edge[i][j]={1,   <i,j>E  (i,j)E0,  
无向图的邻接矩阵表示法

数据结构课程学习笔记_第3张图片

分析1:无向图的邻接矩阵是对称的;

分析2:顶点i 的度=第 i 行 (列) 中1 的个数;

分析3:完全图的邻接矩阵中,对角元素为0,其余为1。

有向图的邻接矩阵表示法

image-20210428172539618

注意:在有向图的邻接矩阵中,

第i行含义:以结点 v i v_i vi为尾的弧(即出度边);

第i列含义:以节点 v i v_i vi为头的弧(即入度边)。

分析1:有向图的邻接矩阵可能是不对称的。

分析2:顶点的出度=第i行元素之和

顶点的入度=第i列元素之和

顶点的度=第i行元素之和+第i列元素之和

网(有权图)的邻接矩阵表示法

定义为:
N . E d g e [ i ] [ j ] = { W i j     < v i , v j > 或 ( v i , V j ) ∈ V R ∞       无 边 ( 弧 ) N.Edge[i][j]=\begin{cases} W_{ij}  或(v_i,V_j){\in}V_R\\ \infty   无边(弧) \end{cases} N.Edge[i][j]={Wij  <vi,vj>(vi,Vj)VR   
image-20210429081052350

邻接矩阵的存储表示

#define MaxInt 3267//表示极大值
#define MVNum 100//最大顶点数
typedef char VerTexType;//假设顶点的数据类型为字符型
ttypedef int ArcType;//假设边的权值类型为整型
typedef struct{
    VerTexType vexxs[MVNum];//顶点表
    ArcType arcs[MVNum][MVNum];//邻接矩阵
    int vexnum,arcnum;//图的当前点数和边数
}AMGraph;

算法:采用邻接矩阵表示法创建无向网

算法步骤:

①输入总顶点数和总边数

②依次输入店的信息存入顶点表中

③初始化邻接矩阵,使每个权值初始化为极大值。

④构造邻接矩阵。依次输入每条边依附的顶点和其权值,确定两个顶点在图中的位置之后,使相应边赋予相应的权值,同时使其对称边赋予相同的权值。

Status CreateUDN(AMGraph &G){
    //采用邻接矩阵表示法,创建无向网G
    cin>>G.vexnum>>G.arcnum;//输入总顶点数,总边数
    for(i=0;i>G.vexs[i];
    for(i=0;ivexnum;j++)
            G.arcs[i][j]=MaxInt;//构造邻接矩阵
    for(k=0;k>v1>>v2>>w;//输入一条边依附的顶点及权值
        i=LocateVex(G,v1);j=LocateVex(G,v2);//确定v1和v2在G中的位置,即顶点数组的下标
        G.arcs[i][j]=w;//边的权值为w
        G.arcs[j][i]=G.arcs[i][j];//置的对称边的权值为w
    }//for
    return OK;
}

算法分析

该算法的时间复杂度是 O ( n 2 ) O(n^2) O(n2)

6.2.2 链式存储结构

  • 邻接表

  • 十字链表

  • 邻接多重表

6.3 图的遍历

6.3.1 深度优先搜索

对于一个连通图,深度优先搜索遍历的过程如下:

①从图中某个顶点v出发,访问v。

②找出刚访问过的顶点的第一个未被访问的邻接点,访问该顶点。以该顶点为新顶点,重复此步骤,直至刚访问的顶点没有被访问的邻接点为止。

③返回前一个访问过的且仍有未被访问的邻接点的顶点,找出该顶点的下一个未被访问的邻接点,访问该顶点。

④重复步骤②和③,直至图中所有顶点都被访问过,搜索结束。

访问顺序是: v 1 − > v 2 − > v 4 − > v 8 − > v 5 − > v 3 − > v 6 − > v 7 v_1->v_2->v_4->v_8->v_5->v_3->v_6->v_7 v1>v2>v4>v8>v5>v3>v6>v7

6.3.2 广度优先搜索

广度优先搜索遍历过程如下:

①从图中某个顶点v出发,访问v。

②依次访问v的各个未被访问过的邻接点。

③分别从这些邻接点出发依次访问它们的邻接点,并使“先被访问的顶点的邻接点”先于“后被访问的顶点的邻接点”被访问。重复步骤③,直至图中所有已被访问的顶点的邻接点都被访问到。

图6.17访问顺序: v 1 − > v 2 − > v 3 − > v 4 − > v 5 − > v 6 − > v 7 − > v 8 v_1->v_2->v_3->v_4->v_5->v_6->v_7->v_8 v1>v2>v3>v4>v5>v6>v7>v8

6.4 图的应用

第七章 查找

7.1 查找的基本概念

  • 查找表:

    由同一类型的数据元素(或记录)构成的集合

  • 静态查找表:

    查找的同时对查找表不做修改操作(如插入和删除)

  • 动态查找表:

    查找的同时对查找表具有修改操作

  • 关键字

    记录中某个数据项的值,可用来识别一个记录

  • 主关键字:

    唯一标识数据元素的值

  • 次关键字:
     可以标识若干个数据元素

  • 平均查找长度(关键字的平均比较次数)

A S L = ∑ i = 1 n p i c i ASL=\sum_{i=1}^{n}p_ic_i ASL=i=1npici

n:记录的个数

p i p_i pi:查找第i个记录的概率(通常认为 p i p_i pi=1/n)

c i c_i ci:找到第i个记录所需的比较次数

7.2 线性表的查找

7.2.1 顺序查找

顺序查找适用于顺序结构,也适用于链式结构

数据元素类型的定义如下

typedef struct{
    KeyType key;
    InfoType otherindo;
}ElemType;

顺序表的定义如下

typedef struct{
    ElemType *R;//表示基地址
    int length;//表示长度
}SSTable

算法步骤:

int Search_Seq(SSTable ST,KeyType key)
{
    ST.R[0].key=key;
    for(i=ST.length;ST.R[i].key!=key;--i)
        return i;
}

算法的时间复杂度是O(n)

平均查找长度: A S L = 1 n ∑ i = 1 n i = n + 1 2 ASL=\frac{1}{n}\sum_{i=1}^{n}i=\frac{n+1}{2} ASL=n1i=1ni=2n+1

7.2.2 折半查找

只适用于顺序结构

算法步骤:

①置查找区间初值,low为1,high为表长

②当low小于等于high时,循环执行以下操作

  • mid取值为low和high的中间值
  • 将给定值key与中间位置记录的关键字进行比较,若相等则查找成功,返回中间位置mid;
  • 若不想等则利用中间位置记录将表对分成前、后两个子表。如果key比中间位置记录的关键字小,则high取为mid-1,否则low取为mid+1。

③循环结束,说明查找区间为空,则查找失败,返回0。

int Search_Bin(SSTable ST,KeyType key){
    low=1;high=ST.length;
    while(low<=high){
        mid=(low+high)/2;
        if(key==ST.R[mid].key) 
            return mid;
        else if
            (key<ST.R[mid].key) high = mid-1;
        else 
            low = mid+1;
    }
    return 0;
}

时间复杂度是 O ( l o g 2 n ) O(log_2n) O(log2n)

平均查找长度: A S L = ∑ i = 1 n P i C i = 1 n ∑ j = 1 h j ⋅ 2 j − 1 = n + 1 n l o g 2 ( n + 1 ) − 1 ASL=\sum_{i=1}^{n}P_iC_i=\frac{1}{n}\sum_{j=1}^{h}j·2^{j-1}=\frac{n+1}{n}log_2(n+1)-1 ASL=i=1nPiCi=n1j=1hj2j1=nn+1log2(n+1)1

当n较大时近似为: A S L = l o g 2 ( n + 1 ) − 1 ASL=log_2(n+1)-1 ASL=log2(n+1)1

7.2.3 分块查找

7.3 树表的查找

7.3.1 二叉排序树

二叉排序树或是空树,或是满足如下性质的二叉树:

(1)若其左子树非空,则左子树上所有结点的值均小于根结点的值;

(2)若其右子树非空,则右子树上所有结点的值均大于等于根结点的值;

(3)其左右子树本身又各是一棵二叉排序树

结点的数据域的类型定义

typedef Struct{
    KeyType key;
    IndoType otherindo;
}ElemType;
typedef struct BSTNode{
    ElemType data;
    struct BSTNode *lchild,*rchild;
}BSTNode,*BSTree;

算法7.4 二叉排序树的递归查找

①若二叉排序树为空,则查找失败,返回空指针。

②若二叉排序树非空,将给定值key与根结点的关键字T->data.key进行比较:

  • 若key等于T->data.key,则查找成功,返回根节点地址
  • 若key小于T->data.key,则递归查找左子树
  • 若key大于T->data.key,则递归查找右子树
BSTree SearchBST(BSTree T,KeyType key){
    if((!T) || key==T->data.key)
        return T;
    else if(key<T->data.key)
        return SearchBST(T->lchild,key);
    else
        return SearchBST(T->rchild,key);
}

7.3.2 平衡二叉树

1.AVL树简介

AVL树的名字来源于它的发明作者G.M. Adelson-Velsky 和 E.M. Landis。AVL树是最先发明的自平衡二叉查找树(Self-Balancing Binary Search Tree,简称平衡二叉树)。

平衡二叉树定义(AVL):它或者是一颗空树,或者具有以下性质的二叉排序树:它的左子树和右子树的深度之差(平衡因子)的绝对值不超过1,且它的左子树和右子树都是一颗平衡二叉树。

平衡二叉树或者是空树,或者是具有如下特征的二叉排序树:

1.左子树和右子树也是平衡二叉树

2.每个结点的左子树和右子树高度差最多为1

数据结构课程学习笔记_第4张图片

图一中左边二叉树的节点45的左孩子46比45大,不满足二叉搜索树的条件,因此它也不是一棵平衡二叉树。

右边二叉树满足二叉搜索树的条件,同时它满足条件二,因此它是一棵平衡二叉树。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Vz3rEWNM-1649591834342)(https://cdn.jsdelivr.net/gh/qnjy/images/data/20210527-image-20210527080726469.png)]

左边二叉树的节点45左子树高度2,右子树高度0,左右子树高度差为2-0=2,不满足条件二;

右边二叉树的节点均满足左右子树高度差至多为1,同时它满足二叉搜索树的要求,因此它是一棵平衡二叉树。

AVL树的查找、插入、删除操作在平均和最坏的情况下都是O(logn),这得益于它时刻维护着二叉树的平衡。如果我们需要查找的集合本身没有顺序,在频繁查找的同时,也经常的插入和删除,AVL树是不错的选择。不平衡的二叉查找树在查找时的效率是很低的,因此,AVL如何维护二叉树的平衡是我们的学习重点。

2.AVL树相关概念

  1. 平衡因子:将二叉树上节点的左子树高度减去右子树高度的值称为该节点的平衡因子BF(Balance Factor)。

    在图二右边的AVL树上:

    节点50的左子树高度为3,右子树高度为2,BF= 3-2 = 1;

    节点45的左子树高度为2,右子树高度为1,BF= 2-1 = 1;

    节点46的左子树高度为0,右子树高度为0,BF= 0-0 = 0;

    节点65的左子树高度为0,右子树高度为1,BF= 0-1 = -1;

    对于平衡二叉树,BF的取值范围为[-1,1]。如果发现某个节点的BF值不在此范围,则需要对树进行调整。

  2. 最小不平衡子树:距离插入节点最近的,且平衡因子的绝对值大于1的节点为根的子树.。

image-20210527081029408

在图三中,左边二叉树的节点45的BF = 1,插入节点43后,节点45的BF = 2。节点45是距离插入点43最近的BF不在[-1,1]范围内的节点,因此以节点45为根的子树为最小不平衡子树。

3.AVL树的平衡调整

定义平衡二叉树结点结构

typedef struct Node{
    int key;
    struct Node *left;
    struct Node *right;
    int height;
}BTNode;

整个实现过程是通过在一棵平衡二叉树中依次插入元素(按照二叉排序树的方式),若出现不平衡,则要根据新插入的结点与最低不平衡结点的位置关系进行相应的调整。分为LL型、RR型、LR型和RL型4种类型,各调整方法如下(下面用A表示最低不平衡结点):

  1. LL型调整:

由于在A的左孩子(L)的左子树(L)上插入新结点,使原来平衡二叉树变得不平衡,此时A的平衡因子由1增至2。下面图1是LL型的最简单形式。显然,按照大小关系,结点B应作为新的根结点,其余两个节点分别作为左右孩子节点才能平衡,A结点就好像是绕结点B顺时针旋转一样。

image-20210527081355420

LL型调整的一般形式如下图2所示,表示在A的左孩子B的左子树BL(不一定为空)中插入结点(图中阴影部分所示)而导致不平衡( h 表示子树的深度)。这种情况调整如下:①将A的左孩子B提升为新的根结点;②将原来的根结点A降为B的右孩子;③各子树按大小关系连接(BL和AR不变,BR调整为A的左子树)。

数据结构课程学习笔记_第5张图片

代码实现:

数据结构课程学习笔记_第6张图片

BTNode *ll_rotate(BTNode *y)
{
    BTNode *x = y->left;
    y->left = x->right;
    x->right = y;   
 
    y->height = max(height(y->left), height(y->right)) + 1;
    x->height = max(height(x->left), height(x->right)) + 1;
 
    return x;
}
  1. RR型调整:

由于在A的右孩子®的右子树®上插入新结点,使原来平衡二叉树变得不平衡,此时A的平衡因子由-1变为-2。图3是RR型的最简单形式。显然,按照大小关系,结点B应作为新的根结点,其余两个节点分别作为左右孩子节点才能平衡,A结点就好像是绕结点B逆时针旋转一样。

数据结构课程学习笔记_第7张图片

RR型调整的一般形式如下图4所示,表示在A的右孩子B的右子树BR(不一定为空)中插入结点(图中阴影部分所示)而导致不平衡( h 表示子树的深度)。这种情况调整如下:

将A的右孩子B提升为新的根结点;
将原来的根结点A降为B的左孩子
各子树按大小关系连接(AL和BR不变,BL调整为A的右子树)。

数据结构课程学习笔记_第8张图片

代码实现:

image-20210527081948695

BTNode *rr_rotate(struct Node *y)
{
    BTNode *x = y->right;
    y->right = x->left;
    x->left = y;
    
 
    y->height = max(height(y->left), height(y->right)) + 1;
    x->height = max(height(x->left), height(x->right)) + 1;
 
    return x;
}
  1. LR型调整

由于在A的左孩子(L)的右子树®上插入新结点,使原来平衡二叉树变得不平衡,此时A的平衡因子由1变为2。图5是LR型的最简单形式。显然,按照大小关系,结点C应作为新的根结点,其余两个节点分别作为左右孩子节点才能平衡。

image-20210527082030489

LR型调整的一般形式如下图6所示,表示在A的左孩子B的右子树(根结点为C,不一定为空)中插入结点(图中两个阴影部分之一)而导致不平衡( h 表示子树的深度)。这种情况调整如下:①将B的左孩子C提升为新的根结点;②将原来的根结点A降为C的右孩子;③各子树按大小关系连接(BL和AR不变,CL和CR分别调整为B的右子树和A的左子树)。

image-20210527082102010

代码实现:

数据结构课程学习笔记_第9张图片

BTNode* lr_rotate(BTNode* y)
{
    BTNode* x = y->left;
    y->left = rr_rotate(x);
    return ll_rotate(y);
}
  1. RL型调整:

由于在A的右孩子®的左子树(L)上插入新结点,使原来平衡二叉树变得不平衡,此时A的平衡因子由-1变为-2。图7是RL型的最简单形式。显然,按照大小关系,结点C应作为新的根结点,其余两个节点分别作为左右孩子节点才能平衡。

image-20210527082230583

RL型调整的一般形式如下图8所示,表示在A的右孩子B的左子树(根结点为C,不一定为空)中插入结点(图中两个阴影部分之一)而导致不平衡( h 表示子树的深度)。这种情况调整如下:①将B的左孩子C提升为新的根结点;②将原来的根结点A降为C的左孩子;③各子树按大小关系连接(AL和BR不变,CL和CR分别调整为A的右子树和B的左子树)。

image-20210527082324312

代码实现:

image-20210527082352317

BTNode* lr_rotate(BTNode* y)
{
    BTNode* x = y->left;
    y->left = rr_rotate(x);
    return ll_rotate(y);
}

插入和删除完整代码

#include
#include

typedef struct Node
{
    int key;
    struct Node *left;
    struct Node *right;
    int height;
}BTNode;

int max(int a, int b);


int height(struct Node *N)
{
    if (N == NULL)
        return 0;
    return N->height;
}

int max(int a, int b)
{
    return (a > b) ? a : b;
}

BTNode* newNode(int key)
{
    struct Node* node = (BTNode*)malloc(sizeof(struct Node));
    node->key = key;
    node->left = NULL;
    node->right = NULL;
    node->height = 1;
    return(node);
}

BTNode* ll_rotate(BTNode* y)
{
    BTNode *x = y->left;
    y->left = x->right;
    x->right = y;

    y->height = max(height(y->left), height(y->right)) + 1;
    x->height = max(height(x->left), height(x->right)) + 1;

    return x;
}

BTNode* rr_rotate(BTNode* y)
{
    BTNode *x = y->right;
    y->right = x->left;
    x->left = y;

    y->height = max(height(y->left), height(y->right)) + 1;
    x->height = max(height(x->left), height(x->right)) + 1;

    return x;
}

int getBalance(BTNode* N)
{
    if (N == NULL)
        return 0;
    return height(N->left) - height(N->right);
}

//插入
BTNode* insert(BTNode* node, int key)
{

    if (node == NULL)
        return newNode(key);

    if (key < node->key)
        node->left = insert(node->left, key);
    else if (key > node->key)
        node->right = insert(node->right, key);
    else
        return node;

    node->height = 1 + max(height(node->left), height(node->right));


    int balance = getBalance(node);



    if (balance > 1 && key < node->left->key) //LL型
        return ll_rotate(node);


    if (balance < -1 && key > node->right->key)     //RR型
        return rr_rotate(node);


    if (balance > 1 && key > node->left->key)     //LR型
    {
        node->left = rr_rotate(node->left);
        return ll_rotate(node);
    }

    if (balance < -1 && key < node->right->key)     //RL型
    {
        node->right = ll_rotate(node->right);
        return rr_rotate(node);
    }

    return node;
}


BTNode * minValueNode(BTNode* node)
{
    BTNode* current = node;

    while (current->left != NULL)
        current = current->left;

    return current;
}

//删除
BTNode* deleteNode(BTNode* root, int key)
{

    if (root == NULL)
        return root;

    if (key < root->key)
        root->left = deleteNode(root->left, key);

    else if (key > root->key)
        root->right = deleteNode(root->right, key);

    else
    {
        if ((root->left == NULL) || (root->right == NULL))
        {
            BTNode* temp = root->left ? root->left : root->right;

            if (temp == NULL)
            {
                temp = root;
                root = NULL;
            }
            else
                *root = *temp;
            free(temp);
        }
        else
        {
            BTNode* temp = minValueNode(root->right);

            root->key = temp->key;

            root->right = deleteNode(root->right, temp->key);
        }
    }


    if (root == NULL)
        return root;

    root->height = 1 + max(height(root->left), height(root->right));

    int balance = getBalance(root);


    if (balance > 1 && getBalance(root->left) >= 0) //LL型
        return ll_rotate(root);


    if (balance > 1 && getBalance(root->left) < 0) //LR型
    {
        root->left = rr_rotate(root->left);
        return ll_rotate(root);
    }

    if (balance < -1 && getBalance(root->right) <= 0) //RR型
        return rr_rotate(root);

    if (balance < -1 && getBalance(root->right) > 0)  //Rl型
    {
        root->right = ll_rotate(root->right);
        return rr_rotate(root);
    }

    return root;
}


void preOrder(struct Node *root)
{
    if (root != NULL)
    {
        printf("%d ", root->key);
        preOrder(root->left);
        preOrder(root->right);
    }
}

int main()
{
    BTNode *root = NULL;

    root = insert(root, 9);
    root = insert(root, 5);
    root = insert(root, 10);
    root = insert(root, 0);
    root = insert(root, 6);
    root = insert(root, 11);
    root = insert(root, -1);
    root = insert(root, 1);
    root = insert(root, 2);
    printf("前序遍历:\n");
    preOrder(root);

    /* The constructed AVL Tree would be
                     9
                    /  \
                   1    10
                 /  \     \
                0    5     11
               /    /  \
              -1   2    6
    */

    root = deleteNode(root, 10);
    /* The AVL Tree after deletion of 10
                       1
                     /   \
                    0     9
                  /     /  \
                -1     5     11
                     /  \
                    2    6
    */
    printf("\n");
    printf("前序遍历:\n");
    preOrder(root);
    return 0;
}

7.3.3 B-树

第八章 排序

以下算法都会用到的类型定义

#define MAXSIZE 20
typedef int KeyType;
typedef struct{
    KeyType key;					//关键字项
    InfoType otherinfo;				//其他数据项
}RedType;							//记录类型
typedef struct{
    RedType r[MAXSIZE+1];			//r[0]闲置或用做哨兵单元
    int length;
}SqList;

所有排序算法比较:

image-20210527094036260

8.1 插入排序

8.1.1 直接插入方法排序

基本操作是将一条记录插入到已排好序的有序表中,从而得到一个新的、记录数量增1的有序表。

8.1.2 折半插入排序

8.1.3 希尔排序

1.基本概念

希尔排序(Shell Sort)是插入排序的一种,它是针对直接插入排序算法的改进。

希尔排序又称缩小增量排序,因 DL.Shell 于 1959 年提出而得名。

它通过比较相距一定间隔的元素来进行,各趟比较所用的距离随着算法的进行而减小,直到只比较相邻元素的最后一趟排序为止。

2.适用说明

希尔排序时间复杂度是 O ( n 3 2 ) O(n^{\frac{3}{2}}) O(n23),空间复杂度是 O ( 1 ) O(1) O(1)只能用于顺序结构,不适用于链式结构

3.过程演示

希尔排序目的为了加快速度改进了插入排序,交换不相邻的元素对数组的局部进行排序,并最终用插入排序将局部有序的数组排序。

我们来看下希尔排序的基本步骤,在此我们选择增量 g a p = l e n g t h 2 gap=\frac{length}{2} gap=2length,缩小增量继续以 g a p = g a p 2 gap=\frac{gap}{2} gap=2gap的方式,这种增量选择我们可以用一个序列来表示,{ n 2 , n 2 2 , . . . , 1 \frac{n}{2},\frac{\frac{n}{2}}{2},...,1 2n,22n,...,1},称为增量序列。希尔排序的增量序列的选择与证明是个数学难题,我们选择的这个增量序列是比较常用的,也是希尔建议的增量,称为希尔增量,但其实这个增量序列不是最优的。此处我们做示例使用希尔增量。

如图所示:

(1)初始增量第一趟 g a p = l e n g t h 2 gap=\frac{length}{2} gap=2length

(2)第二趟,增量缩小为2

(3)第三趟,增量缩小为1,得到最终结果

算法伪码:

void ShellInsert(SqlList &L, int dk){
    //对顺序表L做一趟增量是dk的希尔排序
    for(i=dk+1;i<=L.length;++i)
        if(L.r[i].key<L.r[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.r[j];								//将r[0]即原r[i],插入到正确位置
            L.r[j+dk]=L.r[0];
        }
}

void ShellSort(SqList &L,int dt[],int t){
    //按增量序列dt[0...t-1]对顺序表L作希尔排序
    for(k=0;k<t;++k)
        ShellInsert(L,dt[k]);
}

8.2.1 冒泡排序

8.2.2 快速排序

8.3 选择排序

8.3.1 简单选择排序

8.3.2 树形选择排序

8.3.3 堆排序

你可能感兴趣的:(数据结构)