文章目录
数据结构
大部分笔记属于b战郝斌老师的数据结构课程,因课程课时原因 ,只有到树的内容,供整理后发出一起讨论与学习 |
---|
-
数据结构是专门研究数据存储的问题
-
数据结构 = 个体的存储 + 个体的关系的存储、
-
算法是对存储数据的操作
线性结构[把所有的节点用一根直线串起来]
连续存储【数组】
-
什么叫数组
-
元素类型相同,大小相等
-
数组的优缺点
- 优点
- 存取速度快
- 缺点
- 事先必须知道数组的长度
- 插入删除元素很慢
- 空间通常是有限的
- 需要大块连续的内存块
- 插入删除元素的效率很
- 优点
-
下面看一段连续存储增删改除的代码
-
#include<stdio.h> #include<stdlib.h> struct arr//定义了一个数据类型 该数据类型的名字叫struct arr,该数据类型含有3个成员 //但还没有定义变量 { int* pbase;//储存的是数组第一个元素的地址 int len;//数组所能容纳的最大元素的个数 int cnt;//当前数组有效的个数 }; void init_arr(struct arr *arry1, int length); bool appen_arr(struct arr* parr, int val); bool insert_arr(struct arr* parr,int pos, int val); bool delete_arr(struct arr* parr,int pos, int* pval); int get(); bool is_empty(struct arr* parr); bool is_full(struct arr* parr); void sort_arr(struct arr* parr); void show_arr(struct arr* parr); void inversion_arr(struct arr* parr); int main() { struct arr arry;//还没有指向一个有效的数组 该结构体里面的变量存放的是垃圾值 所以要先完成数组的初始化 //printf("%d\n",arry.len); int val; init_arr(&arry,6);//这里只能传地址才能改变结构体变量的值 地址占8个字节 show_arr(&arry); appen_arr(&arry, 1); appen_arr(&arry, 2); appen_arr(&arry, 3); appen_arr(&arry, 4); appen_arr(&arry, 5); appen_arr(&arry, 6); /*if (appen_arr(&arry, 7)) { printf("追加成功"); } else { printf("追加失败"); }*/ printf("起始数据为:\n"); show_arr(&arry); //if (delete_arr(&arry, 4, &val)) //{ // printf("删除成功:"); // printf("删除的元素是:%d\n",val); // show_arr(&arry); //} if (insert_arr(&arry, 6, 10)) { printf("插入成功:"); printf("插入的元素是:10\n"); show_arr(&arry); } inversion_arr(&arry); printf("倒置后的数据为:\n"); show_arr(&arry); sort_arr(&arry); printf("排序后的数据为:\n"); show_arr(&arry); //printf("%d\n", arry.len); return 0; } void show_arr(struct arr* parr) { if(is_empty(parr))//因为这里应该是填写我们那个结构体变量的地址 根据传参 因为parr=&arry 所以这里应该填parr //如果为&parr 那就变成了我们定义的这个函数的结构体指针变量的地址 //我们也可以把地址值打印出来查看 printf("数组输入为空\n"); else { for (int i = 0; i < parr->cnt; i++) { printf("%4d",parr->pbase[i]);//直接输出 // printf("i:%d\n",i); } printf("\n"); } } bool insert_arr(struct arr* parr, int pos, int val)//插入的数字 { if (is_full(parr))return false;//如果满了 该程序不会执行 if (pos<1 || pos>parr->cnt+1) return false; else { for (int i = parr->cnt - 1; i >= pos - 1; i--) { parr->pbase[i + 1] = parr->pbase[i];//写i+1,i的目的就是i+1=cnt 我们从背后往前面互换各个元素 } parr->pbase[pos - 1] = val; parr->cnt ++;//插入了一个元素 让元素有效长度加一 如果想满了也插入这个元素 可以把满了的判断 与这里注释掉 return true; } } bool appen_arr(struct arr* parr,int val) { if (is_full(parr)) { return false; } else { parr->pbase[parr->cnt] = val;//返回一个有效值 (parr->cnt)++; return true; } } bool is_full(struct arr* parr) { if (parr->cnt == parr->len) return true; else return false; } bool delete_arr(struct arr* parr, int pos, int *pval) { if (is_empty(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; } bool is_empty(struct arr* parr) { if (parr->cnt == 0) return true; else return false; } void inversion_arr(struct arr* parr) { int j = parr->cnt - 1;//往背后的最后一个有效数字与第一个有效数字置换 int temp; for (int i = 0; i < j; i++) { temp = parr->pbase[i]; parr->pbase[i] = parr->pbase[j]; parr->pbase[j] = temp; j--; } } void sort_arr(struct arr* parr) { int temp; for (int i = 0; i < parr->cnt; i++) { for(int 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; } } } } void init_arr(struct arr *arry1,int length) { /*int a[6] = {1,2,3,4,5,6};*/ //元素要通过追加和插入放进去 arry1->pbase = (int*)malloc(sizeof(int)*length); //这个相当于这个指针变量所指向的那个变量(arry)的pbase这个成员 if (arry1->pbase == NULL) { printf("dynamic allocation error"); exit(-1);//表示终止整个程序 } else { arry1->len = length; arry1->cnt = 0; } return; }
离散存储【链表】
-
定义:
-
n个节点离散分配
-
彼此通哟指针相连
-
每个节点只有一个前驱节点,每个节点只有一个后续节点
-
首节点没有前驱节点 尾节点没有后续节点
-
专业术语
- 首节点
- 第一个有效节点
- 尾节点
- 最后一个有效节点
- 头节点
- 头节点的数据类型和首节点的数据类型是一样的
- 头结点的数据类型就和首节点一样,没有存放有效的数据,在首节点之前,存放一个没有实际含义的节点,处在最前面的节点,主要是方便对链表的操作
- 头节点里面没有存放整个节点个数
- 头指针
- 指向的头节点的指针变量
- 尾指针
- 指向尾节点的指针变量
- 首节点
-
(假设我们要确定一个函数 这个函数要可以调用多种链表,因为头结点存储的内存可以非常大,也可能非常小,所以每个链表所存储的内存是不一致的 如果我们在这个函数的参数中传递的是头结点的话,那么因为内存占用差别的问题会变得非常麻烦,也会找不到形参,所以我们用头指针来传递地址到函数是一个最好的办法 因为我们只需要知道它的地址,就可以知道链表的所有信息)
-
确定一个链表需要几个参数:(或者说如果期望一个函数对链表进行操作,我们至少需要接收链表的哪些信息???)
- 只需要一个参数:头指针
- 因为我们通过头指针可以推算出链表的其他所有信息
-
-
分类
-
链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。 -
每一个节点分为指针域和数据域两个部分组成
- 数据域是结点中存储数据元素的部分
- 指针域是结点中存储数据元素之间的链接信息即下一个节点地址的部分。
-
-
单链表
- 每一个节点只有一个指针域(指针域就是存放下一个结构体(节点)的地址)
-
-
双链表
- 每一个节点有两个指针域
-
-
循环链表
- 能通过任何一个节点找到其他所有的节点
-
非循环链表
-
创建一个非循环列表
-
#include<stdio.h> #include<stdlib.h> typedef struct Node { int date; struct Node* pNext;//存放下一个节点的地址的结构体指针 }NODE,*PNODE;//起一个结构体 结构体指针别名 NODE等价于struct student PNODE等价于struct student* PNODE create_list(void); void traverse_lisy(PNODE phead); bool is_empty(PNODE phead); int lengrh_list(PNODE); bool insert_list(PNODE,int ,int); bool delete_list(PNODE,int, int*); void sort_lisit(PNODE); int main(void) { int val; PNODE pHead = NULL; pHead = create_list();//因为返回的是头指针的地址 所以我们必须用结构体指针变量来接收 traverse_lisy(pHead); if (is_empty(pHead)) printf("链表为空"); else printf("链表不空"); int len=lengrh_list(pHead); printf("len:%d\n",len); sort_lisit(pHead); traverse_lisy(pHead); insert_list(pHead, 3, 88); traverse_lisy(pHead); if (delete_list(pHead, 3, &val)) printf("删除成功:%d\n", val); else printf("删除失败"); traverse_lisy(pHead); free(pHead); return 0; } bool is_empty(PNODE phead) { if (phead->pNext == NULL)//头结点的指针域为空就是空 return true; else return false; } int lengrh_list(PNODE phead) { PNODE p = phead->pNext; int len = 0; while (p != NULL) { len++; p = p->pNext; } return len; } bool insert_list(PNODE phead, int pos, int val) { int i = 0; PNODE p = phead; while (NULL!=p&&i<pos-1) { p = p->pNext; i++; } printf("i:%d\n",i); if (i > pos - 1 || NULL == p) return false; PNODE pnew = (PNODE)malloc(sizeof(NODE)); if (NULL == pnew) { printf("动态内存分配失败!\n"); exit(-1); } pnew->date = val; PNODE q = p->pNext; p->pNext = pnew; pnew->pNext = q; return true; } bool delete_list(PNODE phead, int pos, int *pval) { int i = 0; PNODE p = phead; while (NULL != p->pNext && i < pos - 1)//当循环结束时 p就指向了pos-1这个节点 就是删除的前一个节点 { p = p->pNext; i++; } printf("delete i:%d\n", i); if (i > pos - 1 || NULL == p->pNext) return false; PNODE q = p->pNext;//这个意思相当于跳出while循环后 p又指向了下一个节点 也就是我们要删除的这个pos节点 *pval = q->date;//我们取得它的数据 p->pNext = p->pNext->pNext;//我们让p->pnext指向下一个节点pos后的节点 free(q);//最后释放q的空间 q = NULL;//赋为空 return true; } PNODE create_list(void)//返回一个结构体(节点)的地址 { int len; int val; PNODE phead = (PNODE)malloc(sizeof(NODE));//这里的目的是分配一个不存放任何数据的头节点 if (NULL == phead)//如果开辟内存失败 { printf("开辟内存失败"); exit(-1); } PNODE pTail = phead; pTail->pNext = NULL;//方便指向下一个节点 同时可以让phead指向首节点 printf("请输入您需要生成的链表节点的个数:len="); scanf_s("%d",&len); for (int i = 0; i < len; i++) { printf("请输入第%d个节点的值:",i+1); scanf_s("%d", &val); PNODE pnew= (PNODE)malloc(sizeof(NODE));//循环n次 创建n个节点 每次都定义为pnew这个名字 //这样解决了创建一次定义一个名字的问题 if (NULL == pnew)//如果开辟内存失败 { printf("开辟内存失败"); exit(-1); } pnew->date = val;//先让数据域存放输入节点的值 pTail->pNext = pnew;//让pTail这个节点指向pnew这个节点 pnew->pNext = NULL;//把pnew的指针域赋为空 方便指向下一个节点 pTail = pnew;//让pTail指向pnew 这样就可以使pTail指向下一个节点时 //pnew也指向了下一个节点 因为我们的pnew是一个地址 根据指针的知识 //ptail->next指向下一个节点时 同时就可以让pnew->next指向下一个节点 } return phead;//放回一个头节点 因为通过头结点的地址可以找到我们的链表 } void traverse_lisy(PNODE phead) { PNODE p = phead->pNext;//通过头指针可以找到整个链表 while (p != NULL) { printf("%4d",p->date); p = p->pNext;// 此时p又不为空 再次进入while循环 直到p->next为0为止 } printf("\n"); return; } void sort_lisit(PNODE phead) { int i, j, t; PNODE p, q; int len = lengrh_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->date > q->date) { t = p->date; p->date = q->date; q->date = t; } } } return; }
-
-
-
算法
- 遍历
- 查找
- 清空
- 销毁
- 求长度
- 排序
- 选择排序算法
-
- 冒泡排序算法
-
- 选择排序算法
- 删除节点
+
- 插入节点
- 第2种方法
- 算法 :
- 狭义的算法是与数据的存储方式密切相关
- 广义的算法是与数据的存储方式无关
- 泛型://比如一个结构体指针变量 我们++p 那么它可能内部的实现的方式就输 {p=p->next}
- 利用某种技术达到的效果就是:不同的存储方式,执行的操作是一样的
- 泛型://比如一个结构体指针变量 我们++p 那么它可能内部的实现的方式就输 {p=p->next}
-
-
链表的优缺点
- 优点
- 空间没有限制
- 插入删除速度很快
- 缺点
- 存取速度很慢
- 优点
线性结构的两种常见应用之一 栈
-
栈的操作仅仅能在线性表的一端进行**,所以无论*是入栈还是出栈的操作,时间复杂度都是O(1)**
-
定义
-
一种可以实现“先进后出”的存储结构
-
栈类似于箱子(栈的本质是线性表,可以说他是一种特殊的线性表,因为只能在线性表的一端进行操作(栈顶),不能操作的那一端交栈底。)
-
允许进行插入,删除操作的一端称为栈顶。表的另一端称为栈底。当栈中没有数据元素时,称为空栈。栈的插入操作通常称为进栈或入栈。栈的删除操作通常称为退栈或出栈
-
-
分类
-
静态栈(可以复用顺序表的代码)
- 必须提前确定栈的大小(有限的),并且都是连续的【类似于数组的实现】
-
动态栈(可以复用单链表的代码)
- 可以无限大小(内存足够的情况下),并且是不连续的【类似于单链表的实现】
-
-
算法
-
入栈
-
假设在struct定义了链表的指针域和数据域的数据类型之后,我们在它的基础上,struct定义了一个顶部和底部
-
-
如果我们此时想添加第一个入栈,则它必须指向NULL,由于顶部和底部的指针域都指向了NULL,所以入栈1的指针域只要指向顶部就是指向了NULL
-
其实跟链表的增加节点差不多,顶部就是充当为后来的入栈当一个中间变量的作用,这样当每次的进入的栈都可以指向上一次的入栈(因为顶部是等于上一次入栈的地址)
-
-
出栈
- 每次把顶部向下移动,直到顶部等于底部停止
-
#include<stdio.h> #include<stdlib.h> typedef struct Node { int date;//指针域 struct Node* pNext;//数据域 }NODE,*PNODE; typedef struct Stack { PNODE pTop;//struct Node * <<==>> PNODE PNODE pBottom; }STACK,*PSTACK; void init(PSTACK ); void push(PSTACK , int ); void traverse(PSTACK ); bool pop(PSTACK , int* ); void clear(PSTACK ); int main() { STACK S; int val; init(&S); push(&S, 1); push(&S, 2); push(&S, 3); push(&S, 4); traverse(&S); traverse(&S); clear(&S); if(pop(&S,&val))//显示出栈失败 表明清空成功 printf("出栈成功"); else printf("出栈失败"); traverse(&S); return 0; } void init(PSTACK ps)//初始化一个栈 里面只有ptop和pbottom两个节点 { ps->pTop = (PNODE)malloc(sizeof(NODE)); if (ps->pTop == NULL) { printf("分配空间失败"); } else { ps->pBottom = ps->pTop; ps->pTop->pNext = NULL; } } void push(PSTACK ps, int val) { PNODE pnew = (PNODE)malloc(sizeof(NODE)); if (pnew == NULL) { printf("分配内存失败"); exit(-1); } else { pnew->date = val; pnew->pNext = ps->pTop;//让指针域指向上一个栈 ps->pTop = pnew;//让ptop向后移一个位置 野就是指向pnew; } return; } void traverse(PSTACK ps) { PNODE p = ps->pTop; while (p!=ps->pBottom) { printf("%d\n",p->date); p = p->pNext;//将ps->pTop往前面移 } printf("\n"); return; } bool empty(PSTACK ps) { if (ps->pTop == ps->pBottom) { return true; } else return false; } //把ps出栈一次 ,出栈的元素存入pval形参所指的变量中 bool pop(PSTACK ps, int* pval) { if (empty(ps)) return false; else { PNODE r = ps->pTop; *pval = r->date; ps->pTop = r->pNext; free(r); r = NULL; return true; } } void clear(PSTACK ps) { if (empty(ps)) { return; } else { PNODE p = ps->pTop; PNODE q = NULL; while (p != ps->pBottom) { q = p->pNext; free(p); p = q; } ps->pTop = ps->pBottom; } }
-
-
-
应用
- 函数调用
- 中断
- 表达式求值
- 内存分配
- 缓存处理
- 迷宫
线性结构的两种常见应用之二 队列
-
定义
- 一种可以实现“先进后出”的存储结构
-
分类
-
链式队列
- 用链表实现
-
静态队列
-
用数组实现
-
静态队列通常都必须是循环队列
-
删除只能在前端(front)
-
循环队列的讲解:
-
由于条件
rear == max - 1
变为真,因此无法再插入任何元素。此时我们删除了队列的两个元素,但是因为删除的是在前端,此时我们然仍无法在删除的两个元素中插入任何元素因为rear == max - 1仍然成立 ,这是线性队列的主要问题,虽然在数组中有空间可用,但是不能在队列中插入任何更多的元素。这只是内存浪费, -
-
需要克服这个问题。这时就可以考虑使用循环队列了,既为第一个索引紧跟在最后一个索引之后。
- 这时只有当
front = -1
和rear = max-1
时,循环队列将满。循环队列的实现类似于线性队列的实现。只有在插入和删除的情况下实现的逻辑部分与线性队列中的逻辑部分不同。
-
插入只能在后端(rear)
-
-
循环队列需要几个参数来确定 及其含义的讲解
-
需要两个参数来确定
- front
- rear
-
两个参数不同场合有不同的含义(先记住 后体会)
-
队列初始化
- front和rear初始值都为零
-
队列非空
- front代表队列的第一个元素
- rear代表的是队列的最后一个有效元素的下一个元素
-
队列空
-
-
-
-
循环队列入队的伪算法讲解
-
将值存入r所代表的位置
-
将r后移 rear=(rear+1)%数组长度 - 错误写法 rear=rear+1
-
-
循环队列出队的伪算法讲解
-
front = (front+1) % 数组长度
-
-
如果判断循环队列是否为空
- 如果front与rear的值相等,则改队列一定为空
-
然后判断循环队列是否已满
-
预备知识
- front的值和rear的值没有规律 即可以大,小,等
-
多增加一个表标识的参数
-
少用一个队列中的元素
-
如果rear和front的值紧挨着,则队列已满
-
用c语言伪算法表示就是
-
if((r+1)/数组长度==f) 已满 else 不满
-
-
-
-
算法
-
入队
-
出队
-
队列的具体应用
- 所有和时间有关的操作都有队列的影子(例如操作系统认为先进来的先处理)
-
-
#include<stdio.h> #include<stdlib.h> typedef struct Queue { int* pBase; int front; int rear; }QUEUE; void init(QUEUE*); bool en_queue(QUEUE*, int );//入队 void traverse_queue(QUEUE*); bool full_queue(QUEUE*); bool empty_queue(QUEUE*); bool out_queue(QUEUE*, int*pval);//出队 int main(void) { QUEUE Q; int val; 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);//这里将不会再插入元素 因为我们让最后一个队列的元素指定为空 为了来判断 //队列是否为满 不然空和满将无法判断 if (out_queue(&Q, &val)) printf("出队成功,元素为%d\n", val); else printf("出队失败"); traverse_queue(&Q); return 0; } void init(QUEUE* pQ) { pQ->pBase = (int*)malloc(sizeof(int) * 6); pQ->front = 0; pQ->rear = 0; return; } bool full_queue(QUEUE*pQ) { if ((pQ->rear + 1) % 6 == pQ->front) return true; else return false; } bool en_queue(QUEUE*pQ, int val) { if (full_queue(pQ)) return false; else { pQ->pBase[pQ->rear] = val; pQ->rear = (pQ->rear + 1) % 6; return true; } } bool out_queue(QUEUE*pQ, int* pval)//出队 { if (empty_queue(pQ)) return false; else { *pval=pQ->pBase[pQ->front];//出队让当前有效值第一个入队的输出 pQ->front = (pQ->front + 1) % 6;//然后让头指向下一个元素 取%是循环的效果 return true; } } bool empty_queue(QUEUE*pQ) { if (pQ->front == pQ->rear) return true; else return false; } void traverse_queue(QUEUE* pQ) { int i = pQ->front;//这里用i来等效输出 是为了不改变front里面的值 while (i != pQ->rear) { printf("%d",pQ->pBase[i]); i = (i + 1) % 6; } printf("\n"); return; }
-
专题 递归
-
用递归来实现的,最简单,像求阶乘这种没有明确执行次数的问题,都是用递归来解决】
-
定义
- 一个函数自己直接或间接调用自己(一个函数调用另外一个函数和他调用自己是一模一样的,都是那三不,只不过在人看来会有点诡异)
-
递归满足的三个条件
- 递归必须的有一个明确的终止条件(不然会陷入死循环)
- 该函数处理的数据规模必须在递减(递归的值可以增加 但数据规模必须递减)
- 这个转换必须是可解的
-
循环和递归(理论上循环可以解决的 递归也可以,但递归可以的 循环不一定可以)
- 先来看看函数调用的思想
-
- 递归
- 易于理解
- 速度慢
- 存储空间大
- 循环
- 不易于理解
- 速度快
- 存储空间小
-
举例
-
求阶乘
-
1+2+…+n的和
-
汉诺塔【不是线性递归 是非线性递归】
-
走迷宫
-
递归的运用
-
树和森林就是以递归的方式定义的
-
树和图的很多算法都是以递归来实现的
-
很多数学公式就是以递归的方式定义的
-
斐波那契序列
- 1 2 3 5 8 13 21 34。
-
-
-
逻辑结构:
- 线性 (用一根直线穿起来)
- 数组
- 链表
- 栈和队列是一种特殊的线性结构(操作受限的线性结构 不受限的是在任何地方可以增删改查,可以是数组和链表的实现,只要把链表学会,栈和队列都能搞定,数组稍微复杂一些)
- 非线性
- 树
- 图
物理结构:
- 数组
- 链表
模块二:非线性结构
现在人类还没有造出一个容器,能把树和图都装进去的,因为他们很复杂) |
---|
树
-
专业定义
- 有且只有一个称为根的节点
- 有若干个互不相交的子树,这些子树本身也是一棵树
-
通俗的定义
- 树是由节点和边组成(边可以理解成连接树的指针域)
- 每个节点只有一个父节点但可以有很多子节点
- 但有一个节点例外 ,该节点没有父节点,此节点称为根节点
-
专业术语
- 节点 父节点 子节点
- 子孙 堂兄弟
- 深度:
- 从根节点(第一层节点)到最底层节点的层数称为深度
- 叶子节点
- 没有子节点的节点
- 非终端节点
- 就是非叶子节点
- 根节点既可以是叶子节点又可以是非叶子节点
- 度
- 子节点的个数称为度(一棵树看最大的即可)
-
树分类
-
一般树
- 任意一个节点的子节点的个数不受限制
-
二叉树
-
任意一个子节点最多只有两个子节点,且子节点的位置不可更改
-
满二叉树
-
如果二叉树中除了叶子节点,每个节点的度都为2,则此二叉树称为满二叉树
-
性质 满二叉树中第 i 层的节点数为 2^(i-1) 。(i为节点的标号) 深度为 k 的满二叉树必有 2^k-1 个节点 ,叶子数为 2^(k-1) 满二叉树中不存在度为 1 的节点,每一个分支点中都两棵深度相同的子树,且叶子节点都在最底层。 具有 n 个节点的满二叉树的深度为 log2 (n+1)。 -
-
-
完全二叉树(满二叉树就是完全二叉树的一个特例)
-
如果二叉树中除去最后一层节点为满二叉树,且最后一层的结点依次从左到右分布(如果不是依次分布,则是非完全二叉树),则此二叉树被称为完全二叉树。
-
完全二叉树具有不同二叉树的性质
-
性质 (i为节点的标号) n 个结点的完全二叉树的深度为 ⌊log2 n⌋+1。(⌊log2 n⌋ 表示取小于 log2n 的最大整数。例如,⌊log2 4⌋ = 2,而 ⌊log2 5⌋ 结果也是 2。) 当 i>1 时,父亲结点为结点 [i/2] 。(i=1 时,表示的是根结点,无父亲结点) 如果 2**i>n(总结点的个数) ,则结点 i 肯定没有左孩子(为叶子结点);否则其左孩子是结点 2* *i 。 如果 2**i+1>n ,则结点 i 肯定没有右孩子;否则右孩子是结点 2* *i+1 。
-
-
-
-
-
森林
- n个互不相交的树的集合
-
-
一般的二叉树要以数组的方式存储,要先转换成完全二叉树,因为如果不转换成完全二叉树 而你只存有效节点(无论先序,中序,后序),则无法知道这个树的组成方式是什么样子的(不知道原本二叉树的样子),所以要把垃圾节点也存储起来才可以
-
树的存储(都是转换成二叉树来存储)
-
二叉树的存储
- 连续存储【完全二叉树 不是完全二叉树的补充为完全二叉树】
- 优点
- 查找某个节点的父节点和子节点(也包括判断有没有子节点) 速度快
- 缺点
- 耗用内存空间过大
- 优点
- 链式存储
- 两个指针域分别指向两个子节点,没有子节点的为空
- 优点 耗用内存空间小
- 缺点 查找父节点不方便
- 连续存储【完全二叉树 不是完全二叉树的补充为完全二叉树】
-
一般树存储
-
双亲表示法
- 这样的方法求父节点方便 但子节点复杂 是一种连续存储的空间的数组来存储的
-
孩子表示法
-
- 这样的方法求子节点方便 但求父节点非常复杂
-
双亲孩子表示法
- 这样的方法求父节点和子节点都很方便 这也是用一块连续存储的数组来实现的
-
二叉树表示法
- 把一个普通的树转换成二叉树来存储
- 具体转换方法
- 设法保证任意一个节点的
- 左指针域指向它的第一个孩子
- 右指针域指向它的下一个兄弟节点(下一个表示在它的下一层,兄弟表示为它的兄弟)
- 只要能满足此条件,就可以把一个普通的二叉树转换成二叉树
- 一个普通树转换成的二叉树一定没有右子树 如下图的一般树转换成二叉树
-
- 设法保证任意一个节点的
-
森林的存储
- 先把森林转换成二叉树,再存储二叉树
- 具体转换方法
- 将森林中的每个树的节点当做兄弟存储:
- 设法保证任意一个节点的
- 左指针域指向它的第一个孩子
- 右指针域指向它的下一个兄弟
- 只要满足条件,就可以把一个森林转换成二叉树来存储
-
-
二叉树的遍历(把非线性的结构变为线性的结构 这样我们可以更好的进行访问)
- 遍历可以理解为以递归的方式实现
- 先序遍历
- 先先序访问根节点
- 再先序访问左子树
- 再先序访问右子树
- 中序遍历【中序访问根节点】
- 中序遍历左子树
- 再访问根节点
- 再中序遍历右子树
- 后序遍历【最后访问根节点】
- 先后序遍历左子树
- 再后序遍历右子树
- 再访问根节点
-
-
已知两种遍历序列求原始的二叉树
- 先序,中序和后续三种遍历中,只知道其中任意一个,是无法还原其原始的树结构的
- 通过先序和中序 或者 中序和后续我们可以还原出原始的二叉树,但是通过先序和后序是无法还原原始的二叉树的
- 换种说法 只有通过中序和后序 或者 通过中序和后序我们才能唯一的确定一个二叉树的
- 已知先序和中序求后序的例子
- 先序:ABCDEFGC
- 中序:BDCEAFHG
- 求后序:DECBHGFA
- 已知后序和中序求后序的例子
- 中序:BDCEAFHG
- 后序:DECBHGFA (排在后面的为根节点)
- 求先序:ABCDEFGH
-
-
树的具体实现程序
-
#include<stdio.h> #include<stdlib.h> struct BTNode { char date; struct BTNode* pLchild;//L是左child孩子 struct BTNode* pRchild;///R是右child孩子 }; struct BTNode* CreateBTree(void); void PreTraverseBTree(struct BTNode* pT); void inTraverseBTree(struct BTNode* pT); void backTraverseBTree(struct BTNode* pT); int main(void) { struct BTNode* pT = CreateBTree();//发送的时候 只发送根节点的地址 就可以找到整个树 程序运行快 //因为如果不发送地址 发送整个根节点的话 可能就会造成数据过大 程序运行慢 printf("先序遍历\n"); PreTraverseBTree(pT); printf("中序遍历\n"); inTraverseBTree(pT); printf("后序遍历\n"); backTraverseBTree(pT); return 0; } void PreTraverseBTree(struct BTNode* pT) {//先序遍历 if (pT != NULL)//必须加if 如果不加if会访问到空的指针域 跳出遍历 { printf("%c\n", pT->date);//打印根节点 即为访问根节点 if (NULL != pT->pLchild)//加if条件的好处是 可以加快程序运行速度 因为递归调用很慢 { PreTraverseBTree(pT->pLchild); } if (NULL != pT->pRchild) { PreTraverseBTree(pT->pRchild); } } } void inTraverseBTree(struct BTNode* pT) {//中序遍历 /* 中序遍历左子树 再访问根节点 再中序遍历右子树*/ if (pT != NULL)//必须加if 如果不加if会访问到空的指针域 跳出遍历 { if (NULL != pT->pLchild)//加if条件的好处是 可以加快程序运行速度 因为递归调用很慢 { inTraverseBTree(pT->pLchild); } printf("%c\n", pT->date); if (NULL != pT->pRchild) { inTraverseBTree(pT->pRchild); } } } void backTraverseBTree(struct BTNode* pT) {//后序遍历 /* 后序遍历左子树 再后序遍历右子树 再后序访问根节点*/ if (pT != NULL)//必须加if 如果不加if会访问到空的指针域 跳出遍历 { if (NULL != pT->pLchild)//加if条件的好处是 可以加快程序运行速度 因为递归调用很慢 { backTraverseBTree(pT->pLchild); } if (NULL != pT->pRchild) { backTraverseBTree(pT->pRchild); } printf("%c\n", pT->date); } } struct BTNode * CreateBTree(void) { //malloc只返回struct BTNode*类型第一个字节的地址(返回被分配内存的指针) 所以PA存放的也是这个结构体第一个字节的地址 可以通过这个地址找到分配的内存大小 struct BTNode* pA = (struct BTNode*)malloc(sizeof(struct BTNode)); struct BTNode* pB = (struct BTNode*)malloc(sizeof(struct BTNode)); struct BTNode* pC = (struct BTNode*)malloc(sizeof(struct BTNode)); struct BTNode* pD = (struct BTNode*)malloc(sizeof(struct BTNode)); struct BTNode* pE = (struct BTNode*)malloc(sizeof(struct BTNode)); pA->date = 'A'; pB->date = 'B'; pC->date = 'C'; pD->date = 'D'; pE->date = 'E'; pA->pLchild = pB; pA->pRchild = pC; pB->pLchild = pB->pRchild = NULL; pC->pLchild = pD; pC->pRchild = NULL; pD->pRchild = pE; pD->pLchild = NULL; pE->pLchild = pE->pRchild = NULL; return pA; };
-
树的应用
模块三:查找和排序
- 折半排序
- 排序:
- 冒泡
- 插入
- 选择
- 快速排序
- 归并排序
- 排休和查找的关系
- 排序是查找的前提
- 排序是重点
再次讨论什么是数据结构
-
数据结构研究是数据结构的存储和数据的操作的一门学问
-
数据的存储分为两部分
-
个体的存储
-
个体关系的存储
-
从某个角度而言,数据的存储是最核心的就是个体关系的存储
,个体的存储可以忽略不计
-
再次讨论到底什么是泛型
- 同一种逻辑结构,无论该逻辑结构物理存储是什么样子的我们可以对它执行相同的操作