暂时未有相关云产品技术能力~
堆排序前面我们已经实现了堆的实现,其中堆的实现中最重要的算法就是向上调整和向下调整了。接下来的堆排序也同样的跟这两种算法息息相关。所以向上调整和向下调整这两种算法一定要好好掌握。在学习堆排序之前我们先来思考一个问题,既然是堆排序,那么我们在对数据进行排序的时候是否真的需要一个堆呢?是否真的需要先提前把这个堆的数据结构实现完成之后才能实现堆排序这个算法呢?如果真是这样的话那岂不是太麻烦了。所以我们不需要提前实现好堆,我们可以处理数组中的元素使这个数组中的元素变成一个堆。大家不要忘记了,堆是在完全二叉树的基础上增加了一个限制条件,这个限制条件就是堆的某个结点总是不大于或者不小于其父结点的值;另外这个堆底层就是用数组来实现的。我们在实现堆排序的时候可以把这个数组想象成一个完全二叉树,然后通过一定的算法实现把这个完全二叉树搞成一个堆。我们通常把这个过程叫做建堆。好了,实现堆排序的第一步就是把数组中的数据搞成一个堆,即我们必须先实现建堆的算法。而建堆有两种方法来实现,一种是向上调整法,另一种就是向下调整法。建堆向上调整法建堆通过利用我们刚刚实现堆的向上调整的算法来建堆(不要把这个算法想象的那么高深莫测,其实就是我们对这个数组进行一定的算法实现使这个数组中的元素变成堆的数据结构),最终结果是建立一个大根堆。我们直接看最关键的向上调整法的代码:void AdjustUp(int* a, int child) { //父亲就是(孩子-1)/2 int parent = (child - 1) / 2; while (child > 0) { if (a[child] > a[parent]) { swap(&a[child], &a[parent]); child = parent; parent = (child - 1) / 2; } else { break; } } }此时,我们就会得到一个数组,这个数组中的元素就是一个大根堆的结构。这个过程就是模拟插入建堆(操控的数组的数据,而不是堆中的数据)。举个例子吧:这个数组经过向上调整法之后就变成了这样:如果想要得到一个小根堆的话,只需要把向上调整法中a[child] > a[parent]的>改为<就可以了,即a[child] < a[parent]。程序运行之后就是:所以这里得到的就是一个小根堆。向下调整法建堆//向下调整建堆 for (int i = (n - 1 - 1) / 2; i >= 0; i--) { AdjustDown2(a, n, i); }利用堆删除的思想来进行排序刚刚通过向上调整法已经建立了大堆或者小堆,接下来就开始进行排序了。对于排序的话,无非就是升序和降序。结论就是:升序用大堆,降序就用小堆。我们先来看升序的过程(总共有两个步骤):第一步就是先利用向上调整建立一个大堆,第二步就是利用堆删除的思想来排序,最终就会得到一个升序的数组。这里我简单说一下第二个步骤(利用向下调整来进行实现):我们通过第一步建立了一个大堆,接下来开始第二步:让堆顶和堆尾进行交换,然后对此时的堆顶进行向下调整,接着交换堆顶和堆中倒数第二个数据,再次对此时堆顶的数据进行向下调整;重复此过程。最终就可以得到一个升序的数组。下面是升序过程中最核心的代码:void AdjustUp(int* a, int child) { //父亲就是(孩子-1)/2 int parent = (child - 1) / 2; while (child > 0) { if (a[child] > a[parent]) { swap(&a[child], &a[parent]); child = parent; parent = (child - 1) / 2; } else { break; } } } void AdjustDown2(int* a, int n, int parent) { int child = parent * 2 + 1; while (child < n) { if (child + 1 < n && a[child] < a[child + 1]) { child++; } if (a[child] > a[parent]) { swap(&a[child], &a[parent]); parent = child; child = parent * 2 + 1; } else { break; } } } void HeapSort(int* a, int n) { for (int i = 1; i < n; i++) { AdjustUp(a, i); } int end = n - 1; while (end > 0) { swap(&a[end], &a[0]); //AdjustDown1(a, end, 0); AdjustDown2(a, end, 0); end--; } }上图就是整个升序的过程:先利用向上调整来建立一个大堆,然后利用向下调整进行升序排序(第二个过程其实就是一个堆删除的思想)。升序代码#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> #include<assert.h> #include<stdlib.h> void swap(int* p1, int* p2) { int tmp = *p1; *p1 = *p2; *p2 = tmp; } void AdjustUp(int* a, int child) { //父亲就是(孩子-1)/2 int parent = (child - 1) / 2; while (child > 0) { if (a[child] > a[parent]) { swap(&a[child], &a[parent]); child = parent; parent = (child - 1) / 2; } else { break; } } } void AdjustDown2(int* a, int n, int parent) { int child = parent * 2 + 1; while (child < n) { if (child + 1 < n && a[child] < a[child + 1]) { child++; } if (a[child] > a[parent]) { swap(&a[child], &a[parent]); parent = child; child = parent * 2 + 1; } else { break; } } } void HeapSort(int* a, int n) { for (int i = 1; i < n; i++) { AdjustUp(a, i); } int end = n - 1; while (end > 0) { swap(&a[end], &a[0]); //AdjustDown1(a, end, 0); AdjustDown2(a, end, 0); end--; } } int main() { int a[10] = { 8,5,2,1,4,7,9,6,3,10 }; HeapSort(a, 10); for (int i = 0; i < 10; i++) { printf("%d ", a[i]); } return 0; }降序代码其实降序和升序的思路可以说基本上就是一模一样的。#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> #include<assert.h> #include<stdlib.h> void swap(int* p1, int* p2) { int tmp = *p1; *p1 = *p2; *p2 = tmp; } void AdjustUp(int* a, int child) { //父亲就是(孩子-1)/2 int parent = (child - 1) / 2; while (child > 0) { if (a[child] < a[parent]) { swap(&a[child], &a[parent]); child = parent; parent = (child - 1) / 2; } else { break; } } } //向下调整,基础为小堆 void AdjustDown1(int* a, int n, int parent) { int child = parent * 2 + 2; while (child < n) { if (child < n && a[child - 1] < a[child]) { child--; } if (a[child] < a[parent]) { swap(&a[child], &a[parent]); parent = child; child = parent * 2 + 2; } else { break; } } } //向下调整,基础为大堆 void AdjustDown2(int* a, int n, int parent) { int child = parent * 2 + 1; while (child < n) { if (child + 1 < n && a[child] < a[child + 1]) { child++; } if (a[child] > a[parent]) { swap(&a[child], &a[parent]); parent = child; child = parent * 2 + 1; } else { break; } } } void HeapSort(int* a, int n) { //向上调整建堆 for (int i = 1; i < n; i++) { AdjustUp(a, i); } 向下调整建堆 //for (int i = (n - 1 - 1) / 2; i >= 0; i--) //{ // AdjustDown1(a, n, i); //} int end = n - 1; while (end > 0) { swap(&a[end], &a[0]); AdjustDown1(a, end, 0); //AdjustDown2(a, end, 0); end--; } } int main() { int a[10] = { 8,5,2,1,4,7,9,6,3,10 }; HeapSort(a, 10); for (int i = 0; i < 10; i++) { printf("%d ", a[i]); } return 0; }好了,以上就是这篇文章的全部内容了,说实话,不简单,这需要我们不断的理解才可以把着块的内容吃透。诸位一起加油吧!!!就到这里,再见啦,各位
一、什么是引用1.1引用概念引用不是定义一个新的变量,而是给已存在的变量取一个别名,编译器并不会为引用变量开辟内存空间,而是它和它引用的变量共同占用同一块内存空间。引用格式:类型& 引用变量名(对象名)=引用实体。我们直接来举一个样例:上图中b是a的引用,而c又是b的引用,同时a、b、c其实指向的都是同一块内存空间。注意:我们不可以这样使用引用,int& b,这是一种错误写法。即:引用在Swap函数应用场景下面把引用应用到Swap函数上,请看: void Swap(int& m, int& n) { int tmp = m; m = n; n = tmp; } int main() { int a = 10; int b = 20; cout << "a=" << a << endl; cout << "b=" << b << endl; Swap(a, b); cout << "a=" << a << endl; cout << "b=" << b << endl; return 0; }上面代码函数中的形参就是实参的一个引用,即m是a的引用,n是b的引用,改变m就是改变a,改变n就是改变b。为指针定义别名引用还可以为指针定义别名,请看:上图就是我们通过引用交换了两个变量的地址。1.2引用特性1.引用必须要进行初始化(即int& a;这种写法是错误的)。2.一个变量可以有多个引用3.引用一旦引用一个实体,则不能在继续引用其它实体。同时,引用类型必须要与引用实体是同一种类型。二、引用使用场景2.1引用做参数比如,我们刚刚的交换函数Swap就是利用的引用来做参数:void Swap(int& m, int& n) { int tmp = m; m = n; n = tmp; }2.2引用做返回值下面是引用作传值返回。 int fun() { static int n = 0; n++; return n; } int main() { int ret = fun(); return 0; }函数fun中的变量n存放在静态区中,出了局部域后栈帧不会销毁。但是在进行返回n的时候依然会生成临时变量。所以,引用作返回值的时候,不管变量n是局部变量还是静态区的变量又或者是全局变量。编译器都会生成一个临时变量。倘若我们不想生成临时变量的话应该怎么半呢?这就是引用的另一大作用:即引用作返回值的时候不会生成临时变量。不生成临时变量的好处:减少了拷贝,提高了效率。请看:这里就是返回的n的别名(即返回的n的引用)。当我们的变量n是局部变量而非静态区变量的时候。请看:我们已经知道这里返回的是局部变量n的别名,当把局部变量n的值给ret的时候,局部变量n所在的函数栈帧已经销毁了(即归还局部变量n所占空间的使用权),所以返回变量n的值理论上应该是不确定的。所以这里程序的运行结果应该有两种情况:情况一:如果函数fun结束后,栈帧销毁,但是没有销毁栈帧,此时ret的结果侥幸是正确的。情况二:如果函数fun结束后,栈帧销毁,清理栈帧了,那么此时ret的结果是随机的。总之具体栈帧是否销毁还是要看编译器是如何处理的。还有一种情况,请看:首先,ret依然是n的别名(因为返回的是n的别名,而ret又是n的别名的别名,所以最后ret就是n的别名)。其次我们发现程序运行结果就成了随机值了,这里的话就是刚刚上面提到的第二种情况了,即函数栈帧销毁后,清理了函数栈帧,所以打印出来的就是随机值。这里的话依然是和编译器的处理方式有关。我们再来看一种情况:这里打印出来依然是随机值,但是其具体原因与刚刚那种情况有些不同,这里rand()函数调用栈帧正好把fun()函数的栈帧覆盖了(说白了就是后面的函数栈帧覆盖了前面的函数栈帧),打印出来时随机值其实归根揭底还时栈帧被清理的问题。2.3引用的几大作用(1)引用作参数(形参的改变可以改变实参),即引用作输入型参数;引用也可以作输出型参数提高效率。(2)引用作返回值(当对象比较大的时候,与传值返回相比会极大的提高效率)。三、常引用常引用这里我们通过几段代码来引出来:请看:上述变量a经过const修饰后不可更改,既然a本身已经不可以进行更改了,所以就不要妄想通过引用b来更改a了,这里强调的就是在引用的过程中,权限不可以放大。再来看一段代码:上述代码中强调的就是引用过程中权限可以平移但是权限不可以缩小。就比如说上述代码中的const int& b = x;这里缩小的并不是变量x的权限,而是缩小的引用b作为别名的权限。再来看这里,这里要强调的是x++是可以的,但是b++是不可以的,因为引用过程中引用b经过const修饰后权限被缩小了,这里虽然我们不能通过引用b来改变变量x,但是我们可以直接对变量x进行操作(即x++)就可以了。再来看看const int& m = 10;这条语句,首先这条语句是正确的,引用b是常量10的别名,既然10是常量即不可更改,那么就需要const进行修饰。说白了这里就是给常量区别名。下面我们先来看一下隐式类型转换的代码:发生类型转换的时候中间会产生临时变量,所以只要发生类型转换,都会产生临时变量。请再来看下段代码:上述代码的话为什么int& c = a;报错了,但是const int&c = a;没有报错。这里依然是临时变量的问题,请看下图:注意类型转换才会产生临时变量,相同类型不会产生临时变量。下面我们来看一看为什么不同类型会产生临时变量而相同类型不会产生临时变量的原因:上述代码之所以会打印出来就是因为发生了类型转换,产生了临时变量。当运算符两边的内容不是同一类型的时候会发生类型提升,提升有提升的规则,一般时范围小的向范围大的进行提升。上述变量j和变量i是如何进行比较的呢?因为这两个变量不是同一个类型,所以这里依然会生成一个临时变量,对变量i进行提升,这个提升的过程其实就是生成一个临时变量的问题,这个临时变量在这里是8个字节,即生成double类型的临时变量,然后依靠这个double类型的临时变量区和double类型的变量j进行比较。下面我们再来举一个权限放大的例子:此时引用ret1经过const修饰后,相当于权限的平移,就不会报错了。再来看一看权限的平移和权限的缩小的例子:四、指针和引用的区别:1.引用概念上定义一个变量的别名,子还真存储一个变量地址。2.引用在定义时必须初始化,而指针没有要求。3.引用在初始化时引用一个实体后,就不能在引用其他实体,而指针可以在任何时候指向任何一个同类型实体。4.没有NULL引用,但是有NULL指针。5.在sizeof中含义不同:引用结果为引用类型的大小,但是指针始终时地址空间所占个数。6.引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小。7.有多级指针,但是没有多级引用。8.访问实体方式不同,指针需要显式解引用,引用编译器自己处理。9.引用比指针使用起来相对更安全。好了,以上就是对C++中引用的一些基本内容。就到这里吧,再见啦各位!!!
C++关键字(C++98)在C++中,总共有63个关键字,大家还记得在C语言中有多少个关键字吗,没错,在C语言中总共有32个关键字。下面是C++的关键字:asmdoifautoreturntrycontinuedoubleinlineshorttypedefforbooldynamic_castintsignedtypeidpublicbreakelselongsizeoftypenamethrowcaseenummutablestaticunionwchar_tcatchexplicitnamespacestatic_castunsigneddefaultcharexportnewstructusingfriendclassexternoperatorswitchvirtualregisterconstfalseprivatetemplatevoidtrueconst_castfloatprotectedthisvolatilewhiledeletegotoreinterpret_cast命名冲突可以看到:上述代码我们可以通过。那现在这段代码呢?请看:我们发现rand出现了重定义的错误:那为什么rand发生了冲突了呢?其实rand这里是跟库发生了冲突。std中有一个函数也叫rand,所以就会发生命名冲突的问题。发生命名冲突的原因有两种:1.跟库里发生冲突。2.互相之间发生冲突。其实命名冲突这个问题挺麻烦的,我们并不知道库中是否有这个变量,在这个基础上,如果我们又重新定义了这个变量,此时就会出现命名冲突的问题。为了解决命名冲突的问题,就引出了命名空间。命名空间嗯,namespace就是命名空间的意思,也是C++中的一个关键字,就是用来解决命名冲突的问题的。这里,我们先来引出一个问题,请看下面代码: #include<iostream> using namespace std; int main() { return 0; }在C++中,为什么我们一般要加上using namespace std;呢?命名空间的定义命名空间,即namespace,后面跟着命名空间的名字,然后接一对{},{}中的内容就是命名空间的成员。命名空间namespace可以定义一个域出来。举个例子:我们可以看到上图的代码出现了rand重定义的问题,我们如果不想出现这个问题,就可以用namespace定义一个域出来以解决重定义这个问题。请看解决方法: #include<stdio.h> #include<stdlib.h> //域 namespace hello { //hello这个域就会把域中的内容(rand)进行一个隔离 //这里要注意,域中的rand与主函数中的rand不是一回事。 int rand = 0; } int main() { printf("%d\n", rand); return 0; }这个运行结果也说明了域中的rand与主函数中的rand不是一回事。局部域和全局域的关系 #include<stdio.h> #include<stdlib.h> int a = 0; int main() { int a = 1; printf("%d\n", a); return 0; }上面这个代码中的两个a就可以同时存在。我们把主函数外面的a看作是全局域中的a,把主函数里面中的a看作是局部域中的a。同时,我们还要知道一个点局部域和全局域既影响访问,也影响生命周期。域分为好几种,作用域只是域中的一种。域总共分为下面四种:类域命名空间域局部域全局域我们来运行上面的代码:因为局部域中的a优先,所以这里打印出来的是局部域中变量a的值,即打印出来的是1,如果我们想打印全局域中的a中的值怎么办呢?我们可以这样: #include<stdio.h> #include<stdlib.h> int a = 0; int main() { int a = 1; printf("%d\n", a); // ::称为域作用限定符 printf("%d\n",::a); return 0; }运行结果如上图,我们看到我们打印出来的0就是全局域中的a的值。关于局部域和全局域的关系是这样的:我们是默认从局部域开始搜索的,即局部优先;当然,如果局部没有的话,我们就会去全局域进行搜索,所以有局部先访问局部,没有局部才会去访问全局。我们通过域作用限定符::可以去直接访问全局域。即上面的代码,注意观察,::的前面是不加任何东西的(注意前面不需要我们加空格),意思就是我们直接去访问全局域。命名空间域上图代码中总共有三个域(局部域、全局域、命名空间域),每个域中都有一个变量a,我们如何去访问这三个作用域中的a呢,请看下面:我们现在把全局域中的变量a注释掉看看运行结果,所以来看下面这段代码的运行结果:我们发现程序根本运行不了,所以这里就报错了。所以我们通过这个程序发现,这个程序并不会去到命名空间里去进行搜索;那这里就会引出一个新的问题,程序什么时候才会去到命名空间域中去进行搜索呢?这里有两种可能性会去搜索命名空间域中的内容:1.我们展开了命名空间域2.指定访问命名空间我们先来看第一种(展开命名空间):如果我们不展开的话: #include<stdio.h> #include<stdlib.h> int a = 2; namespace hello { int a = 1; } //展开命名空间 //using namespace hello; int main() { int a = 0; printf("%d\n", a); return 0; }上面就是我们不对命名空间进行展开,上面代码中的3个变量a依然可以同时存在。补充一点,我们C++中常用的using namespace std;展开的是std标准库中的命名空间。现在如果我们想访问命名空间中的变量a,我们可以采用第二种方式(指定访问命名空间): #include<stdio.h> #include<stdlib.h> int a = 2; namespace hello { int a = 1; } //展开命名空间 //using namespace hello; int main() { int a = 0; printf("%d\n", a); printf("%d\n", ::a); printf("%d\n", hello::a); return 0; }上面的代码中我们虽然没有对命名空间进行展开,但是我们指定了命名空间。现在新的问题来了,如果我们把命名空间展开会发生什么呢?请看:上述代码中,我们把命名空间里的内容进行展开,展开的意思就是编译时把命名空间里的内容暴露到全局,是是否会到命名空间里面去搜索,,而此时全局域中已经有一个变量a了,所以会出现上图a不明确的问题。所以我们现在再来看namespace这个关键字发现它并不友好,它会把我们的命名空间进行展开,而我们为什么要把命名空间进行展开呢?命名空间进行展开的意义何在?命名空间就是为了防止自己的内容与其它域发生冲突而设立的。小结我们虽然可以指定指定命名空间里面的内容,但是这样不是很方便,所以有些地方有些时候我们会把命名空间进行展开,一旦我们把命名空间进行展开,就可能会出现一系列问题。比如,重定义问题、某个变量不明确等等问题。所以,我们以后不要轻易使用using namespace。现在,我们再来看开头还没有解决的问题:我们既然无法把rand定义在全局域中,那就直接把rand定义在一个命名空间域并不对这个域进行展开就好了,请看:所以我们利用命名空间就可以解决开头rand重定义的问题,但是这个问题C语言无法解决,而C++中的命名空间就可以解决。命名空间中可以定义哪些内容在命名空间中,我们可以定义很多东西,比如结构体类型、定义变量、定义函数等都是可以的。请看举例: #include<stdio.h> #include<stdlib.h> namespace hello { int rand = 10; int Add(int left, int right) { return left + right; } struct Node { struct Node* next; int data; }; } int main() { return 0; }嵌套命名空间命名空间是可以进行嵌套的,比如: //嵌套命名空间 #include<stdio.h> #include<stdlib.h> namespace N1 { int a = 0; int b = 1; int Add(int left, int right) { return left + right; } namespace N2 { int a = 1; int c = 0; int d = 0; int Sub(int left, int right) { return left - right; } } }补充一点:C++中的C++库里的所有东西都会被分装在std命名空间中。stl是C++标准库的一部分,cout等都在C++标准库中。我们如何访问嵌套命名空间呢?请看举例:我们知道不同的域可以有同名名变量,但是同一个域不能有同名变量。我们同一个命名空间如若给非要给同样的变量,这个时候嵌套命名空间就派上用场了。我们在平常最常见的C++代码大体是长这样的,请看: #include<iostream> using namespace std; int main() { return 0; }其实这样直接展开的话会有风险,我们定义的如果跟库中的重名,就报错了。所以项目里面尽量不要去进行展开。注意,展开不一定会报错。如果是日常练习的话(代码少),就可以直接展开;那如果是项目中的话,前往不要随意对命名空间进行展开。建议直接访问指定命名空间,比如:但是如果我们一个项目中有非常多cout的话,我们不可能要输入非常多次的std::,所以这里推荐一种非常实用的方式,请看:这样的话,我们就不需要把整个命名空间进行展开了,我们直接把最常用的展开就可以了。总结命名空间的存在是为了解决C语言的一个缺陷,我们知道C语言存在命名冲突的问题,为了解决命名冲突问题,于是C++就引入了命名空间的内容。命名空间是对全局域的比如变量、函数、类型就行分装,以防止之间发生冲突。再次强调命名空间解决的是全局域的冲突,局部域基本上没有命名空间的问题,局部域的话就相当于在函数里面,函数本身就是一个局部域,不同的函数之间可以定义同名的变量(因为不同的域里面可以定义同名的变量)。我们对命名空间进行搜索主要有三种方式:第一:展开命名空间;第二:直接访问命名空间;第三:访问某个。(其中第二种第三种都是通过域作用限定符来进行操作的)。同时,我们在对命名空间进行展开的时候需要格外小心,不要轻易的对命名空间进行展开。好了,以上就是C++中命名空间的内容,算是C++中的开头。就到这里吧,下次见喽各位,再见啦!!!
二叉树遍历什么是二叉树遍历:二叉树遍历就是按照某种特定的规则,依次堆二叉树中的结点进行相应的操作,并且每个结点只操作一次。访问结点所做的操作依赖于具体的应用问题。遍历是二叉树上最重要的运算之一,也是二叉树上进行其它运算的基础。我们以后看到二叉树应该这样去看待:把他看成根、左子树、右子树。二叉树的遍历有:前序、中序、后序、层序遍历的递归结构遍历:1.前序遍历(Preorder Traversal),也叫前根遍历:2.中序遍历(Inorder Traversal),也叫中根遍历:3.后序遍历(Post orderTraversal)也叫后根遍历:4.层序遍历前序遍历下面是前序遍历的动态图。以下图为例进行代码的编写:请看代码: void PreOrder(BTNode* root) { if (root == NULL) { printf("NULL "); return; } printf("%d ", root->data); PreOrder(root->left); PreOrder(root->right); }这是运行结果:中序遍历请看中序遍历的动态图:还是以下图进行举例:请看代码: //中序遍历 void InOrder(BTNode* root) { if (root == NULL) { printf("NULL "); return; } InOrder(root->left); printf("%d ", root->data); InOrder(root->right); }下面是运行结果:后序遍历下面是后序遍历的动态图:还是以下图进行举例: //后序遍历 void PostOrder(BTNode* root) { if (root == NULL) { printf("NULL "); return; } PostOrder(root->left); PostOrder(root->right); printf("%d ", root->data); }请看运行结果:前中后序总代码 #define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> #include<stdlib.h> #include<assert.h> typedef int BTDataType; typedef struct BinaryTreeNode { BTDataType data; struct BinaryTreeNode* left; struct BinaryTreeNode* right; }BTNode; //新建结点 BTNode* BuyNode(BTDataType x) { BTNode* node = (BTNode*)malloc(sizeof(BTNode)); if (node == NULL) { perror("malloc fail"); return NULL; } node->data = x; node->left = NULL; node->right = NULL; return node; } BTNode* CreatTree() { BTNode* node1 = BuyNode(1); BTNode* node2 = BuyNode(2); BTNode* node3 = BuyNode(3); BTNode* node4 = BuyNode(4); BTNode* node5 = BuyNode(5); BTNode* node6 = BuyNode(6); node1->left = node2; node1->right = node4; node2->left = node3; node4->left = node5; node4->right = node6; return node1; } //前序遍历 void PreOrder(BTNode* root) { if (root == NULL) { printf("NULL "); return; } printf("%d ", root->data); PreOrder(root->left); PreOrder(root->right); } //中序遍历 void InOrder(BTNode* root) { if (root == NULL) { printf("NULL "); return; } InOrder(root->left); printf("%d ", root->data); InOrder(root->right); } //后序遍历 void PostOrder(BTNode* root) { if (root == NULL) { printf("NULL "); return; } PostOrder(root->left); PostOrder(root->right); printf("%d ", root->data); } int main() { BTNode* root = CreatTree(); //PreOrder(root);//前序遍历 //InOrder(root);//中序遍历 //PostOrder(root);//后序遍历 return 0; }最后,其实前序、中序、后序的过程基本上就是一样的,即前序、中序、后序的递归过程是一样的,不一样的就是访问根的时机不一样。层序遍历层序遍历的话可以利用队列先进先出的特点来进行实现,出上一层,带入下一层。所以这里的层序遍历就是利用队列来实现的。注意队列中的结点里面的data存的是树结点的指针。层序遍历总代码Quene.h#pragma once #include<stdio.h> #include<stdlib.h> #include<assert.h> #include<stdbool.h> typedef struct BinaryTreeNode* QDataType; typedef struct QueueNode { struct QueueNode* next; QDataType data; }QueueNode; typedef struct Queue { QueueNode* head; QueueNode* tail; }Queue; void QueueInit(Queue* pq); void QueueDestory(Queue* pq); void QueuePush(Queue* pq,QDataType x);//插入数据 void QueuePop(Queue* pq);//删除数据 QDataType QueueFront(Queue* pq);//取队头的数据 QDataType QueueBack(Queue* pq);//取队尾的数据 int QueueSize(Queue* pq);//有多少个数据 bool QueueEmpty(Queue* pq);//判断队列是否为空 Queue.c#define _CRT_SECURE_NO_WARNINGS 1 #include"Queue.h" //初始化 void QueueInit(Queue* pq) { assert(pq); pq->head = NULL; pq->tail = NULL; } void QueueDestory(Queue* pq) { assert(pq); QueueNode* cur = pq->head; while (cur) { QueueNode* next = cur->next; free(cur); cur = cur->next; } pq->head = pq->tail = NULL; } void QueuePush(Queue* pq, QDataType x) { assert(pq); QueueNode* newnode = (QueueNode*)malloc(sizeof(QueueNode)); newnode->data = x; newnode->next = NULL; if (pq->head == NULL) { pq->head = pq->tail = newnode; } else { pq->tail->next = newnode; pq->tail = newnode; } } //删除 void QueuePop(Queue* pq) { assert(pq); assert(!QueueEmpty(pq)); //if (pq->head == NULL) //{ // return; //}//温柔的处理 QueueNode* next = pq->head->next; free(pq->head); pq->head = next; if (pq->head == NULL) { pq->tail=NULL; } } bool QueueEmpty(Queue* pq) { assert(pq); return pq->head == NULL; } QDataType QueueBack(Queue* pq)//取队尾的数据 { assert(pq); assert(!QueueEmpty(pq)); return pq->tail->data; } QDataType QueueFront(Queue* pq)//取队头的数据 { assert(pq); assert(!QueueEmpty(pq)); return pq->head->data; } int QueueSize(Queue* pq)//有多少个数据 { assert(pq); int n = 0; QueueNode* cur = pq->head; while (cur) { n++; cur = cur->next; } return n; } test.c#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> #include<stdlib.h> #include<assert.h> #include"Queue.h" typedef int BTDataType; typedef struct BinaryTreeNode { BTDataType data; struct BinaryTreeNode* left; struct BinaryTreeNode* right; }BTNode; //新建结点 BTNode* BuyNode(BTDataType x) { BTNode* node = (BTNode*)malloc(sizeof(BTNode)); if (node == NULL) { perror("malloc fail"); return NULL; } node->data = x; node->left = NULL; node->right = NULL; return node; } BTNode* CreatTree() { BTNode* node1 = BuyNode(1); BTNode* node2 = BuyNode(2); BTNode* node3 = BuyNode(3); BTNode* node4 = BuyNode(4); BTNode* node5 = BuyNode(5); BTNode* node6 = BuyNode(6); node1->left = node2; node1->right = node4; node2->left = node3; node4->left = node5; node4->right = node6; return node1; } //前序遍历 void PreOrder(BTNode* root) { if (root == NULL) { printf("NULL "); return; } printf("%d ", root->data); PreOrder(root->left); PreOrder(root->right); } //中序遍历 void InOrder(BTNode* root) { if (root == NULL) { printf("NULL "); return; } InOrder(root->left); printf("%d ", root->data); InOrder(root->right); } //后序遍历 void PostOrder(BTNode* root) { if (root == NULL) { printf("NULL "); return; } PostOrder(root->left); PostOrder(root->right); printf("%d ", root->data); } int size = 0; 求树中结点个数法1 //int TreeSize(BTNode* root,int* psize) //{ // if (root == NULL) // return; // (*psize)++; // TreeSize(root->left, psize); // TreeSize(root->right, psize); //} //求树的结点个数法2 int TreeSize(BTNode* root) { return root == NULL ? 0 : TreeSize(root->left) + TreeSize(root->right) + 1; } //求树的高度 int TreeHeight(BTNode* root) { if (root == NULL) return 0; int leftHeight = TreeHeight(root->left); int rightHeight = TreeHeight(root->right); return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1; } int TreeKLevel(BTNode* root, int k) { if (root == NULL) return 0; if (k == 1) return 1; int leftK = TreeKLevel(root->left, k - 1); int rightK = TreeKLevel(root->right, k - 1); return leftK + rightK; } //二叉树查找值为x的结点 BTNode* BinaryTreeFind(BTNode* root, BTDataType x) { if (root == NULL) return NULL; if (root->data == x) return root; BTNode* lret = BinaryTreeFind(root->left, x); if (lret) return lret; BTNode* rret = BinaryTreeFind(root->right, x); if (rret) return rret; return NULL; } //层序遍历 void LevelOrder(BTNode* root) { Queue q; QueueInit(&q); if (root) QueuePush(&q, root); while (!QueueEmpty(&q)) { BTNode* front = QueueFront(&q); //front指向的不是队列的结点,而是树的结点 QueuePop(&q); printf("%d ", front->data); if (front->left) QueuePush(&q, front->left); if (front->right) QueuePush(&q, front->right); } QueueDestory(&q); } int main() { BTNode* root = CreatTree(); //PreOrder(root);//前序遍历 //InOrder(root);//中序遍历 //PostOrder(root);//后序遍历 求树的结点法1 /*int size1 = 0; TreeSize(root, &size1); printf("TreeSize:%d\n", size1); int size2 = 0; TreeSize(root, &size2); printf("TreeSize:%d\n", size2);*/ 求树的结点法2 //printf("TreeSize:%d\n", TreeSize(root)); //printf("TreeSize:%d\n", TreeSize(root)); //printf("TreeSize:%d\n", TreeSize(root)); 求树的高度 //printf("TreeHeight:%d\n", TreeHeight(root)); //printf("TreeKLevel:%d\n", TreeKLevel(root, 1)); 二叉树查找值为x的结点 //printf("BinaryTreeFind:%p\n", BinaryTreeFind(root, 5)); //printf("BinaryTreeFind:%p\n", BinaryTreeFind(root, 0)); //层序遍历 LevelOrder(root); return 0; } 以上就是二叉树遍历的四种方式(前序、中序、后序、层序)。说实话,内容还是有些难度的,这就更需要我们认真的进行学习并复习了。加油,各位!!!
链表初始化 LTNode* ListInit(LTNode* phead) { //哨兵位头节点 phead = (LTNode*)malloc(sizeof(LTNode)); phead->next = phead; phead->prev = phead; return phead; //利用返回值的方式 }首先,我们需要一个哨兵头节点,该头节点的next和prev均指向该头节点本身,最后,返回这个头节点的地址。打印链表 void ListPrint(LTNode* phead) { assert(phead); LTNode* cur = phead->next;//从phead->开始遍历链表 while (cur != phead)//为了防止死循环,所以终止条件为cur=phead { printf("%d ", cur->data); cur = cur->next; } printf("\n"); }由于链表是双向循环链表,双向循环链表自身的结构很容易在打印时造成死循环,所以我们在打印链表时需要注意循环终止的条件,否则,程序就会陷入死循环。再次提醒,这是一个双向循环链表。当我们循环打印完链表的最后一个数据的时候,此时cur就是指向链表中最后一个节点的,而cur->next是指向链表的哨兵头节点的,所以,循环终止条件就是cur=phead。尾插 void ListPushBack(LTNode* phead, LTDateType x) { //链表为空时,依然可以处理 assert(phead); LTNode* tail = phead->prev;//找到尾节点 LTNode* newnode = (LTNode*)malloc(sizeof(LTNode)); newnode->data = x; //phead tail->next = newnode; newnode->prev = tail; newnode->next = phead; phead->prev = newnode; }尾删 //尾删 void ListPopBack(LTNode* phead) { assert(phead); assert(phead->next != phead);//当链表为空时,就表示不能再删除了 //找到尾 LTNode* tail = phead->prev; phead->prev = tail->prev; tail->prev->next = phead; //最后释放空间 free(tail); }既然是尾删,我们首先要先找到尾,即LTNode* tail = phead->prev;这样会方便很多,同时尾删的时候一定要注意**free()**的释放时机。注意一种特殊情况:当phead->next==phead的时候,此时链表为空,就不能继续删除了。所以需要加上 assert(phead->next != phead);。新建一个节点 LTNode* BuyListNode(LTDateType x) { LTNode* newnode = (LTNode*)malloc(sizeof(LTNode)); newnode->data = x; newnode->prev = NULL; newnode->next = NULL; return newnode; }该函数功能就是新建一个节点,把该节点的数据进行赋值(即newnode->data = x;),并把指针均变成空指针(newnode->prev = NULL; newnode->next = NULL;)。最后返回这个新节点的地址即可。头插 void ListPushFront(LTNode* phead, LTDateType x) { assert(phead); LTNode* newnode = BuyListNode(x); LTNode* next = phead->next; phead->next = newnode; newnode->prev = phead; newnode->next = next; next->prev = newnode; }还是看一下特殊情况,即如果链表是一个空链表,我们来简单分析一下:链表为空时phead->next就是phead本身。我们只需要处理phead、next、newnode三者之间的链接关系即可。最后发现,链表为空时依然可以进行处理。头删 void ListPopFront(LTNode* phead) { assert(phead); //链表为空就不需要头删了 LTNode* next = phead->next; LTNode* nextNext = next->next; phead->next = nextNext; nextNext->prev = phead; free(next); }链表为空时就不要进行头删操作了,故加上assert(phead);我们最好还是提前定义好next和nextNext即LTNode* next = phead->next;LTNode* nextNext = next->next;这样后面会很方便,可以减少不必要的麻烦,接下来处理phead、next、nextNext三者之间的链接关系就好了。查找查找的实现与打印的实现差不太多,提前定义一个cur指向phead的next,即LTNode* next = phead->next;循环终止条件依然是cur = phead,其它按部就班即可。 LTNode* ListFind(LTNode* phead, LTDateType x) { assert(phead); LTNode* cur = phead->next; while (cur != phead) { if (cur->data == x) { return cur; } cur = cur->next; } //没找到就返回NULL return NULL; }在pos之前插入* void ListInsert(LTNode* pos, LTDateType x) { assert(pos); LTNode* posPrev = pos->prev; LTNode* newnode = BuyListNode(x); //处理posPrev newnode pos三者之间的链接关系 posPrev->next = newnode; newnode->prev = posPrev; newnode->next = pos; pos->prev = newnode; } 一定要提前定义一个posPrev即LTNode* posPrev = pos->prev;,然后进行newnode、pos、posPrev之间的链接就好。在这里,我们还可以利用ListInsert这个函数来完成头插尾插的操作。首先,我们先利用ListInsert来完成尾插的操作。当pos是我们的哨兵位节点phead的时候,由于这个函数是在pos之前插入,所以此时就相当于尾插了(因为phead->prev就是尾)。 void ListPushBack(LTNode* phead, LTDateType x) { //链表为空时,依然可以处理 assert(phead); //LTNode* tail = phead->prev;//找到尾节点 //LTNode* newnode = (LTNode*)malloc(sizeof(LTNode)); //newnode->data = x; phead //tail->next = newnode; //newnode->prev = tail; //newnode->next = phead; //phead->prev = newnode; ListInsert(phead, x); }现在再来看头插:当phead->next和pos相等时,此时就相当于头插。 void ListPushFront(LTNode* phead, LTDateType x) { assert(phead); //LTNode* newnode = BuyListNode(x); //LTNode* next = phead->next; //phead->next = newnode; //newnode->prev = phead; //newnode->next = next; //next->prev = newnode; ListInsert(phead->next, x); }所以我们以后想要快速的写双向循环链表的时候,头插、尾插、或者任意位置的插入都可以利用ListInsert这个函数来快速的实现双向循环链表。把phead->prev传给pos就是尾插,把phead->next传给pos就变成了头删。所以双向链表只需要实现两个函数(ListInsert和ListErase)就都搞定了,这也是双向链表结构的一个优势。删除pos位置 void ListErase(LTNode* pos) { assert(pos); LTNode* posPrev = pos->prev; LTNode* posNext = pos->next; posPrev->next = posNext; posNext->prev = posPrev; free(pos); }一般来说,我们想要删除某个数据先是调用ListFind来返回一个地址,然后才调用ListErase继而把该数据删除,请看: void TestList2() { LTNode* plist = NULL; plist = ListInit(plist); ListPushFront(plist, 1); ListPushFront(plist, 2); ListPushFront(plist, 3); ListPushFront(plist, 4); ListPrint(plist); ListPushBack(plist, 1); ListPushBack(plist, 2); ListPushBack(plist, 3); ListPushBack(plist, 4); ListPrint(plist); LTNode* pos = ListFind(plist, 2); if (pos != NULL) { ListErase(pos); } ListPrint(plist); }我们可以看到运行结果成功把第一个2删除了。然而ListErase的功能不仅仅只有这些,我们还可以利用ListErase来完成头删尾删的操作。请看: //尾删 void ListPopBack(LTNode* phead) { assert(phead); assert(phead->next != phead);//当链表为空时,就表示不能再删除了 找到尾 //LTNode* tail = phead->prev; //phead->prev = tail->prev; //tail->prev->next = phead; 最后释放空间 //free(tail); ListErase(phead->prev); } //头删 void ListPopFront(LTNode* phead) { assert(phead); 链表为空就不需要头删了 //LTNode* next = phead->next; //LTNode* nextNext = next->next; //phead->next = nextNext; //nextNext->prev = phead; //free(next); ListErase(phead->next); }现在我们来测试一下: void TestList2() { LTNode* plist = NULL; plist = ListInit(plist); ListPushFront(plist, 1); ListPushFront(plist, 2); ListPushFront(plist, 3); ListPushFront(plist, 4); ListPrint(plist); ListPushBack(plist, 1); ListPushBack(plist, 2); ListPushBack(plist, 3); ListPushBack(plist, 4); ListPrint(plist); LTNode* pos = ListFind(plist, 2); if (pos != NULL) { ListErase(pos); } ListPrint(plist); ListPopBack(plist); ListPopBack(plist); ListPopFront(plist); ListPopFront(plist); ListPrint(plist); } 销毁链表最后,我们再来实现一下销毁链表。 //销毁链表 void ListDestroy(LTNode* phead) { assert(phead); LTNode* cur = phead->next; while (cur != phead) { LTNode* next = cur->next; free(cur); cur = next; } free(phead); //想要把phead置为空,需要再函数外部进行置空,当然如果传二级指针也可以在函数内部把 //phead置为空,不过因为我们这个双向链表都是传的一级指针,所以为了保持接口一致性, //我们在函数外部把phead置为空即可 } 以上就是双向循环链表所以接口函数的实现。总代码test.c#include"List.h" void TestList1() { LTNode* plist = NULL;//初始化 plist = ListInit(plist); ListPushBack(plist, 1); ListPushBack(plist, 2); ListPushBack(plist, 3); ListPushBack(plist, 4); ListPrint(plist); ListPushFront(plist, 1); ListPushFront(plist, 2); ListPushFront(plist, 3); ListPushFront(plist, 4); ListPrint(plist); } void TestList2() { LTNode* plist = NULL; plist = ListInit(plist); ListPushFront(plist, 1); ListPushFront(plist, 2); ListPushFront(plist, 3); ListPushFront(plist, 4); ListPrint(plist); ListPushBack(plist, 1); ListPushBack(plist, 2); ListPushBack(plist, 3); ListPushBack(plist, 4); ListPrint(plist); LTNode* pos = ListFind(plist, 2); if (pos != NULL) { ListErase(pos); } ListPrint(plist); ListPopBack(plist); ListPopBack(plist); ListPopFront(plist); ListPopFront(plist); ListPrint(plist); ListDestroy(plist); plist = NULL; } int main() { //TestList1(); TestList2(); return 0; } list.h#pragma once #include<stdio.h> #include<stdlib.h> #include<assert.h> typedef int LTDateType; typedef struct ListNode { LTDateType data; struct ListNode* next; struct ListNode* prev; }LTNode; LTNode* ListInit(LTNode* phead);//初始化 void ListPrint(LTNode* phead); //打印链表 //尾插 void ListPushBack(LTNode* phead, LTDateType x); //尾删 void ListPopBack(LTNode* phead); //头插 void ListPushFront(LTNode* phead, LTDateType x); //头删 void ListPopFront(LTNode* phead); //创建新节点 LTNode* BuyListNode(LTDateType x); //查找 LTNode* ListFind(LTNode* phead, LTDateType x); //pos位置之前插入 void ListInsert(LTNode* pos, LTDateType x); //删除pos位置 void ListErase(LTNode* pos); //销毁聊表 void ListDestroy(LTNode* phead); list.c#include"List.h" //初始化 LTNode* ListInit(LTNode* phead) { //哨兵位头节点 phead = (LTNode*)malloc(sizeof(LTNode)); phead->next = phead; phead->prev = phead; return phead; //利用返回值的方式 } void ListPushBack(LTNode* phead, LTDateType x) { //链表为空时,依然可以处理 assert(phead); //LTNode* tail = phead->prev;//找到尾节点 //LTNode* newnode = (LTNode*)malloc(sizeof(LTNode)); //newnode->data = x; phead //tail->next = newnode; //newnode->prev = tail; //newnode->next = phead; //phead->prev = newnode; ListInsert(phead, x); } void ListPrint(LTNode* phead) { assert(phead); LTNode* cur = phead->next;//从phead->开始遍历链表 while (cur != phead)//为了防止死循环,所以终止条件为cur=phead { printf("%d ", cur->data); cur = cur->next; } printf("\n"); } //尾删 void ListPopBack(LTNode* phead) { assert(phead); assert(phead->next != phead);//当链表为空时,就表示不能再删除了 找到尾 //LTNode* tail = phead->prev; //phead->prev = tail->prev; //tail->prev->next = phead; 最后释放空间 //free(tail); ListErase(phead->prev); } LTNode* BuyListNode(LTDateType x) { LTNode* newnode = (LTNode*)malloc(sizeof(LTNode)); newnode->data = x; newnode->prev = NULL; newnode->next = NULL; return newnode; } void ListPushFront(LTNode* phead, LTDateType x) { assert(phead); //LTNode* newnode = BuyListNode(x); //LTNode* next = phead->next; //phead->next = newnode; //newnode->prev = phead; //newnode->next = next; //next->prev = newnode; ListInsert(phead->next, x); } //头删 void ListPopFront(LTNode* phead) { assert(phead); 链表为空就不需要头删了 //LTNode* next = phead->next; //LTNode* nextNext = next->next; //phead->next = nextNext; //nextNext->prev = phead; //free(next); ListErase(phead->next); } //查找 LTNode* ListFind(LTNode* phead, LTDateType x) { assert(phead); LTNode* cur = phead->next; while (cur != phead) { if (cur->data == x) { return cur; } cur = cur->next; } //没找到就返回NULL return NULL; } //pos位置之前插入 void ListInsert(LTNode* pos, LTDateType x) { assert(pos); LTNode* posPrev = pos->prev; LTNode* newnode = BuyListNode(x); //处理posPrev newnode pos三者之间的链接关系 posPrev->next = newnode; newnode->prev = posPrev; newnode->next = pos; pos->prev = newnode; } //删除pos位置 void ListErase(LTNode* pos) { assert(pos); LTNode* posPrev = pos->prev; LTNode* posNext = pos->next; posPrev->next = posNext; posNext->prev = posPrev; free(pos); } //销毁链表 void ListDestroy(LTNode* phead) { assert(phead); LTNode* cur = phead->next; while (cur != phead) { LTNode* next = cur->next; free(cur); cur = next; } free(phead); //想要把phead置为空,需要再函数外部进行置空,当然如果传二级指针也可以在函数内部把 //phead置为空,不过因为我们这个双向链表都是传的一级指针,所以为了保持接口一致性, //我们在函数外部把phead置为空即可 } 好了,双向循环链表的实现就到这里了,其实在这里面最重要的两个接口函数就是ListErase和ListInser,这两个函数可以帮助我们快速的实现这个链表,剩余的就是一些边边角角的问题了。这块的内容还是要多画图,来帮助我们更好的理解。
2023年05月