数据结构笔记

本篇是对之前内容做了修订,主要是针对代码错误地方的修改,同时调整了一些地方的排版,感谢各位小伙伴指正!

小伙伴指正的地方列表:

小伙伴昵称 错误地方
BEaR 链表部分 遍历函数 traverse_list(PNODE pHead) 中应该是 while(NULL !=p){ } 使用的是while循环不是if语句吧 :)
mulun2 动态内存的分配和释放 中的  第16行: pArr = 2;//类似于 a[0]=4,因为数组名就是指向了第一个元素的地址,跟pArr一样 这里a[0]=4是不是应该为a[0]=2?
HzY5524 你的选择排序是降序

 ps:需要下载原文件的可以点击文末链接进行下载。

数据结构

概述

  • 数据结构定义

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

    数据结构 = 个体的存储 + 个体的关系存储

    算法 = 对存储数据的操作

  • 算法定义

    • 通俗的说算法是解题的方法和步骤

    • 衡量算法的标准

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

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

      3. 难易程度:用易懂,避免过于复杂。

      4. 健壮性

  • 数据结构的地位

    数据结构是软件中最核心的课程

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

预备知识

  1. 指针

    • 指针的重要性:指针是C语言的灵魂

    • 定义:

      • 地址:内存单元的编号,从0开始的非负整数

      • 指针:指针就是地址,地址就是指针;指针变量是存放内存单元地址的变量;指针的本质是一个操作受限的非负整数。

    • 分类:基本类型的指针;指针和数组的关系

  2. 结构体

    • 为什么会出现结构体:为了表示一些复杂的数据,而普通的基本类型变量无法满足要求;

    • 定义:结构体是用户根据实际需要自己定义的复合数类型;

    • 如何使用结构体

      //定义结构体
       struct Student
       {
           int sid;
           char name[200];
           int age;
       };
       ​
       //1.整体赋值,类似于Java中new类的构造函数 
       struct Student st = {1001,"zhangsan",18};
       //单个赋值
       st.id=1001;
       strcpy(st.name,"zhangsan");
       st.age=18;
       //2.通常使用指针的方式赋值
       struct Student *pst;
       //pst所指向的结构体变量中的sid这个成员
       pst->sid=1001;
       strcpy(pst->name,"lisi");
       pst->age=19;
    • 注意事项:结构体变量不能算术计算,但是可以赋值;普通结构体变量和结构体指针变量作为函数传参的问题,推荐使用传递结构体指针的方式,这样效率高节约内存。

  3. 动态内存的分配和释放   

 样例代码:    

//一维数组:
 ​
 # include <stdio.h>
 # include <malloc.h>
 ​
 int main(void)
 {
  //静态分配数组
  int a[5] = {2,3,5,6,9};
  //len为一维数组的长度,可以根据需求动态分配长度
  int len;
  printf("请输入你需要分配的数组长度:len=");
  scanf("%d",&len);//len=5
  //malloc为分配内存的函数,返回第一个字节的地址,但是默认返回是一个干地址,没有实际意义,必须加强制类型转换为指定的指针类型才有意义,(int *)表示强转为int类型的指针,那么返回的地址指向的就是第一个元素的地址,那么第二个元素的地址就是第一个地址向后挪一位
  int * pArr = (int *)malloc(sizeof(int) * len);
  *pArr = 2;//类似于 a[0]=2,因为数组名就是指向了第一个元素的地址,跟*pArr一样
  pArr[1] = 3; //类似于 a[1]=3
 ​
  free(pArr);//把pArr所代表的动态分配的20个字节的内存释放
 ​
  return 0;
 }
  1. 跨函数使用内存

    # include <stdio.h>
     # include <malloc.h>
     ​
     struct Student
     {
     int sid;
     int age;
     };
     ​
     struct Student * CreateStudent(void);
     void ShowStudent(struct Student *);
     ​
     int main(void)
     {
         struct Student * ps;
      ps = CreateStudent();
      ShowStudent(ps);
      return 0;
     }
     ​
     void ShowStudent(struct Student * pst)
     {
     printf("%d %d",pst->sid,pst->age);
     }
     ​
     struct Student * CreateStudent(void)
     {
      struct Student * p = (struct Student *)malloc(sizeof(struct Student));
      p->sid = 1001;
      p->age = 18;
      return p;
     }
  2. typedef函数的使用

    typedef int INT; // 相当于给int起了一个别名 INT别名 INT
     ​
     typedef struct Student
     {
      int sid;
      char name[100];
      char sex;
     } ST; //ST st 就相当于 struct Student st,给struct Student 起了别名ST,这样简洁了代码st,给struct Student 起了别名ST,这样简洁了代码
     ​
     typedef struct Student
     {
      int sid;
      char name[100];
      char sex;
     } * ST; //ST就相当于struct Student *

线性结构

把所有节点用一条直线串起来

  • 连续存储【数组】

    1. 什么叫数组:元素类型相同,大小相等

    2. 数组的优缺点

      • 优点:存取速度很快

      • 缺点:插入删除元素很慢

      数组例子

      # include <stdio.h>
       # include <malloc.h>
       # include <stdlib.h>
       # include <stdbool.sh>
       ​
       struct Arr
       {
        int * pBase;//数组首地址
        int cnt;//当前数组内已有元素长度
        int len;//数组长度
       };
       ​
       //初始化数组
       void init_arr(struct Arr * pArr,int length);
       //获取某个位置上的元素
       int get(int pos);
       //判断数组是否已经满了
       bool is_full(struct Arr * pArr);
       //判断数组是否为空
       bool is_empty(struct Arr * pArr);
       //打印数组内所有元素
       void show_arr(struct Arr * pArr);
       //向数组中追加元素
       void append_arr(struct Arr * pArr,int val);
       //向数组某个位置插入元素
       void insert_arr(struct Arr * pArr,int pos,int val);
       //排序
       void sort_arr (struct Arr * pArr);
       ​
       int main(void)
       {
        struct Arr arr;
        init_arr(&arr,6);
       ​
       ​
        return 0;
       }
       ​
       void init_arr(struct Arr * pArr,int length)
       {
        pArr->pBase = (int *)malloc(sizeof(struct Arr) * length);
        if ( NULL == pArr->pBase)
        {
            printf("动态内存分配失败\n");
            exit(-1);
        }
        else
        {
            pArr->len = length;
            pArr->cnt = 0;
        }
        return;
       }
       ​
       bool is_empty(struct Arr *pArr)
       {
        return 0 == pArr->cnt ? true:false;
       }
       ​
       bool is_full(struct Arr * pArr)
       {
        return pArr->cnt == pArr->len ? true:false;
       }
       ​
       void show_arr(struct Arr * pArr)
       {
        int i;
        if ( is_empty(pArr) )
        {
            printf("数组为空");
        }
        else
        {
            for (i = 0; i<pArr->cnt; i++)
            {
                printf("%d ",pArr->pBase[i]);
            }
            printf("\n");
        }
        return;
       }
       ​
       void append_arr(struct Arr * pArr,int val)
       {
        if( is_full(pArr) )
        {
            printf("数组已满,不能再追加!\n");
        }
        else
        {
            pArr->pBase[pArr->cnt]=val;
            ++ pArr->cnt;
        }
        return;
       }
       ​
       void insert_arr(struct Arr * pArr,int pos,int val)
       {
        if ( is_full(pArr) )
        {
            printf("数组已满,无法插入!\n");
            return;
        }
        if ( pos < 1 || pos > pArr->cnt)
        {
            printf("指定位置有误,无法插入!\n");
            return;
        }
       ​
        for (int i=pArr->cnt; i<pos-1; --i)
        {
            pArr->pBase[i+1]=pArr->pBase[i];
        }
        pArr->pBase[pos-1]=val;
        pArr->cnt ++;
        return;
       }
       ​
       void inversion_arr (struct Arr * pArr)
       {
        int i;
        int j = pArr-> cnt -1;
        int t;
       ​
        while (i < j)
        {
            t = pArr->pBase[i];
            pArr->pBase[i] = pArr->pBase[j];
            pArr->pBase[j] = t;
            ++i;
            --j;
        }
        return;
       }
       ​
       void sort_arr (struct Arr * pArr)
       {
        int i,j,t;
       ​
        for (i=0; i<pArr->cnt-1; ++i)
        {
            for (j=i+1; j<pArr->cnt; ++j)
            {
                if (pArr->pBase[i]>pArr->pBase[j])
                {
                    t = pArr->pBase[j];
                    pArr->pBase[j]=pArr->pBase[i];
                    pArr->pBase[i]=t;
                }
            }
        }
        return;
       }
  • 离散结构【链表】

    • 定义:n个节点离散分配,彼此通过指针相连,每个节点只有一个前驱节点同时每个节点只有一个后续节点,首节点没有前驱节点,尾节点没有后续节点。

      专业术语

      • 首节点:存放第一个有效数据的节点

      • 尾节点:存放最后一个有效数据的节点

      • 头结点:位于首节点之前的一个节点,头结点并不存放有效的数据,加头结点的目的主要是为了方便对链表的操作

      • 头指针:指向头结点的指针变量

      • 尾指针:指向尾节点的指针变量

    • 确定一个链表需要几个参数:只需要一个头指针参数,因为我们通过头指针可以推算出链表的其他所有信息

      例子

      # include <stdio.h>
       ​
       typedef struct Nodeypedef struct Node
       {
        int data;//数据域int data;//数据域
        struct Node * PNext;//指针域struct Node * PNext;//指针域
       } NODE, *PNODE;//NODE等价于struct Node,PNODE等价于struct Node * NODE, *PNODE;//NODE等价于struct Node,PNODE等价于struct Node *
       ​
       int main(void)nt main(void)
       {   
        return 0;return 0;
       }
    • 分类

      • 单链表:每一个节点只有一个指针域

      • 双链表:每一个节点有两个指针域

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

      • 非循环链表:不能通过任何一个节点找到其他所有的节点

        循环链表属于双链表的一种特殊形式,即循环链表是双链表的一个子集。

    • 优缺点

      • 优点:空间没有限制,插入和删除元素很快

      • 缺点:存取速度很慢

    • 算法

      算法:

      • 侠义的算法是与数据的存储方式密切相关

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

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

      • 遍历

      • 查找

      • 清空

      • 销毁

      • 求长度

      • 排序

      • 删除节点

      • 插入节点

      代码实现

      # include <stdio.h>
       # include <malloc.h>
       # include <stdbool.h>
       ​
       typedef struct Node
       {
        int data;//数据域
        struct Node * pNext;//指针域
       } NODE,*PNODE; //NODE相当于struct Node,*PNODE相当于struct Node *
       ​
       //创建链表
       PNODE create_list(void);
       //遍历链表
       void traverse_list(PNODE pHead);
       //判断是否为空
       bool is_empty(PNODE pHead);
       //返回链表长度
       int length_list(PNODE pHead);
       //在指定节点处插入某个元素
       bool insert_list(PNODE,int,int);
       //删除指定位置的元素
       bool delete_list(PNODE,int,int *);
       //对链表排序
       void sort_list(PNODE pHead);
       ​
       int main(void)
       {
        PNODE pHead = NULL;//定义头结点指针
        pHead = create_list();
        return 0;
       }
       ​
       PNODE create_list(void)
       {
        int len;//链表成员个数,由用户输入
        int i;
        int val;//链表成员值,由用户输入
       ​
        PNODE pHead = (PNODE)malloc(sizeof(NODE));//定义头结点指针
        if (NULL == pHead)
        {
            printf("分配内存失败,程序结束");
            exit(-1);
        }
       ​
        printf("请输入链表长度,len=");
        scanf("%d",&len);
       ​
        PNODE pTail = pHead;
        pTail->pNext = NULL;
       ​
        for(i=0; i < len; i++)
        {
            PNODE pNew = (PNODE)malloc(sizeof(NODE));
            if (NULL == pNew)
            {
                printf("分配内存失败,程序结束");
                exit(-1);
            }
            printf("请输入要插入链表的值,val=");
            scanf("%d",&val);
       ​
            pNew->data = val;
            pTail->pNext = pNew;
            pNew->pNext = NULL;
            pTail = pNew;
        }
        return pHead;
       }
       ​
       void traverse_list(PNODE pHead)
       {
        PNODE p = pHead->pNext;
        while (NULL != p)
        {
            printf("%d\t",p->data);
            p = p->pNext;
        }
        printf("\n");
        return;
       }
       ​
       bool is_empty(PNODE pHead)
       {
        if (NULL == pHead->pNext)
            return true;
        else
            return false;
       }
       ​
       int length_list(PNODE pHead)
       {
        int len = 0;
        PNODE p = pHead->pNext;
        while(NULL != p)
        {
            ++len;
            p = p->pNext;
        }
        return len;
       }
       ​
       bool insert_list(PNODE pHead,int pos,int val)
       {
        int i;
        PNODE p = pHead;
       //循环到p指向pos-1的位置
        while( NULL != p && i<pos-1)
        {
            p = p->pNext;
            ++i;
        }
        if (NULL == p || i > pos -1)
        {
            return false;
        }
       //插入的数申请内存
        PNODE pNew = (PNODE)malloc(sizeof(NODE));
        if (NULL == pNew)
        {
            printf("分配内存失败,程序终止!\n");
            exit(-1);
        }
        pNew->data = val;
        PNODE q = p->pNext;
        p->pNext = pNew;
        pNew->pNext = q;
        return ture;
       }
       ​
       bool delete_list(PNODE pHead,int pos,int *pVal)
       {
        int i;
        PNODE p = pHead;
       //循环到p指向pos-1的位置
        while( NULL != p->pNext && i<pos-1)
        {
            p = p->pNext;
            ++i;
        }
       if (NULL == p->pNext || i > pos -1) 
       { 
           return false; 
       } 
      PNODE q = p->pNext; 
      *pVal = p->data; 
      p->pNext=q->pNext; 
      free(q); 
      q=NULL; 
      return true; 
      } 
      ​ 
      void sort_list(PNODE pHead) 
      { 
      int i,j,t; 
      PNODE p,q; 
      int len = length_list(pHead); 
      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; 
               } 
           } 
       } 
      return; 
      }
  • 线性结构的两种常见应用之一:栈

    • 定义:一种可以实现“先进后出”的存储结构

    • 分类:静态栈;动态栈。

    • 算法:压栈;出栈

    • 应用:函数调用;中断;表达式求值;内存分配;缓冲处理;迷宫

    示例代码

    # include <stdio.h>
     # include <malloc.h>
     # include <stdlib.h>
     ​
     typedef struct Node
     {
      int data;
      struct Node * pNext;
     } NODE,* PNODE;
     ​
     typedef struct Stack
     {
      PNODE pTop;
      PNODE pBottom;
     } STACK,* PSTACK;
     ​
     //初始化
     void init_stack(PSTACK);
     //压栈
     void push_stack(PSTACK,int);
     //出栈
     int pop_stack(PSTACK);
     //遍历栈
     void traverse_stack(PSTACK);
     //判断栈是否为空
     bool is_empty(PSTACK);
     //清空栈
     void clear(PSTACK);
     ​
     int main(void)
     {
      int val;//出栈的值
      STACK s;
      init_stack(&s);//目的是造出一个空栈
      push_stack(&s,1);//压栈
      push_stack(&s,2);
      push_stack(&s,3);
      push_stack(&s,4);
      push_stack(&s,5);
      traverse_stack(&s);
      pop_stack(&s,&val);
      return 0;
     }
     ​
     void init_stack(PSTACK pS)
     {
      pS->pTop=(PNODE)malloc(sizeof(NODE));
      if (NULL == pS->pTop)
      {
          printf("分配内存失败,程序终止!\n");
          exit(-1);
      }
      else
      {
          pS->pBottom = pS->pTop;
          pS->pTop->pNext = NULL;
      }
      return;
     }
     ​
     void push_stack(PSTACK pS,int val)
     {
      PNODE pNew = (PNODE)malloc(sizeof(NODE));
      if (NULL == pNew)
      {
          printf("分配内存失败,程序终止!\n");
          exit(-1);
      }
      pNew->data = val;
      pNew->pNext = pS->pTop;
      pS->pTop = pNew;
     ​
      return;
     }
     ​
     void traverse_stack(PSTACK pS)
     {
     ​
      PNODE p = pS->pTop;
      while (p != ps->pBottom)
      {
          printf("%d\t",p->data);
          p = p->pNext;
      }
      printf("\n");
      return;
     }
     ​
     bool is_empty(PSTACK pS)
     {
      if (pS->pTop == pS->pBottom)
          return true;
      else
          return false;
     }
     ​
     bool pop_stack(PSTACK pS,*pVal)
     {
      if (is_empty(pS))
      {
          return false;
      }
      else
      {
          PNODE p = pS->pTop;
          *pVal = p->data;
          pS->pTop = p->pNext;
          free(p);
          p = NULL;
          return true;
      }
     }
     ​
     void clear(PSTACK pS)
     {
      while (!is_empty(pS))
      {
          PNODE p = pS->pTop;
          pS->pTop = p->pNext;
          free(p);
          p=NULL;
      }
      return;
     }
  • 线性结构的两种常见应用之二:队列

    • 定义:一种可以实现“先进先出”的存储结构

    • 分类

      • 链式队列——用链表实现

      • 静态队列——用数组实现

        1. 静态队列通常都必须是循环队列

        2. 循环队列的讲解

          • 静态队列为什么必须是循环队列

          • 循环队列需要几个参数来确定

            需要两个参数来确定队列,front|rear;

          • 循环队列各个参数的含义

            1. 队列初始化:front和rear的值都是0

            2. 队列非空:front代表的是队列的第一个元素,rear代表的是队列的最后一个有效元素的下一个位置

            3. 队列空:front和rear的值相等,但不一定是0

          • 循环队列入队伪算法

            1. 将值存入rear所代表的位置

            2. rear = (rear + 1) % 数组的长度

          • 循环队列出队伪算法

            1. front = (front + 1) % 数组的长度

          • 如何判断循环队列是否为空

            if (front == rear)

          • 如何判断循环队列是否已满

            两种方式:

            1. if ((rear + 1 % 数组的长度) == front)

            2. 元素个数=数组长度-1

        3. 队列的具体应用:所有和时间有关的操作都与队列有关

          样例代码

          # include <stdio.h>
          # include <malloc.h>
          # include <stdboo.h>
          

          typedef struct Queue
          {
          int * pBase;//循环数组的首地址
          int front;//队头
          int rear;//队尾
          } QUEUE;

          //初始化队列
          void init(QUEUE *);
          //判断队列是否为空
          bool is_empty(QUEUE *);
          //队列是否已满
          bool is_full(QUEUE *);
          //插入队列 入队
          bool en_queue(QUEUE *,int val);
          //遍历队列
          void traverse_queue(QUEUE *);
          //出队
          bool out_queue(QUEUE *,int *);

          int main(void)
          {
          int val;//出队的元素
          QUEUE Q;
          init(&Q);
          en_queue(&Q,1);
          en_queue(&Q,2);
          en_queue(&Q,3);
          en_queue(&Q,4);
          en_queue(&Q,5);
          en_queue(&Q,6);
          traverse_queue(&Q);
          if(out_queue(&Q,&val))
          {
          printf("The elem out queue is %d .",val);
          }
          return 0;
          }

          void init(QUEUE * pQ)
          {
          pQ->pBase = (int *)malloc(sizeof(int) * 6);
          pQ->front = 0;
          pQ->rear = 0;
          }

          bool is_empty(QUEUE * pQ)
          {
          return pQ->front == pQ->rear? true:false;
          }

          bool is_full(QUEUE * pQ)
          {
          return pQ->front == (pQ->rear + 1) % 6 ? true:false;
          }

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

          }

          //遍历队列
          void traverse_queue(QUEUE * pQ)
          {
          if(is_empty(pQ))
          {
          printf("The queue is empty!");
          return;
          }
          else
          {
          int val;
          while(pQ->front != pQ->rear)
          {
          printf("%d ",pQ->pBase[pQ->front]);
          pQ->front = (pQ->front + 1) % 6;
          }
          printf("\n");
          return;
          }
          }

          //出队
          bool out_queue(QUEUE * pQ,int *pVal)
          {
          if(is_empty(pQ))
          {
          printf("The queue is empty.");
          return false;
          }
          else
          {
          *pVal = pQ->pBase[pQ->front];
          pQ->front = (pQ->front + 1) % 6;
          return true;
          }
          }

主题:递归

定义:一个函数自己直接或间接调用自己

递归满足三个条件

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

  2. 该函数所处理的数据规模必须在递减

  3. 这个转化必须是可解的

循环和递归

  • 递归

    • 易于理解

    • 速度慢

    • 存储空间大

  • 循环

    • 不易理解

    • 速度快

    • 存储空间小

函数的调用:

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

    1. 将所有的实际参数、返回地址等信息传递给被调函数保存。

    2. 为被调函数的局部变量(也包括行参)分配存储空间。

    3. 将控制转移到被调函数的入口。

  • 从被调函数返回函数之前,系统也要完成三件事:

    1. 保存被调函数的返回结果。

    2. 释放被调函数所占的存储空间。

    3. 依照被调函数保存的返回地址将控制转移到调用函数。

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

  • A函数调用A函数和A函数调用B函数在计算机看来是没有任何区别的,只不过用我们日常的思维方式理解比较怪异而已!

  1. 1+2+3+...+100的和

    代码实现

    # include <stdio.h>
    

    int sum(int);

    int main(void)
    {
    int n = 100;
    printf("%d\n",sum(n));
    return 0;
    }

    int sum(int n)
    {
    if(1 == n)
    return 1;
    else
    return n + sum(n-1);
    }

  2. 求阶乘

    代码实现

    # include <stdio.h>
    

    long multi(int);

    int main(void)
    {
    printf("%ld\n",multi(6));
    return 0;
    }

    long multi(int n)
    {
    if (1 == n)
    return 1;
    else
    return n * multi(n - 1);
    }

  3. 汉诺塔

    伪代码:

    如果只有一个盘子:

    直接将盘子从A柱子移到C柱子

    否则:

    先将A柱子上的n-1个盘子借助C柱子移到B柱子

    再直接将盘子从A柱子移到C柱子

    最后再将B柱子上的盘子借助A柱子移到C柱子上

    代码实现样例:

    # include <stdio.h>
    

    void hanoi(int,char,char,char);

    int main(void)
    {
    //柱子编号
    char ch1 = 'A';
    char ch2 = 'B';
    char ch3 = 'C';
    //盘子数量
    int n;
    printf("请输入盘子的数量");
    scanf("%d",&n);
    hanoi(n,'A','B','C');

    return 0;
    }

    void hanoi(int n,char 'A',char 'B',char 'C')
    {
    if (1 == n)
    printf("编号为%d的盘子:%c-->%c\n",n,'A',"C");
    else
    {
    hanoi(n-1,A,C,B);
    printf("编号为%d的盘子:%c-->%c\n",n,'A',"C");
    hanoi(n-1,B,A,C);
    }
    }

  4. 走迷宫

    A*(A-Star)算法实现

递归的应用
  • 树和森林就是以递归的方式定义的

  • 树和图的很多算法

  • 很多数学公式:例如斐波拉契数列

非线性结构

树的定义
  • 专业定义:

    1. 有且只有一个称为根的节点

    2. 有若干个互不相交的子树,这些子树本身也是一颗树

  • 通俗定义:

    1. 树是由节点和边组成

    2. 每个节点只有一个父节点但可以有多个子节点

    3. 但有一个节点例外,该节点没有父节点,此节点称为根节点

  • 专业术语:

    • 节点,父节点,子节点

    • 子孙,堂兄弟

    • 深度:从根节点到最底层节点的层数称之为深度,根节点是第一层

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

    • 非终端节点:实际就是非叶子节点

    • 度:子节点的个数

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

  • 二叉树:任意一个节点的子节点个数最多两个,且子节点的位置不可更改

    • 分类

      • 一般二叉树

      • 满二叉树:在不增加树层数的前提下,无法再多添加一个节点的二叉树就是满二叉树

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

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

树的存储
  • 二叉树的存储

    • 连续存储【完全二叉树】

      优点:查找某个节点的父节点和子节点(也包括判断有没有子节点)

      缺点:耗用内存空间过大

    • 链式存储

  • 一般树的存储

    • 双亲表示法:求父节点方便

    • 孩子表示法:求子节点方便

    • 双亲孩子表示法:求父节点和子节点都很方便

    • 二叉树表示法:把一个普通树转化成二叉树来存储

      具体转换方法:

      设法保证任意一个节点的左指针域指向它的第一个孩子,右指针域指向它的兄弟,只要满足此条件,就可以把一个普通树转化为二叉树。

      一个普通树转化成的二叉树一定没有右子树

  • 森林的存储

    先把森林转化为二叉树,再存储二叉树:

    将相邻的父节点依次作为节点的右子树再对各父节点进行转化

树的操作
  • 遍历

    • 先序遍历【先访问根节点】

      1. 先访问根节点

      2. 再先序访问左子树

      3. 最后先序访问右子树

        例子:

                A
               / \
              B   C
             / \   \
            D   E    F
           / \   \  / \
          G   H   I J  k

        先序遍历结果:ABDGHEICFJK

    • 中序遍历【中间访问根节点】

      1. 中序遍历左子树

      2. 再访问根节点

      3. 再中序遍历右子树

        例子:

                A
               / \
              B   C
             / \   \
            D   E    F
           / \   \  / \
          G   H   I J  k

        中序遍历结果:GDHBEIACJFK

    • 后续遍历【最后访问根节点】

      1. 先中序遍历左子树

      2. 再中序遍历右子树

      3. 最后遍历根节点

        例子:

                A
               / \
              B   C
             / \   \
            D   E    F
           / \   \  / \
          G   H   I J  k

        后序遍历结果:GHDIEBJKFCA

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

    通过先序和中序或者中序和后序我们可以还原出原始二叉树,但是通过先序和后序是无法还原出原始二叉树;

    换句话,只有通过先序和中序或者中序和后序,我们才可以唯一的确定一个二叉树。

    例子1:

    先序:ABCDEFGH
    中序:BDCEAFHG
    求后序?
    分析:按照先序的定义,A为最外层根节点,按照中序的定义和前面的结论可知BDCE为A节点的左子树节点,FHG为A节点的右子树,再依次按照两个遍历定义可以推出原始二叉树为:
         A
        / \
       B   F
         \  \
          C  G
         / \ /
        D  E H
    那么此二叉树的后序为:DECBHGFA

    例子2:

    先序:ABDGHCEFI
    中序:GDHBAECIF
    求后序?
    分析:按照先序的定义得到A为最外层根节点,再根据中序结果可知GDHB为A的左子树,ECIF为A的右子树;B先出现在先序结果中可知B为左子树的根节点,再根据中序结果知B节点没有右子树,GDH均为B节点的左子树,再根据先序结果D先出现,知D为B左子树的根节点,再根据先序发现G在D的后面且中序中G在D的前面得出G为D左子树的根节点,那么D的右子树的根节点就是H了,依次类推A的右子树,得出原始二叉树为:
       A
      / \
     B   C
    /   / \
    D   E   F
    / \     /
    G   H   I
    那么此二叉树的后序为:GHDBEIFCA

    例子3:

    中序:BDCEAFHG
    后序:DECBHGFA
    求先序?
    分析:由后序结果知A为最外层根节点,再根据中序结果知BDCE为A节点的左子树,FHG为A的右子树;A的左子树中B最靠近A那么根据后序规则得出B为左子树的根节点,再根据中序结果B在结果的第一位,由中序规则知B没有左子树,DCE均为B的右子树,在DCE中后序结果C最靠近B,得出C为B的右子树的根节点,再依据中序结果知C前面是D后面是E得出D为C的左子树,E为C的右子树,同理可以推出A的右子树,得出原始二叉树为:
         A
        / \
       B   F
        \   \
         C   G
        / \  /
       D   E H
    那么此二叉树的先序为:ABCDEFGH
  • 二叉树的代码实现举例

    # include <stdio.h>
    # include <malloc.h>
    

    typedef struct BinaryTree
    {
    char data;//节点值 数据域
    struct BinaryTree * pLchild;//左子树 左指针域
    struct BinaryTree * pRchild;//右子树 右指针域
    } BINARYTREE,* PBINARYTREE;

    //创建一个静态二叉树
    PBINARYTREE create_binary_tree(void);
    //先序遍历
    void pre_traversal(PBINARYTREE);
    //中序遍历
    void in_traversal(PBINARYTREE);
    //后序遍历
    void post_traversal(PBINARYTREE);

    int main(void)
    {
    PBINARYTREE pT = NULL;
    pT = create_binary_tree();
    printf("先序遍历结果为:");
    pre_traversal(pT);
    printf("\n中序遍历结果为:");
    in_traversal(pT);
    printf("\n后序遍历结果为:");
    post_traversal(pT);
    return 0;
    }

    /**创建以下结果的二叉树,此二叉树结构是上面二叉树遍历的例子,可对比结果是否正确
    A
    /
    B C
    / \
    D E F
    / \ \ /
    G H I J k
    */
    PBINARYTREE create_binary_tree(void)
    {
    //为每个二叉树节点申请内存空间
    PBINARYTREE pA = (PBINARYTREE)malloc(sizeof(BINARYTREE));
    PBINARYTREE pB = (PBINARYTREE)malloc(sizeof(BINARYTREE));
    PBINARYTREE pC = (PBINARYTREE)malloc(sizeof(BINARYTREE));
    PBINARYTREE pD = (PBINARYTREE)malloc(sizeof(BINARYTREE));
    PBINARYTREE pE = (PBINARYTREE)malloc(sizeof(BINARYTREE));
    PBINARYTREE pF = (PBINARYTREE)malloc(sizeof(BINARYTREE));
    PBINARYTREE pG = (PBINARYTREE)malloc(sizeof(BINARYTREE));
    PBINARYTREE pH = (PBINARYTREE)malloc(sizeof(BINARYTREE));
    PBINARYTREE pI = (PBINARYTREE)malloc(sizeof(BINARYTREE));
    PBINARYTREE pJ = (PBINARYTREE)malloc(sizeof(BINARYTREE));
    PBINARYTREE pK = (PBINARYTREE)malloc(sizeof(BINARYTREE));
    //给每个二叉树节点的数据域赋值
    pA->data = 'A';
    pB->data = 'B';
    pC->data = 'C';
    pD->data = 'D';
    pE->data = 'E';
    pF->data = 'F';
    pG->data = 'G';
    pH->data = 'H';
    pI->data = 'I';
    pJ->data = 'J';
    pK->data = 'K';
    //给每个二叉树节点的左右指针域赋值
    pA->pLchild = pB;
    pA->pRchild = pC;
    pB->pLchild = pD;
    pB->pRchild = pE;
    pC->pLchild = NULL;
    pC->pRchild = pF;
    pD->pLchild = pG;
    pD->pRchild = pH;
    pE->pLchild = NULL;
    pE->pRchild = pI;
    pF->pLchild = pJ;
    pF->pRchild = pK;
    pG->pLchild = pG->pRchild = NULL;
    pH->pLchild = pH->pRchild = NULL;
    pI->pLchild = pI->pRchild = NULL;
    pJ->pLchild = pJ->pRchild = NULL;
    pK->pLchild = pK->pRchild = NULL;

    return pA;
    

    }

    void pre_traversal(PBINARYTREE pT)
    {
    if(NULL != pT)
    {
    printf("%c\t",pT->data);
    if(NULL != pT->pLchild)
    pre_traversal(pT->pLchild);
    if(NULL != pT->pRchild)
    pre_traversal(pT->pRchild);
    }
    return;
    }

    void in_traversal(PBINARYTREE pT)
    {
    if(NULL != pT)
    {
    if(NULL != pT->pLchild)
    in_traversal(pT->pLchild);
    printf("%c\t",pT->data);
    if(NULL != pT->pRchild)
    in_traversal(pT->pRchild);
    }
    return;
    }

    void post_traversal(PBINARYTREE pT)
    {
    if(NULL != pT)
    {
    if(NULL != pT->pLchild)
    post_traversal(pT->pLchild);
    if(NULL != pT->pRchild)
    post_traversal(pT->pRchild);
    printf("%c\t",pT->data);
    }
    return;
    }

树的应用
  • 树是数据库中数据组织的一种重要形式

  • 操作系统子父进程的关系本身就是一个树

  • 面向对象语言中类的继承关系

  • 赫夫曼树

视频中没有具体讲解

查找

  1. 折半查找

    折半查找又叫二分查找,通常叫二分查找比较普遍。它对要查找的序列有两个要求,一是该序列必须是有序的(即该序列中的所有元素都是按照大小关系排好序的,升序和降序都可以,本文假设是升序排列的),二是该序列必须是顺序存储的。

    示例代码:

    # include <stdio.h>
    

    int binary_search(int array[],int len,int target);

    int main()
    {
    int len =8;
    int array[] = {2,3,4,5,7,9,20,31};
    int target = 20;
    int pos =binary_search(array, len, target);
    if(pos==-1)
    printf("未找到目标值");
    else
    printf("找到目标值,位置为:%d",pos+1);
    return 0;
    }

    int binary_search(int array[],int len,int target)
    {
    int low=0;
    int high=len;
    while(low<=high)
    {
    int mid=low+(high-low)/2;//还是溢出问题
    if(array[mid]>target)
    high=mid-1;
    else if(array[mid]<target)
    low=mid+1;
    else
    return mid;
    }
    return-1;
    }

排序

关于排序算法,推荐去中文维基百科上面查看,有动态排序过程,有助于理解算法本质!

排序算法

对于无法访问中文维基的同学可以访问下面的博主分享:

十大经典排序算法(动图演示) - 一像素 - 博客园 (cnblogs.com)

注:这个博主的代码时js编写的,和c类似,各位同学可以翻译成c实现。

  1. 冒泡排序

    //简单冒泡排序举例【升序】
    

    include <stdio.h>

    void bubble_sort(int *,int);

    int main(void)
    {
    int i;
    int len = 6;
    int arr[len] = {2,10,8,5,3,1};

    bubble_sort(arr,len);
    for(i = 0; i &lt; 6; i++)
        printf("%d ",arr[i]);
    printf("\n");
    return 0;
    

    }

    void bubble_sort(int * arr,int len)
    {
    int i,j,t;
    for(i = 0; i < len; i++)
    {
    for(j = i+1; j < len-1; j++)
    {
    if(arr[i] > arr[j])
    {
    t = arr[i];
    arr[i] = arr[j];
    arr[j] = t;
    }
    }
    }
    }

  2. 插入排序

    //直接插入排序【升序】
    

    include <stdio.h>

    void insertion_sort(int *,int);

    int main(void)
    {
    int i;
    int len = 6;//数组长度
    int arr[len] = {2,10,8,5,3,1};
    insertion_sort(arr,len);
    for(i = 0; i < 6; i++)
    printf("%d ",arr[i]);
    printf("\n");
    }

    void insertion_sort(int * arr,int len)
    {
    int i,j,t;
    for (i=1; i<len; i++)
    {
    if(arr[i]<arr[i-1])
    {
    t = arr[i];
    j = i - 1;
    for(j; j>=0 && arr[j]>t; j--)
    {
    arr[j+1] = arr[j];
    }
    a[j+1] = t;
    }
    }
    }

  3. 选择排序

    //直接选择排序【升序】
    

    include <stdio.h>

    void selection_sort(int *,int);

    int main(void)
    {
    int i;
    int len = 6;//数组长度
    int arr[6] = {2,10,8,5,3,1};
    selection_sort(arr,len);
    for(i = 0;i < 6;i++)
    printf("%d ",arr[i]);
    printf("\n");
    return 0;
    }

    void selection_sort(int * arr,int len)
    {
    int i,j,t,min;
    for(i=0; i<len-1; i++)
    {
    for(min=i,j=i+1; j<len; j++)
    {
    if(arr[min] > arr[j])
    min = j;
    }
    if(min != j)
    {
    t = arr[i];
    arr[i] = arr[min];
    arr[min] = t;
    }
    }
    }

  4. 快速排序

    //快速排序【升序】
    # include <stdio.h>
    

    void quick_sort(int *,int,int);
    int find_pos(int *,int,int);

    int main(void)
    {
    int i,len,low,high;
    low = 0;
    high = 6;
    int arr[7] = {13,2,1,3,8,5,1};
    quick_sort(arr,low,high);//low表示起始位置,high表示结束位置
    for(i = 0; i < 7; i++)
    printf("%d ",arr[i]);
    printf("\n");
    return 0;
    }

    void quick_sort(int * arr,int low,int high)
    {
    int pos;
    if (low < high)
    {
    pos = find_pos(arr,low,high);
    quick_sort(arr,low,pos-1);
    quick_sort(arr,pos+1,high);
    }
    }

    int find_pos(int * arr,int low,int high)
    {
    int val = arr[low];
    while(low < high)
    {
    while(low < high && arr[high] >= val)
    --high;
    arr[low] = arr[high];
    while(low < high && arr[low] <= val)
    ++low;
    arr[high] = arr[low];
    }
    arr[low] = val;
    return high;
    }

  5. 归并排序

    摘自中文维基百科-归并排序

    # include <stdio.h>
    

    void merge_sort(int arr[], const int len);
    void merge_sort_recursive(int arr[], int reg[], int start, int end);

    int main(void)
    {
    int i;
    int arr[7] = {13,2,1,3,8,5,1};
    merge_sort(arr,7);
    for(i = 0; i < 7; i++)
    printf("%d ",arr[i]);
    printf("\n");
    return 0;
    }

    void merge_sort_recursive(int arr[], int reg[], int start, int end)
    {
    if (start >= end)
    return;
    int len = end - start, mid = (len >> 1) + start;
    int start1 = start, end1 = mid;
    int start2 = mid + 1, end2 = end;
    merge_sort_recursive(arr, reg, start1, end1);
    merge_sort_recursive(arr, reg, start2, end2);
    int k = start;
    while (start1 <= end1 && start2 <= end2)
    reg[k++] = arr[start1] < arr[start2] ? arr[start1++] : arr[start2++];
    while (start1 <= end1)
    reg[k++] = arr[start1++];
    while (start2 <= end2)
    reg[k++] = arr[start2++];
    for (k = start; k <= end; k++)
    arr[k] = reg[k];
    }

    void merge_sort(int arr[], const int len)
    {
    int reg[len];
    merge_sort_recursive(arr, reg, 0, len - 1);
    }

总结

坚持就是胜利!

 下载链接:

链接:https://pan.baidu.com/s/1DtjDM9bcBy3Gssp3Hdzrvg?pwd=2npy
提取码:2npy

posted @ 2019-04-25 15:18  进击的小陀螺  阅读(36904)  评论(7编辑  收藏  举报