数据结构基础知识点总结
数据结构基础知识
线性结构
(1)连续储存(地址在内存中为连续)-----数组
(2)离散储存(地址在内存中不一定为连续的)-----链表
非线性结构
(1)树
(2)图
基础算法(查找排序):
折半查找
排序:
(1)冒泡排序
(2)插入排序
(3)选择排序
(4)快排
(5)并归
3.C语言指针(数据结构基础):
(1)指针
(2)结构体(C++可以使用类)
(3)动态内存的分配与释放(malloc函数)
//声明一个整型指针变量p
//注意:此时我们声明的变量为p,而不是*p
int a=100;
int* p;
int** q;
p=&a;// *p=a;
q=&p;=》q等于p的地址=》*q=p;=》**q=*p;=》**q=a;
此时,以下变量的含义是:
p: p为指向一个整型变量的指针变量,储存着所指向的整型变量的地址
*p: *是指针运算符,*p为指针变量p所指向的存储单元中的内容
&a:&为取地址运算符,&a为a的在内存中的地址
q:一个指向指针变量p的指针变量,其中储存的是p的地址
具体可见下表所示:
变量 | a | p | *p | q | *q | **q |
储存的值 | 100 | a的地址 | 100 | p的地址 | p | 100 |
概念混淆梳理:
1.指针变量(4字节)地址编号为首字节编号
2.指针变量和指针(地址)不是一个概念,指针变量是用来的存放地址的变量
3.指针变量占四个字节,指针变量大小和操作系统有关,32位系统时占四个字节,64位系统时占八个字节,当然,也与编译器类型有关,32位编译器(Win32)编译的程序占四个字节
附图:
传参数改变值案例代码:
#include "stdio.h" void f(int* i) { *i = 1000; } int main(void) { int p = 100; f(&p); printf("p=%d\n", p); return 0; }
代码最后执行结果是:p=1000
值传递和地址传递问题:
值传递原理在于将值复制到形参相应储存单元中,即形参和实参拥有不同的储存单元(地址),这种方式形参值变化不影响实参,而地址传递则是将
一维数组:
一维数组名是一个指针常量:
int a[5]={0,1,2,3,4,5}; //声明一个大小为5的数组
a是一个指针常量,这是因为a实际上是数组第一个元素的地址,即a[0]的地址,即:
*a = a[0]; // a = &a[0];
有以下式子成立:
a[i] 《=》*(a+i) //原因为一维数组在内存中地址连续,故a[i]的值为地址a加上i后得到的新地址取其指向的值
Tip:关于编译预处理命令
作用:编译之前要处理的内容
详解:stdio: std:标准 io:输入输出流
#include
#include “stdio.h” =》引入自定义文件时使用,其先搜索项目目录,再搜索系统目录,耗时较长
注意,千万不要写成studio(手动滑稽)
被调函数修改主调函数中一维数组的值示例代码
//只需要传入被调函数两个参数,1.数组首元素地址2.数组长度 此时便可以在被调函数中随意访问数组元素 #includevoid f(int *p ,int len) { p[1] = 9; } int main(void) { int a[5] = { 0,1,2,3,4 }; sizeof(int); f(a,sizeof(a)/sizeof(int)); printf("a[1]为 %d\n", a[1]); return 0; }
结构体:
只有成员,没有方法
为了表示一些较为复杂的数据结构,而普通的基本类型无法满足要求
结构体定义与初始化示例代码:
#include#include <string.h> struct Student { int sid; char name[200]; //C语言中没有字符串概念,统一使用字符数组来操作 int age; };//这里不要忘记加上';',这里是语句终止 int main(void) { //这里是第一种赋值方式 struct Student st = { 1000 , "strcpy()" , 20 }; //这里是第二种赋值方式 //struct Student st; //st.sid =1000; //strcpy_s(st.name,"zhangsan");//这里使用可以保证缓冲区大小的安全型函数strcpy_s(),使用strcpy()会报错,提示不安全 //st.age =20; printf("%d %s %d\n", st.sid, st.name, st.age); return 0; }
使用指针来初始化结构体示例代码
#include#include <string.h> struct Student { int sid; char name[200]; //C语言中没有字符串概念,统一使用字符数组来操作 int age; }; int main(void) { struct Student st; struct Student* Pst; Pst = &st; Pst->sid = 99; //Pst->sid 等价于(*Pst).sid 指针Pst所指向的变量的成员sid Pst->age = 20; strcpy_s(Pst->name, "zhangsan"); printf("%d %s %d\n", st.sid, st.name, st.age); return 0; }
综上,指针的优越性在于:
当以传值方式来调用时,需要将变量复制一份,故要在内存中开辟一块同样大小的内存,并且会耗费大量的时间。
当以传递指针方式来调用时,此时只需要开辟一块新的内存空间来存放新声明的指针变量,大小永远为4个字节,由此可见,当新声明的变量大小大于四个字节时,使用指针变量无疑是非常合适的,既能节省空间,也可以节省时间。
当然,对于int类型的变量来说,就无所谓,因为其也为四个字节大小。对于结构体来说,指针变量无疑是福音。
线性结构:
(1)数组
(2)链表:栈,队列
动态数组
代码实现一个动态数组:
#include#include struct Arr { int* pBase; //数组首地址 int len; //数组所容纳最大元素个数 int cnt; //当前数组有效元素个数 int increment; //自动增长因子 }; void init_arr(struct Arr *pArr ,int length ); //初始化 bool is_empty(struct Arr* pArr); //检测数组是否为空 bool is_full(struct Arr* pArr); //检测数组是否为满 void show_arr(struct Arr* pArr);//输出数组元素 bool append_arr(struct Arr* pArr,int val);//在数组末尾追加元素 bool insert_arr(struct Arr* pArr, int pos, int val);//插入到pos位置,pos表示数组中第几个 bool delete_arr(struct Arr* pArr, int pos, int* pval);//删除指定位置的元素,pos表示数组中第几个 void inversion_arr(struct Arr* pArr); //void sort_arr();//数组排序 int main(void) { int pval; struct Arr arr; init_arr(&arr,10); show_arr(&arr); append_arr(&arr, 1); append_arr(&arr, 3); append_arr(&arr, 2); append_arr(&arr, 4); show_arr(&arr); insert_arr(&arr,3,9); show_arr(&arr); delete_arr(&arr,3, &pval); show_arr(&arr); printf("删除的元素是%d \n", pval); inversion_arr(&arr); show_arr(&arr); return 0; } //初始化动态数组 void init_arr(struct Arr* pArr, int length) { pArr->pBase = (int*)malloc(sizeof(int) * length); if (NULL == pArr->pBase) { printf("动态内存分配失败!"); exit(-1);//终止程序 } else { pArr->len = length; pArr->cnt = 0; } } //检测数组是否为空 bool is_empty(struct Arr* pArr) { if (pArr->cnt == NULL) { return true; } else { return false; } } //检测数组是否为满 bool is_full(struct Arr* pArr) { if (pArr->cnt == pArr->len) { return true; } else { return false; } } //输出数组元素 void show_arr(struct Arr* pArr) { if (is_empty(pArr)) { printf("数组为空!\n"); } else { for (int i = 0;i < pArr->cnt;i++) { printf("%d", pArr->pBase[i]); printf("\n"); } } } //在数组末尾追加元素 bool append_arr(struct Arr* pArr, int val) { //数组已满 if (is_full(pArr)) { return false; } pArr->pBase[pArr->cnt] = val; (pArr->cnt)++; } //插入到pos位置,pos表示数组中第几个 //插入位置后面的元素全部后移 bool insert_arr(struct Arr* pArr, int pos, int val) { if (pos<1 || pos>pArr->cnt + 1 || is_full(pArr)) //pos最大只能插入到现有元素后一个 { return false; } for (int i = pArr->cnt - 1;i >= pos - 1;--i) { pArr->pBase[i + 1] = pArr->pBase[i]; } pArr->pBase[pos - 1] = val; (pArr->cnt)++; } //删除指定位置的元素,pos表示数组中第几个 //删除位置后面的元素全部前移 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; } //将数组中所有元素倒序 void inversion_arr(struct Arr* pArr) { int i = 0; 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; } }
链表
预备知识:typedef的用法
typedef struct Student { int sid; char name[100]; char sex; }*PST,ST ;
这里的意思是:将结构体Student重命名为ST,将结构体Student指针类型重命名为*PST,现可声明如下:
ST st; // struct Student st;
PST pst; // struct Student* pst;
想要确定一个链表,我们需要什么参数?(什么可以确定一个链表?)
头指针
通过头指针我们即可推算出链表其他所有信息
下面为一个链表的示意图:
数据域:数据域中储存的是指针中实际的数据
指针域:指针域中储存的是下一个节点的地址(指向下一个节点的指针)
头指针:头指针是为了方便链表操作所添加的节点
首指针:存有数据的链表的第一个节点
下面是使用代码来实现一个指针节点:
#includetypedef struct Node { int data; PNODE pNext;//struct Node* pNext }NODE,*PNODE;
链表的分类:
单链表:每个节点只有一个指针域
双链表:每一个节点有两个指针域
循环链表:能通过任何一个节点找到其他所有的节点
实现的链表算法:
- 遍历
- 查找
- 清空
- 销毁
- 求长度
- 排序
- 删除节点
- 插入节点
对于链表的插入节点与删除节点操作:
1.插入节点:
q->pNext = p->pNext; //q的指针域指向p的下一个节点
p->pNext=q; //p的指针域指向q所指向的节点
2.删除节点:
r=p->pNext;//将p的下一个节点的地址赋给r
p->pNext=p->pNext->pNext;//p指向下下个节点
int i =r->data;
free(r);//释放无用内存
return i ;//返回删除的节点值
代码实现一个链表的基础操作:
#include#include #include typedef struct Node { int data; struct Node* pNext;//不能写为:PNODE pNext }NODE,*PNODE; PNODE create_list(void); //生成一个链表 void traverse_list(PNODE pHead); //输出一个链表 bool is_empty(PNODE pHead); //判断链表是否为空 int length_list(PNODE pHead); //计算链表节点个数 void sort_list(PNODE pHead); //链表排序算法(冒泡排序脚标式) bool insert_list(PNODE pHead, int pos, int val); //在链表中指定位置插入一个数据 bool delete_list(PNODE pHead, int pos, int* pval); //在链表中指定位置删除一个数据 int main(void) { PNODE pHead = create_list(); int len = length_list(pHead); printf("链表元素个数为%d\n", len); traverse_list(pHead); bool e = is_empty(pHead); printf("链表%s", e?"为空":"不为空"); sort_list(pHead); traverse_list(pHead); int val = 0; int pos = 0; printf_s("请输入要插入到链表中的值与位置!"); scanf_s("%d %d", &val, &pos); insert_list(pHead, pos, val); traverse_list(pHead); printf_s("请输入要删除链表中的元素位置!"); scanf_s("%d",&pos); delete_list(pHead,pos,&val); printf_s("被删除的元素是%d!", val); return 0; } //生成一个链表 PNODE create_list(void) { int len; //生成的链表节点个数,用户输入 int i; int val;//用户输入的节点临时值 PNODE pHead = (PNODE)malloc(sizeof(NODE));//分配头结点内存空间 if (pHead == NULL) { printf("分配失败,程序终止!"); exit(-1);//终止程序 } PNODE pTail = pHead;//尾节点指针先指向头结点 pTail->pNext = NULL; printf("请输入您需要生成的链表元素个数:"); scanf_s("%d", &len); for (i = 0;i < len;++i) { printf("请输入第%d个节点的值:",i+1); scanf_s("%d", &val); PNODE pNew = (PNODE)malloc(sizeof(NODE)); if (pNew == NULL) { printf("分配失败,程序终止!"); exit(-1); } pNew->data = val; //分配数据 pTail->pNext = pNew;//尾节点指针域指向新生成的节点,相当于将新节点链到链表末尾 pNew->pNext = NULL; pTail = pNew; //新生成的节点变成尾节点,此时尾节点指向新生成的节点 } return pHead;//头指针便可确定一个链表,故返回头节点即可 } //输出一个链表 void traverse_list(PNODE pHead) { PNODE p = pHead->pNext;//p指向首节点 if (NULL == p) { printf("链表为空!"); exit(-1); } while (NULL!=p)//C语言区分大小写 { printf("%d", p->data); p = p->pNext;//p指向链表下一个节点 } printf("\n"); } //判断链表是否为空 bool is_empty(PNODE pHead) { if (NULL == pHead->pNext) { return true; } else { return false; } } //计算链表节点个数 int length_list(PNODE pHead) { PNODE p = pHead->pNext;//p指向首节点 int len = 0; while (NULL != p)//C语言区分大小写 { p = p->pNext;//p指向下一节点 len++; } return len; } //链表排序算法(冒泡排序脚标式) 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 pNext) { if (p->data > q->data) { t = p->data; p->data = q->data; q->data = t; } } } return; } //在链表中指定位置插入一个数据 //pHead:头节点指针 //pos:插入位置,值从1开始,插入在第pos个元素前面 //val:插入元素的值 bool insert_list(PNODE pHead ,int pos,int val) { int i=0; PNODE p = pHead; //指针指向到pos位置前一个元素身上,需移动(pos-1)次,由于i从0开始,故为小于pos-1 while (NULL!=p&&i 1) { p = p->pNext; ++i; } if (i > pos - 1 || NULL == p) { return false; } PNODE pNew = (PNODE)malloc(sizeof(NODE)); if (NULL == pNew) { printf_s("内存分配失败!"); exit(-1); } //将pNew链到p后面,将原先的p后面的元素用q保存,最后将这些元素链到pNew后面 pNew->data = val; PNODE q = p->pNext; p->pNext = pNew; pNew->pNext = q; return true; } //在链表中指定位置删除一个数据 //pHead:头节点指针 //pos:删除位置,值从1开始 //pval:带出值的指针 bool delete_list(PNODE pHead, int pos, int *pval) { int i = 0; PNODE p = pHead; //指针指向到pos位置前一个元素身上,需移动(pos-1)次,由于i从0开始,故为小于pos-1 while (NULL != p->pNext && i < pos - 1) { p = p->pNext; ++i; } if (i > pos - 1 || NULL == p->pNext) { return false; } //先用q保存要删掉的节点地址,防止内存泄露,再将下下个节点链到p后面即可 PNODE q = p->pNext; *pval = q->data; //删除后面的节点 p->pNext = p->pNext->pNext; free(q); q = NULL; return true; }
关于泛型:
数据实现不一样,操作一样,便是泛型
如何实现:
通过C++中的重载运算符便可以使代码在表面上看起来是统一的,从而实现泛型。
程序:
数据结构:研究数据存储问题
(1)个体的存储
(2)个体关系的存储
算法:对存储数据的操作
栈
一种可以实现“先进后出”的存储结构
分类:
静态栈
动态栈(链表实现)
栈的核心操作:
压栈(入栈)
出栈
栈的示意图如下:
为什么不反过来?
原因:入栈代码确实会更简单,但出栈代码会很复杂,pTop上一个节点会找不到,导致pTop无法指向上一个节点。
实现代码如下:
#include#include #include typedef struct Node { int data; struct Node* pNext; } NODE,*PNODE; typedef struct Stack { PNODE pTop; PNODE pBottom; }STACK,*PSTACK; void init(PSTACK pS); //初始化一个栈 void push(PSTACK pS, int val); //入栈 bool empty(PSTACK pS); //判断栈是否为空 void traverse(PSTACK pS); //遍历一个栈 bool pop(PSTACK pS, int* pval);//出栈 void clear_one(PSTACK pS); //清空栈第一种实现 void clear_two(PSTACK pS); //清空栈第二种实现 int main(void) { STACK S; init(&S); traverse(&S); push(&S,1); push(&S,2); push(&S,3); push(&S,4); push(&S,5); push(&S,6); traverse(&S); int i; pop(&S,&i); pop(&S,&i); printf_s("出栈元素是%d", i); pop(&S,&i); pop(&S,&i); traverse(&S); push(&S, 1); push(&S, 2); push(&S, 3); push(&S, 4); push(&S, 5); push(&S, 6); traverse(&S); clear_one(&S); traverse(&S); push(&S, 1); push(&S, 2); push(&S, 3); push(&S, 4); push(&S, 5); push(&S, 6); traverse(&S); clear_two(&S); traverse(&S); return 0; } //初始化一个栈 void init(PSTACK pS) { pS->pTop = (PNODE)malloc(sizeof(NODE)); if (NULL== pS->pTop) { printf_s("动态分配内存失败"); exit(-1); } else { pS->pBottom = pS->pTop; //将pTop指向的节点的指针域赋空(使其不链着下一个节点) pS->pTop->pNext = NULL; } } //入栈 //pS:指向栈的指针 //val:入栈元素的值 void push(PSTACK pS, int val) { PNODE pNew = (PNODE)malloc(sizeof(NODE)); if (NULL== pNew) { printf_s("动态分配内存失败"); exit(-1); } pNew->data = val; pNew->pNext = pS->pTop; pS->pTop = pNew; return; } //判断栈是否为空 bool empty(PSTACK pS) { if (pS->pTop == pS->pBottom) { return true; } else { return false; } } //遍历一个栈 void traverse(PSTACK pS) { if (empty(pS)) { printf_s("栈为空!"); return; } PNODE p = pS->pTop; while (p!=pS->pBottom) { printf_s("%d \n", p->data); p = p->pNext; } } //出栈 //pS:指向栈的指针 //pval:出栈元素的指针 bool pop(PSTACK pS, int* pval) { if (empty(pS)) { return false; } else { PNODE p = pS->pTop; *pval = p->data; pS->pTop = pS->pTop->pNext; free(p); p = NULL; return true; } } //清空栈第一种实现 //r为一个指针,永远指向p下一个节点 void clear_one(PSTACK pS) { if (empty(pS)) { printf_s("栈为空!"); return; } PNODE p = pS->pTop; PNODE r = NULL; while (p!=pS->pBottom) { r = p->pNext; free(p); p = r; } pS->pTop = pS->pBottom; return; } //清空栈第二种实现,比较好理解 //r用于保存要释放内存的节点 void clear_two(PSTACK pS) { if (empty(pS)) { printf_s("栈为空!"); return; } PNODE p = pS->pTop; PNODE r = NULL; while (p != pS->pBottom) { r = p; p = p->pNext; free(r); r = NULL; } pS->pTop = pS->pBottom; return; }
栈的应用:
1.函数调用
2.中断
3.表达式求值
4.内存分配
5.缓冲处理
6.走迷宫问题
队列
定义:一种可以实现先进先出的存储结构
分类:
1.链式队列(链表实现)
2.静态队列(数组实现)
重点:数组实现的循环队列
队列头:front 指向第一个元素
队列尾:rear 指向最后一个元素后面的元素
示意图如下所示:
由图中可见,rear指向的实际上是一个空的位置,即队列中最后一个元素后面的位置。
确定一个队列所需要的参数:
1.队列头 front
2.队列尾 rear
以下三种情况下front,rear的值:
1.初始化时:
(1)front:0
(2)rear:0
2.队列非空:
(1)front:队列第一个元素
(2)rear:队列最后一个有效元素的后一个元素
3.队列为空:
front,rear值相等,但不一定为零
循环队列
对于循环队列,其本质是为了更有效利用连续的存储空间。
我们需要讨论如下问题:
(1)如何进行入队操作
(2)如何进行出队操作
(3)如何判断循环队列是否为空
(4)如何判断循环队列是否已满
入队
首先,我们在对一个一般队列进行入队操作时,角标r(rear)所要进行的操作是向后移动一个,即:
r=r+1;
但由于现在是循环队列,当r指向队尾最后一个存储空间时,再进行+1的操作就会溢出,若这时队列头部已经有空闲空间,我们便可使r指向队列头部的空闲空间,这便是循环队列的入队操作,对于此操作,我们可以有:
//Arr.length为循环队列分配的内存元素个数
r=(r+1)% Arr.Length;
出队
同理,我们在对一个一般队列进行出队操作时,角标f(front)所要进行的操作是指向要出队元素下一个元素的位置,故有:
f=f+1;
当为循环队列时:
同理,我们有:
f=(f+1)%Arr.Length;
如何判断循环队列是否为空:
对于为空的情况,我们先假设队列中有多个有效元素,经过不断出队,f一直会向同个方向移动(即向队列尾的方向)直到剩余一个有效元素时,此时如果此有效元素出队,则队列变为空状态,f向队列尾方向移动一个单位,此时r指向的是刚才的有效元素的下一个位置,这时便会f和r指向同一个位置,即f=r的时候,队列为空。
如何判断循环队列是否已满:
对于判断循环队列为满时,我们首先要知道的是循环队列实际上是一个类似环形的结构,如下图所示:
上图中r指向4,故此时整个循环队列中只有0,1,2,3四个位置有有效元素,加入我们要进行入队或者出队操作,那么f或者r应该向顺时针方向移动,所以此时我们们要判断队列是否已满,可以有两种方式,第一种是声明一个新的变量cnt,专门用于记录队中有效元素个数,入队+1,出队-1,用cnt和队列空间大小比较即可得出此时队列是否为满,第二种是先规定循环队列最后一个位置不能存放元素,意思是当循环队列差一个元素就满了时最后一个空间不允许存放有效数据。此时f和r不可以相等,这便有效的将队列满的情况与队列空的情况区分开了,因为队列为空时的条件为r和f相等,故这种情况下我们会有:
if((r+1)% Arr.Length==f);
这个条件下队列即为满
我们通常使用第二种方法
循环队列C语言代码实现
#include; #include ; #include ; typedef struct Queue { int* pBase; //数组首地址 int front; int rear; } QUEUE; void init(QUEUE* pQ); //初始化队列 bool empty_queue(QUEUE* pQ); //队列是否为空 bool full_queue(QUEUE* pQ); //队列是否为满 void traverse_queue(QUEUE* pQ); //遍历队列 void in_queue(QUEUE* pQ, int val); //入队 bool out_queue(QUEUE* pQ, int* pval); //出队 int main() { int i; QUEUE Q; init(&Q); in_queue(&Q, 1); in_queue(&Q, 2); in_queue(&Q, 3); in_queue(&Q, 4); in_queue(&Q, 5); in_queue(&Q, 6); in_queue(&Q, 7); traverse_queue(&Q); out_queue(&Q, &i); printf_s("元素%d已出队\n", i); out_queue(&Q, &i); printf_s("元素%d已出队\n", i); out_queue(&Q, &i); printf_s("元素%d已出队\n", i); traverse_queue(&Q); out_queue(&Q, &i); printf_s("元素%d已出队\n", i); out_queue(&Q, &i); printf_s("元素%d已出队\n", i); return 0; } //初始化队列 void init(QUEUE* pQ) { pQ->pBase = (int*)malloc(sizeof(int) * 6); if (NULL == pQ->pBase) { printf_s("动态分配内存失败"); exit(-1); } pQ->front = 0; pQ->rear = 0; } //队列是否为空 bool empty_queue(QUEUE* pQ) { if (pQ->front == pQ->rear) { return true; } else { return false; } } //队列是否为满 bool full_queue(QUEUE* pQ) { if ((pQ->rear + 1) % 6 == pQ->front) { return true; } else { return false; } } //遍历队列 void traverse_queue(QUEUE* pQ) { int i =pQ->front; while (i != pQ->rear) { printf_s("%d", pQ->pBase[i]); i = (i + 1) % 6; } } //入队 //val:入队元素的值 void in_queue(QUEUE* pQ, int val) { if (full_queue(pQ)) { printf_s("队列已满,无法入队!"); return; } else { pQ->pBase[pQ->rear] = val; pQ->rear = (pQ->rear + 1) % 6; } } //出队 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; } }
队列基础应用:
所有与时间相关的操作都与队列相关。
例如:抢票系统等就由队列来实现
递归
表面上是一个函数自己直接/间接调用自己
*递归是用栈来实现的
1.为什么递归能自己调用自己?
首先我们来看一个场景,有两个函数A()和B(),现在A函数要调用B函数,那么要做些什么?
(1)将所有实参,返回地址传递给被调函数B
(2)为被调函数局部变量(也包括形参)分配存储空间
(3)将控制权转移到被调函数入口
当B函数被调用完毕后返回A时:
(1)保存被调函数B的返回结果
(2)释放被调函数B所占的存储空间
(3)依照被调函数保存的返回地址将控制权转移到调用函数A
实际上递归的时候也是如此,只不过A,B都是同一个函数罢了
用栈实现递归,具体如下图所示:
A调用B,将B压栈。B调用完毕,B出栈。此时A位于栈顶,A执行完毕,A出栈。
递归要满足的三个条件:
(1)明确的终止条件
(2)该函数所处理的数据复杂程度在递减
(3)这个转化必须是可解的
循环和递归的比较:
所有循环都能转化为递归,但是并不一定所有的递归问题都能用循环来解决
递归与循环的优缺点比较:
递归:
(1)优点:易于理解
(2)缺点:速度慢,存储空间占用多
循环:
(1)优点:速度快,浪费空间几乎没有
(2)缺点:不易理解
递归应用
例子:
(1)求阶乘
#includelong f(long n) { if (1 == n) { return 1; } else { return n * f(n - 1); } } int main() { long a = f(10); printf_s("%d", a); }
(2)高斯问题(求1+2+3…+100之和)
#includeint f(int n) { if (1 == n) { return 1; } else { return n + f(n - 1); } } int main() { int a = f(100); printf_s("%d", a); }
(3)汉诺塔(重点)
汉诺塔说的是有三根柱子A,B,C。 A柱子上有64个从底部到顶部越来越大的圆盘,现在要借助B柱子,来将A柱子上的圆盘全部搬运到C柱子上,且大圆盘不能放在小圆盘上面,请写出算法。(如下图所示)
实现逻辑:不管有几个盘子,我们所做的都是三件事:
1.将前n-1个盘子借助C从A移动到B上
2.这时便可以将n这个盘子从A移动到C上面
3.将B上的n-1个盘子借助A移动到C
实现代码如下:
#includevoid Hannuo(int n, char A, char B, char C); int main(void) { char ch1 = 'A'; char ch2 = 'B'; char ch3 = 'C'; int n = 5; Hannuo(n, ch1, ch2, ch3); } //n:盘子个数 //A:所在柱子 //B:中转主子 //C:目标柱子 void Hannuo(int n, char A, char B, char C) { if (1 == n) { printf_s("将编号为%d的盘子直接从%c柱子移动到%c的柱子上\n",n,A, C); } else { Hannuo(n - 1, A, C, B); printf_s("将编号为%d的盘子直接从%c柱子移动到%c的柱子上\n",n,A, C); Hannuo(n - 1, B, A, C); } }
树
**定义:**有且只有一个称为根的节点,下面有若干互补相交的子树
通俗的定义:
1.树是由节点和边组成
2.一个节点只有一个父节点,但可以有多个子结点
3.但有一个节点例外,该节点没有父节点,此节点称为根节点
专业术语:
1.节点
2.父节点
3.子结点
4.子孙节点
5.堂兄弟
6.深度:树中节点的最大层次称为树的深度(从根节点到最底层节点的层数)
根节点是第一层
7.叶子节点:没有子节点的节点
8.非终端节点:非叶子节点
9.根节点可以是叶子,也可以是非叶子
10.度:子节点的个数称为度,树的度为最大的度
树的分类:
(1)一般树:任意一个节点的子结点个数不受限制
(2)二叉树:任意一个节点的子结点个数最多两个且子结点位置不可更改
(2)森林:n个互不相交树的集合
二叉树:
(1)一般二叉树
(2)满二叉树:在不增加树的层数的前提下,无法再多添加一个节点的二叉树就是满二叉树
(3)完全二叉树:如果只是删除了满二叉树最底层最右边连续若干个节点,这样形成的二叉树就是完全二叉树
二叉树存储:
(1)连续存储 (数组)必须为完全二叉树
(2)链式存储
为什么一颗二叉树以数组方式存储时要是完全二叉树?
因为无法通过接过来推算出原来的树的结构。
(根据内存顺序无法推断出以前的二叉树的结构)
完全二叉树可以根据(1)先序(2)中序(3)后序 来转化成线性结构
完全二叉树数组存储优缺点:
(1)优点:查找某个节点的父节点和子结点(或判断是否有无)速度快
(2)缺点:耗用内存空间过大
二叉树链式存储优缺点:
(1)优点:耗用内存小,求子结点方便
(2)缺点:空指针域浪费空间(但很少),求父节点困难
一般树的存储(无序):
(1)双亲表示法
(2)孩子表示法
(3)双亲孩子表示法
(4)二叉树表示法
重点:二叉树表示法
一般书先转化为二叉树来存储
具体转换方法:
设法保证任意一个节点的左指针域指向它的第一个孩子,右指针域指向它的下一个兄弟节点,只有能满足这个条件,就可以把一个普通树转化为一个二叉树
一个普通树转化为二叉树,没有右子树,左边为孩子,右边为兄弟
森林的储存
森林的转化:
将B作为A的兄弟,将G作为B的兄弟即可
左边的线指向第一个孩子节点,右边的线指向下一个兄弟节点,结果如下图所示:
二叉树先中后序遍历(以下图为例)
先序遍历:
先访问根节点,再先序访问左子树,再先序访问右子树
故上图中访问顺序应为:ABFKGECDQN
中序遍历:
中序遍历左子树,再访问根节点,最后中序遍历右子树
故上图中访问顺序应为:KFGBEACQDN
后序遍历:
先后序遍历左子树,在后序遍历右子树,最后访问根节点
故上图中访问顺序应为:KGFEBQNDCA
已知两种遍历序列求原始二叉树:
我们只能通过中序加先序和后序中任意一种求原始二叉树,通过先,后序无法确定二叉树原貌
1.已知先序和中序,如何还原原始二叉树?
先序:ABCDEFGH
中序:BDCEAFHG
求后序?
步骤如下:
(1)先序中A在第一位,故A为根节点
(2)则BDCE为左子树,FHG为右子树,因为先访问左子树后访问右子树
(3)首先看左子树BDCE,在先序中B先出现,故B为左子树根节点,因为先序先访问根节点
(4)在中序中B左边没有,右边是DCE,故以B为节点的二叉树无左子树,右子树为DCE
(5)同理,C为根节点,左右分别为D和E
(6)右子树推理方式相同
最后得到的二叉树图是:
故后序遍历结果为:DECBHGFA
已知后序和中序求先序遍历结果:
中序:BDCEAFHG
后序:DECBHGFA
求先序:
(1)在后序中A为最后一个,故A为根节点
(2)根据中序遍历,左子树为BDCE,右子树为FHG
(3)BDCE中在B在最后,故B为左子树根节点,B在中序中左边无节点,右边是DCE,故其无左子树,右子树为DCE
(4)接下来操作步骤同理
最后得到的二叉树图是: