今天我们开始数据结构的学习,当然,这个有些概念是十分抽象的,只看文章是不一定能懂的,或者说会耗费不少的时间。所以我会持续在B站上面更新讲解视频,都是自己的一些理解和想法。会拿出来和大家一起分享,都是免费的。原创不易,希望大家可以三连支持一下,也希望能给大家带来进步。
博客地址:
喜欢吃豆的博客_CSDN博客-C++笔记,C语言笔记,C/C++课外闲谈领域博主
https://blog.csdn.net/m0_63309778?spm=1000.2115.3001.5343
喜欢吃豆 的个人主页 - 动态 - 掘金 (juejin.cn)
https://juejin.cn/user/251108556018989
仓库地址:
喜欢吃豆 (i-like-beans) - Gitee.com
https://gitee.com/i-like-beans
B站视频地址:
忘了吃豆的个人空间_哔哩哔哩_bilibili
https://space.bilibili.com/1032401779?spm_id_from=333.1007.0.0
谢谢大家的支持了。
大家学习这个数据结构之前需要有一本数据结构(第二版C语言)书是最好的。
一,引子
首先,我们先考虑一下,什么是数据结构?我们既然要学习数据结构,那我们就应该知道,数据结构是什么呢?
其实这个问题至今仍然没有一个官方的解答,但是有很多大牛都给出了自己的见解。
有多少人可以看懂?在这里,所有的老师都会选择使用例子来让大家更好的理解。
一,例一:
书上给的例子是,如何在书架上面摆放图书?
大家可能会有各种各样的想法,因为很少有事情是绝对的,很多事情都有多种解决方法的。
首先,我们来看一看这个问题,这个问题从本质上来说就是不科学的。
准确来说是不够严谨的,我并没有规定书的规模,还有书架的款式。
那如果我们不那么严谨,我们来考虑以下,有什么样的解决方法?
1,随便放,见缝插针。但是找书的时候会很痛苦,你甚至都不能确定你是否有这本书
2,按书名的拼音的首字符来排序,但是如果这样的话我如果要插入新书,就会对后面所有的书本进行移动
3,把书架划分为几块区域,每块区域指定摆放某种类别的图书,在每种类别内,按照书名的拼音字母顺序排放。但是我们并不知道每一类书有多少本,需要多大的一个区域来存放这些书,会造成空间的一个浪费。
所以我们可以知道解决问题方法的效率,跟数据的组织方式有关。
二,例二:
写程序实现一个函数PrintN,使得传入一个正整数为N的参数后,能顺序打印从1~N的所有整数。
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> //打印从1~N的全部整数 void PrintN(int N) { int i; for (i = 1; i <= N; i++) { printf("%d\n", i); } } //递归实现 void PrintN(int N) { if (N > 0) { PrintN(N - 1); printf("%d\n", N); } } int main() { int N; scanf("%d", &N); PrintN(N); return 0; }
这里我给出了 两个版本,一个是用循环语句实现,还有一个是以递归的形式实现,似乎两种方法都可以完成任务。
我们在测试时可以发现,当N充分大的时候,递归实现居然拒绝工作了,这是为什么呢?
我们就要来考虑递归的内部实现了,我们知道,调用函数时,会开辟一块空间让他存储变量,但是递归中有N个函数的调用,它的函数调用不执行完,之前的函数调用也不会被释放,如果调用的足够多,占有的空间也会足够多,直到将内存空间全部占用后,有可能函数的调用还是没有结束,那么就可能会导致程序崩溃,因为内存中没有足够的空间提供让程序进行计算。
所以我们知道,解决问题方法的效率,跟空间利用效率有关。
三,例三:
最直接的方法就是暴力解法,根据多项式的标准表达式通过循环累计求和来实现这个函数。
#include<stdio.h> #include<math.h> //暴力 double f(int n, double a[], double x) { int i; double p = a[0]; for (i = 1; i <= n; i++) { p += a[i] * pow(x, i); } return p; } //秦九韶解法 double f(int n, double a[], double x) { int i; double p = a[n]; for (i = n; i > 0; i++) { p = a[i - 1] + x * p; } return p; }
这就是秦九韶公式。
我们看上面的两个函数看起来都差不多的样子,但实际上呢?
这里就需要我们学习一个clock工具了。
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> #include<time.h> clock_t start, stop;//clock_t是clock函数返回值的类型 double duration;//记录被测函数运行时间,以秒为单位 int main() { //不在测试范围内的准备工作写在clock函数调用之前 start = clock();//开始计时 MyFunction();//这里写入想测的函数 stop = clock();//停止计时 duration = ((double)(stop - start))/CLK_TCK;//计算运行时间 //CLK_TCK是机器时钟每秒所走的时钟打点数 //在某些IDE下也可能叫做CLOCKS_PER_SEC //其他不在测试范围的处理写在后面。 return 0; }
所以我们知道,解决问题方法的效率,跟算法的巧妙程度有关。
通过上面的三个例子,我们知道了,解决问题方法的效率与其相关的因素,那这些因素的定义与什么有关呢?、
好,我们现在正式开始数据结构的一个整体大概的介绍。
什么是数据结构?
1,数据对象在计算机中的组织方式,
1)逻辑结构(线性or非线性)
2)物理存储结构(数组还是链表)
2,数据对象必定与一系列加在其上的操作相关联
3,完成这些操作所用的方法就是算法
我们讨论数据对象的各种不同的组织方式,是为了得到处理这些数据对象的最高效的算法(依需求而定)。所以我们在讨论数据结构这个概念时,关心的不仅仅是数据对象本身以及他们在计算机中的组织方式,还要关心与他们相关联的一个操作集,以及实现这些操作的最高效的算法。
四,抽象数据类型
抽象数据类型是一种对“数据类型”的描述,这种描述是抽象的
数据类型:数据对象集,与数据相关的操作集
抽象:我们描述数据类型是不依赖于具体实现的,即数据对象集和操作集的描述与存放数据的机器无关,与数据存储的物理结构无关,与实现操作的算法和编程语言无关。
总的来说,只描述数据对象集和相关操作集“是什么”并不涉及“如何做到”的问题。
我们还要重点讲一讲抽象,抽象是计算机求解问题的基本方式和重要手段,它使得一种设计可以应用于多种场景。而且可以通过抽象屏蔽掉底层的细节,使得设计更加简单,理解更加方便。
二,算法
算法的设计是一门艺术,解决同一个问题,一般有多种算法,但漂亮的算法与其他算法相比往往有天壤之别。
1,什么是算法?
1)一个有限的指令集
2)接受一些输入(也可以不接受)
3)产出输出(不然没有意义)
4)一定在有限步骤之后终止
5)每一条指令必须有充分明确的目标,不可以有歧义。必须在计算机能处理的范围之内。且描述应不依赖于任何一种计算机语言以及具体实现手段。
2.算法复杂度
什么是好的算法?
比较算法优劣的指标:
1)空间复杂度S(n):根据算法写成的程序在执行时占用存储单元的长度
若过高,会导致内存超出限制,造成程序的非正常中断。
2)时间复杂度T(n):根据算法写成的程序在执行时耗费的时间的长度。
3,在分析一般算法的效率时,我们经常会关注下面的两种复杂度:
对最坏情况复杂度的分析会更容易,因为很多时候平均并不是一件简单的事。
4,渐近表示法:
当n足够大的时候,一些常数或者说一些低阶次的变量的影响就不会那么大了,所以我们一般用渐近表示法。
可能大家对不同的空间复杂度和时间复杂度没有一个很明确的认识,给大家看看这三个表就知道了。
告诉大家一些对给定算法做渐进分析时的小窍门:
三,应用实例:最大子列和问题
我们这里将会让大家更加清楚的认识到算法效率的差别,加深大家对算法的理解
1,穷举所有子列和,从中找出最大值
int MaxSubseqsuml(int list[],int N) { int i, j, k; int ThisSum, MaxSum = 0; for (i = 0; i < N; i++) {//i是子列左端的位置 for (j = i; j < N; j++) {//j是子列右端的位置 ThisSum = 0;//ThisSum是i到j的子列和 for (k = i; k <= j; k++) { ThisSum += list[k]; } if (ThisSum > MaxSum) MaxSum = ThisSum; } } return MaxSum; }
2,少用一个for循环
int MaxSubseqsuml(int list[], int N) { int i, j, k; int ThisSum, MaxSum = 0; for (i = 0; i < N; i++) {//i是子列左端的位置 for (j = i; j < N; j++) {//j是子列右端的位置 ThisSum += list[j]; if (ThisSum > MaxSum) MaxSum = ThisSum; } } return MaxSum; }
3,分而治之
顾名思义,分而治之的基本思路就是将原问题拆分为若干个小问题,分别解决后在将结果和而治之
,用递归实现十分方便。
#define _CRT_SECURE_NO_WARNINGS 1 int Max3(int a, int b, int c) { return a > b ? a > c ? a : c : b > c ? b : c; } int DivideAndConquer(int List[], int left, int right) { //分治法求List[left]到List[right]的最小子列和 int MaxLeftSum, MaxRightSum;//存放左右子问题的解 int MaxLeftBorderSum, MaxRightBoderSum;//存放跨分界线的结果 int LeftBorderSum, RightBorderSum; int center, i; if (left == right) {//递归的结束条件,子列只有一个数字 if (List[left] > 0) return List[left]; else return 0; } //下面是分的过程 //找到中分点 center = (left + right) / 2; //递归求两边子列的最大和 MaxLeftSum = DivideAndConquer(List, left, center); MaxRightBoderSum = DivideAndConquer(List, center + 1, right); //下面求跨分界线最大子列和 MaxLeftBorderSum = 0; LeftBorderSum = 0; for (i = center; i >= left; i--) { LeftBorderSum += List[i]; if (LeftBorderSum > MaxLeftBorderSum) MaxLeftBorderSum = LeftBorderSum; } //左边扫描结束 MaxRightBoderSum = 0; RightBorderSum = 0; for (i = center + 1; i <= right; i++) { RightBorderSum += List[i]; if (RightBorderSum > MaxRightBoderSum) MaxRightBoderSum = RightBorderSum; } //右边扫描结束 //最后进行比较 return Max3(MaxLeftSum, MaxRightSum, MaxLeftBorderSum + MaxRightBoderSum); } int MaxSubseqsum3(int List[],int N) { return DivideAndConquer(List, 0, N - 1); }
4,在线处理
“在线”的意思是指每输入一个数据就进行即时处理,得到的结果是对于当前已经读入的所有数据都成立的解,即在任何地方中止输入,算法都能正确给出正确的解。
int MaxSubseqSum4(int List[], int N) { int i; int ThisSum, MaxSum; ThisSum = MaxSum = 0; for (i = 0; i < N; i++) { ThisSum += List[i]; if (ThisSum > MaxSum) MaxSum = ThisSum; else if (ThisSum < 0) ThisSum = 0; } return MaxSum; }
这种算法并不要求存储序列中的数据,只需要一个一个读入,同时一个一个处理即可,处理过的数据没必要存储,可以说是最快的算法了。