C/C++数据结构

1.数据结构与算法概述

1.1数据结构定义

​ 把现实中大量而复杂的问题,以特定的数据类型(个体)和特定的存储结构(个体之间的关系)保存到主存储器(内存)中,以及在此基础之上为实现某个功能(比如查找某个元素,删除某个元素,对所有元素进行排序)而执行的相应操作,这个相应的操作也叫做算法。

数据结构 = 个体 + 个体之间的关系

算法 = 对存储数据的操作

程序 = 数据的存储 + 数据的操作 + 可以被计算机执行的语言

1.2算法概念

​ 算法(algorithm)是指在解决问题时,按照某种机械的步骤一定可以得到问题的结果(有的问题有解,有的没有)的处理过程。算法就是解决这个问题的方法和步骤的描述。

1.2.1衡量算法的标准

​ 1.时间复杂度:大概程序要执行的次数,而非执行的时间

​ 2.空间复杂度:算法执行过程中大概所占用的最大内存

​ 3.难易程度:算法的难易程度

​ 4.健壮性:算法在遇到错误或崩溃时的容错性

1.3数据的存储结构

1.3.1逻辑结构

所谓逻辑结构就是数据与数据之间的关联关系,准确的说是数据元素之间的关联关系。

注:所有的数据都是由数据元素构成,数据元素是数据的基本构成单位。而数据元素由多个数据项构成。

逻辑结构有四种基本类型:集合结构、线性结构、树状结构和网络结构。也可以统一的分为线性结构和非线性结构。

四种基本类型:

(1)集合结构 : 集合结构中的元素关系,除了同属于一个集合这个关系以外,再无其他关系。

(2)线性结构:线性结构中,元素间的关系就是一对一,顾名思义,一条线性的结构。

(3)树形结构:树形结构中,元素间的关系就是一对多,一颗大叔,伸展出的枝叶,也是类金字塔形。

(4)图形结构:图形结构中,元素间的关系就是多对多,举例:一个人可以通过6个人间接认识到世界上的每一个人。类蛛网形。

统一划分:

​ 线性结构:数组、链表 注:栈和队列是特殊的线性结构

​ 非线性结构:树、图

1.3.2物理结构

​ 数据的物理结构就是数据存储在磁盘中的方式。官方语言为:数据结构在计算机中的表示(又称映像)称为数据的物理结构,或称存储结构。它所研究的是数据结构在计算机中的实现方法,包括数据结构中元素的表示及元素间关系的表示。

物理结构一般有四种:顺序存储,链式存储,散列,索引

参考自https://blog.csdn.net/qq_39385118/article/details/80835048

1.4数据结构的重要性

​ 数据结构是编程中的核心

2.数据结构分类:

2.1线性结构(把所有的结点用一根直线穿起来)

2.1.1连续存储 [数组]

数组中元素类型相同,大小相等

优点:

​ 存取速度快效率高

缺点:

​ 需要占用大块连续且连续的内存

​ 在分配内存前必须指定数组的长度

​ 插入和删除元素效率很低

数组的常用操作(C++)

#include <iostream>
using namespace std;

//定义数组结构体
struct Arr
{
    int* pBase;     //存储数组第一个元素的地址
    int len;          //存储数组的长度
    int cnt;         //存储数组当前有效元素个数
};

void init_arr(Arr* pArr, int length);        //初始化数组 
bool append_arr(Arr* pArr, int value);      //对数组进行追加新值
bool insert_arr(Arr* pArr, int pos, int value);//在pos位置前插入新的数组元素(pos从1开始)
bool delete_arr(Arr* pArr, int pos,int * pVal);//删除pos位置的元素,并返回被删除的元素(pos从1开始)
int get(Arr* pArr, int pos);  //返回指定位置的元素值(pos从1开始)
bool is_myempty(Arr* pArr);   //判断数组是否为空
bool is_full(Arr* pArr);      //判断数组是否已满   
void sort_arr(Arr*pArr);      //升序排列
void show_arr(Arr * pArr);    //遍历数组
void inversion_arr(Arr*pArr); // 倒置数组
int main()
{
    Arr arr;
    int val;
    init_arr(&arr,6);
    append_arr(&arr, 1);
    append_arr(&arr, 2);
    append_arr(&arr, 3);
    append_arr(&arr, 4);
    append_arr(&arr, 5);
    inversion_arr(&arr);
    sort_arr(&arr);
    show_arr(&arr);
    cout << get(&arr, 4) << endl;;
    return 0;
}

// 创建数组
void init_arr(Arr * pArr,int length)
{
    pArr->pBase = (int*)malloc(sizeof(int) * length);   //为数组首地址动态分配内存
    if (pArr->pBase == NULL)
    {
        cout << "动态内存分配失败!" << endl;
        exit(-1);
    }
    else
    {
        pArr->cnt = 0;
        pArr->len = length;
    }
    return;
}

// 判断数组是否为空
bool is_myempty(Arr* pArr)
{
    if (pArr->cnt == 0)
        return true;
    else
        return false;

}

// 遍历数组
void show_arr(Arr* pArr)
{
    if (is_myempty(pArr))
    {
        cout << "数组为空!" << endl;
    }
    else
    {
        for (int i = 0; i < pArr->cnt; i++)
        {
            cout << "a["<< i<<"]=" << *(pArr->pBase + i) << endl;
        }
    }
}

// 追加数据
bool append_arr(Arr* pArr, int value)
{
    if (is_full(pArr))
    {
        cout << "添加失败!数组已满!" << endl;
        return false;
    }
    else
    {
        pArr->pBase[pArr->cnt] = value;
        (pArr->cnt)++;
        return true;
    }
}

// 判断数组是否已满
bool is_full(Arr* pArr)
{
    if (pArr->cnt == pArr->len)
        return true;
    else
        return false;
}

//插入新的数组元素(pos从1开始)
bool insert_arr(Arr* pArr, int pos, int value)
{
    if (is_full(pArr))
        return false;
    if (pos<1 || pos>pArr->cnt + 1)
        return false;
    for (int i = pArr->cnt - 1; i >= pos - 1; i--)
    {
        pArr->pBase[i + 1] = pArr->pBase[i];
    }
    pArr->pBase[pos - 1] = value;
    pArr->cnt++;
    return true;
}

//删除
bool delete_arr(Arr* pArr, int pos, int* pVal)
{
    if (is_full(pArr))
        return false;
    if (pos<1 || pos>pArr->cnt)
        return false;
    *pVal = pArr->pBase[pos - 1];
    for (int i = pos; i <pArr->cnt; i++)
    {
        pArr->pBase[i -1] = pArr->pBase[i];
    }
    pArr->cnt--;
    return true;
}

//倒置数组
void inversion_arr(Arr* pArr)
{
    int i = 0;
    int j = pArr->cnt - 1;
    int temp;
    while (i < j)
    {
        temp = pArr->pBase[i];
        pArr->pBase[i] = pArr->pBase[j];
        pArr->pBase[j] = temp;
        i++;
        j--;
    }
    return ;
}

//升序排列
void sort_arr(Arr* pArr)
{
    int i, j,temp;
    for (i = 0; i < pArr->cnt; i++)
    {
        for (j = i + 1; j < pArr->cnt; j++)
        {
            if (pArr->pBase[i] > pArr->pBase[j])
            {
                temp = pArr->pBase[i];
                pArr->pBase[i] = pArr->pBase[j];
                pArr->pBase[j] = temp;
            }
        }
    }
}

//返回指定位置的元素值
int get(Arr* pArr, int pos)
{
    return*(pArr->pBase + pos - 1);
}
2.1.2离散存储 [链表]

特点:

​ 1)n个结点离散分配

​ 2)彼此通过指针相连

​ 3)每个结点只有一个前驱结点,每个结点只有一个后续结点

​ 4)首结点没有前驱结点,尾结点没有后续结点

优点:

​ 空间没有限制,插入和删除元素速度快

缺点:

​ 存取的速度慢

常用术语:

​ 首结点:第一个有效结点

​ 尾结点:最后一个有效结点

​ 头结点:首结点之前的结点,数据类型与首结点相同。注:头结点不存放有效数据,目的是为了方面对链表的操作

​ 头指针:指向头结点的指针变量(存放头结点的地址)

​ 尾指针 :指向尾结点的指针变量(存放尾结点的地址)

确定链表需要的参数

​ 头指针

链表的分类

​ 单链表

​ 双链表:每个结点有两个指针域

​ 循环链表:能通过任何一个结点找到其他所有的结点

​ 非循环链表

链表常用算法(C++):

#include <iostream>
using namespace std;

//定义结点结构体
typedef struct  Node
{
    int data;            //数据域,存放结点中的数据
    Node* pNext;   //指针域,指向下一个结点的指针
}NODE,* PNODE;      //NODE等价Node类型(表示结点),pNODE等价于Node*(表示结点的地址)

PNODE create_list();                          //创建链表,并返回链表的头结点的地址
void traverse_list(PNODE pHead);   //遍历链表
bool is_empty(PNODE pHead);         //判断链表是否为空
int length_list(PNODE pHead);           //求链表长度
bool insert_list(PNODE pHead, int pos, int value);      //把指定结点值插入到指定位置(pos的值从1开始)
bool delete_list(PNODE pHead, int pos, int* value);     //删除指定位置的结点,并返回删除值(pos的值从1开始)
void sort_list(PNODE pHead);    //对链表进行升序排列

 int main()
{
    PNODE pHead = NULL; //创建一个头指针
    pHead = create_list();      //创建一个非循环单链表,并将链表的头结点地址赋给头指针
    traverse_list(pHead);   //遍历链表
    cout << length_list(pHead) << endl;//输出链表的长度
    sort_list(pHead);
    cout << endl;
    insert_list(pHead, 1, 99);
    int d;
    delete_list(pHead, 3, &d);
    cout << d << endl;
    traverse_list(pHead);
    return 0;
}

 //创建链表
PNODE create_list()
{
    int len;
    int val;
    cout << "请输入您要生成链表结点的个数:" << endl;
    cin >> len;
    PNODE pHead = (PNODE)malloc(sizeof(NODE)); //为头结点分配内存(不存放有效数据,目前数据域为空,指针域为空)
    PNODE pTail = pHead;        //初始化一个尾结点,地址等于头结点地址
    pTail->pNext = NULL;        //初始化尾结点的指针域为空
    if (pHead == NULL)
    {
        cout << "内存分配失败,程序终止!";
        exit(-1);
    }
    for (int i = 0; i < len; i++)
    {
        cout << "请输入第"<<i+1<<"个结点的值:" << endl;
        cin >> val;
        PNODE pNew = (PNODE)malloc(sizeof(NODE));
        if (pNew == NULL)
        {
            cout << "内存分配失败,程序终止!";
            exit(-1);
        }
        pNew->data = val;       //定义新结点的数据域
        pTail->pNext = pNew;    //使尾结点的指针域指向新结点
        pNew->pNext = NULL; //使新结点指向空
        pTail = pNew;                   //尾结点时刻指向创建的新结点
    }

    return pHead;
}

//遍历链表
void traverse_list(PNODE  pHead)
{
    PNODE p = pHead->pNext;     //创建首结点
    while (p!=NULL)
    {
        cout << p->data <<"\t";
        p = p->pNext;
    }
    return;
}

//判断链表是否为空
bool is_empty(PNODE pHead)
{
    if (pHead->pNext == NULL)
        return true;
    else
        return false; 
}

//求链表长度
int length_list(PNODE pHead)
{
    int length = 0;
    PNODE p = pHead->pNext;     //创建首结点
    while (p != NULL)
    {
        length++;
        p = p->pNext;
    }
    return length;
}

//对链表进行升序操作
void sort_list(PNODE pHead)
{
    int len = length_list(pHead);
    PNODE p, q;     //用于交换data值
    int i, j, t;       //ij用于计数,t用于传值
    for(i = 0,p = pHead->pNext;i<len-1;i++,p=p->pNext)
        for (j = i + 1, q = p->pNext; j < len; j++, q = q->pNext)
        {
            if (p->data > q->data)
            {
                t = p->data;
                p->data = q->data;
                q->data = t;
            }
        }
}

// 插入操作
bool insert_list(PNODE pHead, int pos, int value)
{
    int i = 0;
    PNODE p = pHead;
    while (p != NULL && pos - 1 > i)    //当头指针存在时定义pos-1<i的作用是使p指向所插入位置的前一个结点
    {
        p = p->pNext;
        i++;
    }
    if (pos - 1 < i || p == NULL)
        return false;


    PNODE pNew = (PNODE)malloc(sizeof(NODE));//创建新的结点
    if (pNew == NULL)
    {
        cout << "内存分配失败,程序终止!";
        exit(-1);
    }
    pNew->data = value;     //对新的结点赋值
    PNODE q = p->pNext;     //定义临时结点q等于插入位置结点
    p->pNext = pNew;        //插入位置前的结点指向下一个结点(即插入位置结点)等于新的结点
    pNew->pNext = q;        //新结点的下一个结点等于插入位置结点(插入后即为插入位置的下一个结点)
}

//删除操作
bool delete_list(PNODE pHead, int pos, int* value)
{
    int i = 0;
    PNODE p = pHead;
    while (p->pNext != NULL && pos-1  > i)    //当所要删除的结点不为空时,定义pos-1<i的作用是使p指向所删除位置的前一个结点
    {
        p = p->pNext;
        i++;
    }
    if (pos-1 < i || p->pNext == NULL)
        return false;

    PNODE q = p->pNext;
    *value = q->data;
    p->pNext = q->pNext;
    free (q);
    q = NULL;
}

算法:

​ 狭义的算法与数据的存储方式密切相关

​ 广义的算法与数据的存储方式无关

泛型:

​ 利用某种技术达到的效果:不同的存储方式,执行的操作是一样的

2.2线性结构的常见应用之一-----栈

定义:一种可以实现“先进后出”的存储结构,只能在栈顶出入,由栈顶指向栈底(从栈顶开始访问)。

分类

1)静态栈(连续)

​ 类似于数组的结构关系

2)动态栈(不连续)

​ 即链式栈

栈常用算法(C++):

#include <iostream>
using namespace std;

//创建结构体用于存储结点信息
typedef struct Node
{
    int data;           //数据域,存储结点数据
    Node* pNext;  //指针域,存储下一个结点地址
}NODE, * PNODE;

//创建结构体用于存储栈的顶和底
typedef struct Stack       
{
    PNODE pTop;            //指向栈顶
    PNODE pBottom;      //指向栈底
}STACK,* PSTACK;

void init_Stack(PSTACK pS);      //初始化一个空栈
bool push_Stack(PSTACK pS,int value);    //压栈
void traverse_Stack(PSTACK pS);     //遍历栈
bool pop(PSTACK pS, int* pValue);        //出栈
bool is_empt(PSTACK pS);        //判断栈是否为空
void clear(PSTACK pS);          //清空栈中的有效数据,变成空栈
int main()
{
    STACK S;
    int value;      //存放出栈的元素值
    init_Stack(&S);
    push_Stack(&S, 5);
    push_Stack(&S, 4);
    push_Stack(&S, 3);
    push_Stack(&S, 2);
    push_Stack(&S, 415);
    traverse_Stack(&S);
    clear(&S);
    traverse_Stack(&S);
    return 0;
}

// 初始化栈
void init_Stack(PSTACK pS)
{
    pS->pTop = (PNODE)malloc(sizeof(NODE));     //分配无有效值的结点(头结点)内存,并把地址传给栈顶指针
    if (pS->pTop == NULL)
    {
        cout << "动态内存分配失败,程序已退出!" << endl;
        exit(-1);
    }
    else
    {
        pS->pBottom = pS->pTop;     //初始化时栈顶指针与栈底指针都指向头结点
        pS->pTop->pNext = NULL;
    }
    return;
}

//判断栈是否为空
bool is_empt(PSTACK pS)
{
    if (pS->pBottom == pS->pTop)
        return true;
    else
        return false;
}
//压栈操作
bool push_Stack(PSTACK pS, int value)
{
    PNODE pNew = (PNODE)malloc(sizeof(NODE));
    if (pS->pTop == NULL)
    {
        cout << "动态内存分配失败,程序已退出!" << endl;
        exit(-1);
    }
    else
    {
        pNew->data = value;
        pNew->pNext = pS->pTop;
        pS->pTop = pNew;
    }
    return true;
}

//遍历操作
void traverse_Stack(PSTACK pS)
{
    PNODE p = pS->pTop;
    while (p != pS->pBottom)
    {
        cout << p->data << "\t";
        p = p->pNext;
    }
    return;
}

//出栈操作
bool pop(PSTACK pS, int* pValue)
{
    if (is_empt(pS))
    {
        return false;
    }
    else
    {
        PNODE q = pS->pTop;
        *pValue = q->data;
        pS->pTop = q->pNext;
        free(q);
        q = NULL;
        return true;
    }
}

//清空操作
void clear(PSTACK pS)
{
    if (is_empt(pS))
    {
        return;
    }
    else
    {
        PNODE p = pS->pTop;
        PNODE q = NULL;
        while (p != pS->pBottom)
        {
            q = p->pNext;
            free(p);
            p = q;
        }
        pS->pBottom = pS->pTop;
    }
}

栈的应用:

1)函数的嵌套调用(利用了栈的压栈和出栈)

2)中断

3)表达式求值

4)内存分配

5)缓存处理

6)迷宫

2.3线性结构的常见应用之二-----队列

定义:一种可以实现“先进先出 出”的存储结构,队首出队尾入,由队首指向队尾(从队首开始访问)。

分类

1)静态队列(用数组实现)

静态队列必须是循环队列

循环队列相关问题:

  1. 静态队列为什么必须是循环队列?

    因为静态队列是基于数组实现的,如果不用循环队列,会导致删除的元素所使用的空间无法继续使用,造成空间的浪费。

  2. 循环队列需要几个参数来确定?

    需要两个参数确定:front表示队头;rear表示队尾

  3. 循环队列各个参数的含义?

    注意:上面两个参数在下面不同的场合有不同的含义

    ​ 1)队列初始时

    ​ front与rear的值都是零

    ​ 2)队列非空时

    ​ front代表队列的第一个元素

    ​ rear代表队列最后一个有效元素的下一个元素(不存放有效值)

    ​ 3)队列为空时

    ​ front等于rear,但其值不一定为零

  4. 循环队列入队伪算法?

    第一步:将入队的值存入rear所表示的位置

    第二步:更新rear的值:rear = (rear+1)%队列长度

  5. 循环队列出队伪算法?

    直接更新front的值:front = (front+1)%队列长度。

  6. 如何判断循环队列是否为空?

    如果front与rear的值相等,则队列为空。

  7. 如何判断循环队列是否已满?

    两种方式:

    1)增加一个标志参数

    ​ 用参数记录当前有效元素的个数

    2)少用一个元素位置(n个位置只用n-1个)

    ​ if((r+1)%队列长度==f)

    ​ 队列已满

    ​ else

    ​ 队列未满

2)链式队列(用链表实现)

循环队列的常用算法(C++):

#include <iostream>
using namespace std;

//定义队列结构体
typedef struct Queue
{
    int* pBase;   //数组的首地址
    int front;      //存储队首下标
    int rear;       //存储队尾下标
}QUEUE;

void init_queue(QUEUE*pQ);      //初始化队列
bool en_queue(QUEUE* pQ, int value);  //入队
void traverse_queue(QUEUE* pQ);     //遍历队列
bool is_full(QUEUE* pQ);         //判断队列是否已满
bool is_empt(QUEUE* pQ);    //判断队列是否为空
bool out_queue(QUEUE* pQ, int* pValue); //出栈,并返回出栈元素
int main()
{
    QUEUE Q;
    int value;
    init_queue(&Q);
    en_queue(&Q, 1);
    en_queue(&Q, 2);
    en_queue(&Q, 3);
    en_queue(&Q, 4);
    en_queue(&Q, 5);
    traverse_queue(&Q);
    out_queue(&Q, &value);
    cout << value << endl;
    traverse_queue(&Q);
    return 0;
}

//初始化队列
void init_queue(QUEUE* pQ)
{
    pQ->pBase = (int*)malloc(sizeof(int) * 6);      //创建数组长度为6
    pQ->front = 0;
    pQ->rear = 0;
}

//判断队列是否已满
bool is_full(QUEUE* pQ)
{
    if ((pQ->rear + 1) % 6 == pQ->front)
        return true;
    else
        return false;
}

//入队
bool en_queue(QUEUE* pQ, int value)
{
    if (is_full(pQ))
        return false;
    else
    {
        pQ->pBase[pQ->rear] = value;
        pQ->rear = (pQ->rear + 1) % 6;
        return true;
    }
}

//遍历队列
void traverse_queue(QUEUE* pQ)
{
    int i = pQ->front;
    while (i != pQ->rear)
    {
        cout << pQ->pBase[i] << "\t";
        i = (i + 1) % 6;
    }
    cout << endl;
    return;
}

//判断队列是否为空
bool is_empt(QUEUE* pQ)
{
    if (pQ->front == pQ->rear)
        return true;
    else
        return false;
}

//出队
bool out_queue(QUEUE* pQ, int* pValue)
{
    if (is_empt(pQ))
        return false;
    else
    {
        *pValue = pQ->pBase[pQ->front];
        pQ->front = (pQ->front + 1) % 6;
        return true;
    }
}

队列的应用:

​ 所有与时间有关的操作都与队列有关(顺序操作)

3.递归概述

定义:一个函数直接或间接的调用本身

关于函数的调用:

​ 1、当一个函数运行期间调用另一个函数时,在运行被调用函数之前,系统需要完成三件事:

​ 1)将所有的实际参数,返回该函数的地址等信息传递给被调用的函数保存

​ 2)为被调用函数的局部变量(包括形参)分配存储空间

​ 3)将控制转移到被调用函数的入口

​ 2、从被调用函数返回到主调函数之前,系统需要完成的三件事:

​ 1)保存被调用函数的返回结果

​ 2)释放被调用函数所占用的存储空间

​ 3)根据被调用函数之前所保存的主函数地址将控制转移到主调函数

​ 3、当有多个函数相互调用时,按照“后调用的函数先返回先释放空间”的原则,上述函数之间的信息 传递和控制转移必须借助“栈”来实现,系统将整个程序运行时所需要的数据空间安排在一个栈中, 每当调用一个函数时,就在栈顶分配一个存储区域,进行压栈操作,每当一个函数退出时,从栈顶 释放它的存储空间,进行出栈操作,当前运行的函数永远都在栈顶位置。

递归需要满足的三个条件:

​ 1、递归必须得有一个明确的中止条件

​ 2、递归的函数所处理的数据规模必须在递减

​ 3、问题转化到递归后必须时可解的

循环与递归的比较

循环:

​ 优点:速度快、占用存储空间小

​ 缺点:不易被人理解

递归:

​ 优点:易于理解

​ 缺点:速度慢、占用存储空间大

程序举例:(求和、求阶乘、汉诺塔、斐波拉契数列)

#include <iostream>
using namespace std;

//斐波拉契序列
int feibolaxi(int n)
{
    if (n == 1|| n==0)
        return 1;
    else
        return feibolaxi(n - 1)+feibolaxi(n-2);
}

//求和
int qiuhe(int n)
{
    if (n == 1)
        return 1;
    else
        return qiuhe(n - 1) + n;
}

//求阶乘值
int jiecheng(int n)
{
    if (n == 1)
        return 1;
    else
        return jiecheng(n - 1)*n;
}

//求汉诺塔需要移动的次数
int hannuota_count (int n)
{
    if (n == 1)
        return 1;
    else
        return 2*hannuota_count(n - 1)+1 ;
}

//汉诺塔问题的伪算法
/*
    设第一个柱子为A第二个为B第三个为C,有n个盘子
    思路:
        1、先把A柱子上的前n-1个盘子从A借助C移动到B
        2、在将A柱子上的第n个盘子直接从A移动到C
        3、最后将B柱子上的n-1个盘子借助A移动到C

*/
void hannuota(int n, char A, char B, char C)
{
    if (n == 1)     //如果只有一个盘子直接将A柱子上的盘子从A移到C
        printf("直接将编号为%d的柱子从%c柱子移动到%c柱子\n", n, A, C);
    else
    {
        hannuota(n - 1, A, C, B);
        printf("直接将编号为%d的柱子从%c柱子移动到%c柱子\n", n, A, C);
        hannuota(n - 1, B, A, C);
    }
}

int main()
{
    while (1)
    {
        int n;
        cout << "请输入您要计算的阶乘数值:" << endl;
        cin >> n;
        cout << "结果为:" << jiecheng(n) << endl;

        cout << "请输入您要求和的数值:" << endl;
        cin >> n;
        cout << "结果为:" << qiuhe(n) << endl;

        cout << "请输入您要计算的汉诺塔的叠数:" << endl;
        cin >> n;
        cout << "结果为:" << hannuota_count(n) << endl;
        cout << "请输入您要计算的汉诺塔的叠数:" << endl;
        cin >> n;
        cout << "结果为:" << feibolaxi( n) << endl;
    }
    return 0;
}

递归的应用

​ 树和图的算法、数学方式(例如:斐波那契数列)

4.非线性结构

4.1树

4.1.1树的定义

​ 树是由n(n≥1)个有限节点组成一个具有层次关系的[集合]。把它叫做“树”是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。

4.1.2树的特点
  1. 一棵树有且只有一个称为根的结点
  2. 一棵树有若干个互不相交的子树,这些子树本身也是一棵树
  3. 一棵树由结点和边组成
  4. 每个结点只有一个父结点,但可以由多个子结点
  5. 根节点没有父节点
4.1.3专业术语:

深度:根节点开始到最底层结点的层数称为深度(根节点是第一层)

叶子结点:没有子节点的结点

非终端结点(非叶子结点)

:子节点的个数称为度

4.1.4树的分类:

一般树:任意一个结点的子节点个数都不受限制

二叉树:任意一个结点的子结点的个数最多为两个,且子节点的位置不可更改(左右顺序:左子树 右子树是有序的),所以二叉树为有序树

二叉树的分类: 1)一般二叉树

​ 2)满二叉树:在不增加树的层数的前提下,无法再多添加一个结点的二叉树称 为满二叉树

​ 3)完全二叉树:如果只删除了满二叉树最底层最右边的连续若干个结点,这样 形成的二叉树就是完全二叉树(满二叉树是完全二叉树的一个 特例)

森林:n个互不相交的树的集合

4.1.5树的存储

思想:把非线性结构的树转化为线性结构存储

二叉树的存储:

​ 1.连续存储【完全二叉树】

​ 优点:查找某个结点的父节点和子节点速度快

​ 缺点:占用内存空间过大

​ 2.链式存储

一般树的存储:

​ 1.双亲表示法 //易于求父节点

​ 2.孩子表示法 //易于求子结点

​ 3.双亲孩子表示法 //结合上面两个的优点

​ 4.二叉树表示法 //把一个普通树转化为二叉树存储(方法:把任意一个结点的左指针域指向 第一个孩子,右指针域指向它的兄弟) 注:一个普通的树转换成二叉树一 定没有右子树。

4.1.6森林的存储

同样也是转化为二叉树来存储

方法与普通树转化为二叉树相似,不同的是不同的树之间的关系为兄弟关系,如下图。

image-20210807154139975

4.1.7树的常见操作

1.二叉树的遍历

先序遍历[先访问根节点]:

​ 含义:先访问根节点,再先序遍历左子树,最后先序遍历右子树。

中序遍历[中间访问根节点]:

​ 含义:先中序遍历左子树,再访问根节点,最后中序遍历右子树。

后序遍历[最后访问根节点]:

​ 含义:先后序遍历左子树,再后序遍历右子树,最后访问根节点。

4.1.8已知两种遍历序列求原始二叉树

​ 已知先序和中序或已知中序和后序,可以还原原始二叉树。而已知先序和后序无法确定

1.已知先序和中序求后序遍历序列:

​ 例一:

​ 例二:

2.已知后序和中序求先序遍历序列:

3.二叉树三种遍历的代码实现(C++)

#include <iostream>
using namespace std;

//定义结构体存储结点信息
typedef struct BTNode
{
	char data;								//结点的数据域
	struct BTNode* pLchild;		//结点的左值针(指向左孩子)
	struct BTNode* pRchild;		//结点的右值针(指向右孩子)
}BTNODE,*pBTNODE;

pBTNODE create_BTree();		//创建二叉树并返回根节点地址
void PreTraverseBTree(pBTNODE pT);	//先序遍历二叉树
void InTraverseBTree(pBTNODE pT);		//中序遍历二叉树
void PosTraverseBTree(pBTNODE pT);	//后序遍历二叉树
int main()
{
	pBTNODE pT = create_BTree();
	PreTraverseBTree(pT);
	cout << endl;
	InTraverseBTree(pT);
	cout << endl;
	PosTraverseBTree(pT);
	return 0;
}

//创建二叉树
pBTNODE create_BTree()
{
	pBTNODE pA = (pBTNODE)malloc(sizeof(BTNODE));
	pBTNODE pB = (pBTNODE)malloc(sizeof(BTNODE));
	pBTNODE pC = (pBTNODE)malloc(sizeof(BTNODE));
	pBTNODE pD = (pBTNODE)malloc(sizeof(BTNODE));
	pBTNODE pE = (pBTNODE)malloc(sizeof(BTNODE));
	pA->data = 'A';
	pB->data = 'B';
	pC->data = 'C';
	pD->data = 'D';
	pE->data = 'E';

	pA->pLchild = pB;
	pA->pRchild = pC;
	pB->pLchild = NULL;
	pB->pRchild = NULL;
	pC->pLchild = pD;
	pC->pRchild = NULL;
	pD->pLchild = NULL;
	pD->pRchild = pE;
	pE->pLchild = NULL;
	pE->pRchild = NULL;
	return pA;
}

//先序遍历
void PreTraverseBTree(pBTNODE pT)
{
	if (pT != NULL)
	{
		cout << pT->data << endl;
		if (pT->pLchild != NULL)
		{
			PreTraverseBTree(pT->pLchild);
		}
		if (pT->pRchild != NULL)
		{
			PreTraverseBTree(pT->pRchild);
		}
	}
}

//中序遍历
void InTraverseBTree(pBTNODE pT)
{
	if (pT != NULL)
	{
		if (pT->pLchild != NULL)
		{
			InTraverseBTree(pT->pLchild);
		}
		cout << pT->data << endl;
		if (pT->pRchild != NULL)
		{
			InTraverseBTree(pT->pRchild);
		}
	}
}

//后序遍历
void PosTraverseBTree(pBTNODE pT)
{
	if (pT != NULL)
	{
		if (pT->pLchild != NULL)
		{
			PosTraverseBTree(pT->pLchild);
		}
		if (pT->pRchild != NULL)
		{
			PosTraverseBTree(pT->pRchild);
		}
		cout << pT->data << endl;
	}
}
4.1.9树的应用

​ 1.树是数据库中数据组织的一种重要形式

​ 2.操作系统子父进程的关系本身就是一棵树

​ 3.面向对象语言中类的继承关系

​ 4.应用于哈夫曼树

4.2图

6.查找和排序

6.1排序和查找的关系

​ 排序是查找的前提,排序是重点内容。

6.2查找:

6.2.1折半查找

6.3排序:

6.3.1冒泡排序
6.3.2插入排序
6.3.3选择排序
6.3.4快速排序
6.3.5归并排序
	pC->pRchild = NULL;
	pD->pLchild = NULL;
	pD->pRchild = pE;
	pE->pLchild = NULL;
	pE->pRchild = NULL;
	return pA;
}

//先序遍历
void PreTraverseBTree(pBTNODE pT)
{
	if (pT != NULL)
	{
		cout << pT->data << endl;
		if (pT->pLchild != NULL)
		{
			PreTraverseBTree(pT->pLchild);
		}
		if (pT->pRchild != NULL)
		{
			PreTraverseBTree(pT->pRchild);
		}
	}
}

//中序遍历
void InTraverseBTree(pBTNODE pT)
{
	if (pT != NULL)
	{
		if (pT->pLchild != NULL)
		{
			InTraverseBTree(pT->pLchild);
		}
		cout << pT->data << endl;
		if (pT->pRchild != NULL)
		{
			InTraverseBTree(pT->pRchild);
		}
	}
}

//后序遍历
void PosTraverseBTree(pBTNODE pT)
{
	if (pT != NULL)
	{
		if (pT->pLchild != NULL)
		{
			PosTraverseBTree(pT->pLchild);
		}
		if (pT->pRchild != NULL)
		{
			PosTraverseBTree(pT->pRchild);
		}
		cout << pT->data << endl;
	}
}
4.1.9树的应用

​ 1.树是数据库中数据组织的一种重要形式

​ 2.操作系统子父进程的关系本身就是一棵树

​ 3.面向对象语言中类的继承关系

​ 4.应用于哈夫曼树

4.2图

6.查找和排序

6.1排序和查找的关系

​ 排序是查找的前提,排序是重点内容。

6.2查找:

6.2.1折半查找

6.3排序:

6.3.1冒泡排序
6.3.2插入排序
6.3.3选择排序
6.3.4快速排序
6.3.5归并排序
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值