1、数据结构是什么?
数据结构+算法=程序.
数据结构(Data Structure):是计算机存储、组织数据的方式,指相互之间存在一种或多种特定关系的数据元素的集合。
例如后面会提到的顺序表,链表这些线性数据结构,还有后面的二叉树树形数据结构等.
算法(Algorithm):就是定义良好的计算过程,他取一个或一组的值为输入,并产生出一个或一组值作为输出。简单来说算法就是一系列的计算步骤,用来将输入数据转化成输出结果.
例如:排序算法.
数据结构与算法对于一个程序员是很重要的,不论对你思考问题的方式还是对你编程的思维都会有很大的好处。同时在找工作时算法也是一个重要考点之一.
2、数据结构应该怎么学呢?
1.多多练习代码.
数据结构的学习并不简单,需要多锻炼代码能力,最怕偷懒,很多时候头脑虽然理解了,但是动起手来写代码会忽略很多细节,导致程序出错,不能光有思路,而代码能力却实现不了就很尴尬了.
2.多画图(这个强烈推荐)
除了代码能力需要锻炼以外,很重要的一点是要有思路,通过画图辅助,可以很好地帮助我们找到思路和理解数据结构中的很多思想,切忌上来就开始码代码,这样对于简单的问题可能可以解决,但是对于稍微复杂的问题可能会让你头痛(大佬除外😂😂),很容易被绕进去,陷入痛苦的调试找bug环节.
画图会让提供给我们清晰的思路,同时,即使出现了bug,也可以很快的找到,清晰可见.写代码只是用于实现思路,思路清晰,代码写起来并不困难.
3.刷题
刷题会锻炼我们的思考能力,解题是一种很灵活的事情.一方面可以巩固我们学的基础知识,另一方面可以拓展思维.
最后,坚持学习才是最重要的.
算法效率如何衡量?
对于一个问题,可以有很多解法,那怎样衡量一个算法的好坏呢?
比谁的代码更简洁吗?
算法的效率主要考虑两点:1.时间复杂度. 2.空间复杂度
一个算法在编译生成可执行文件后,运行时会耗费时间资源和空间(内存)资源 。
从时间和空间两个维度来衡量一个算法的好坏是比较合理的,这就是时间复杂度和空间复杂度。
时间复杂度主要衡量一个算法的运行快慢,而空间复杂度主要衡量一个算法运行所需要的额外空间。在计算机发展的早期,计算机的存储容量很小。所以对空间复杂度很是在乎。但是经过计算机行业的迅速发展,计算机的存储容量已经达到了很高的程度。所以我们如今已经不需要再特别关注一个算法的空间复杂度。
一、 时间复杂度
时间复杂度的定义:在计算机科学中,算法的时间复杂度是一个函数,它定量描述了该算法的运行时间。一个算法执行所耗费的时间.
但是从理论上说,这个只有将代码进行测试,并统计时间才能知道.并不能通过计算得到.
但对于每一个算法,我们都去跑一下,这未免显得有些麻烦,我们可以通过算法中的代码估计运行大概的时间,看看属于哪一个量级来衡量它的效率.
算法中的基本操作的执行次数,为算法的时间复杂度。
即:找到某条基本语句与问题规模N之间的数学表达式,就是算出了该算法的时间复杂度
理论不是很理解的话,我们来点实际的,找几段代码算算吧!
🌰小试牛刀
你能算出在test1中++count语句最终被执行了多少次吗?
void test1(int N) { int count = 0; //1 for (int i = 0; i < N; i++) { for (int j = 0; j < N; j++) { ++count; } } //2 for (int k = 1; k < N; k*=2) { ++count; } //3 for (int k = 0; k < 2 * N; k++) { ++count; } //4 int a = 100; while (a--) { ++count; } printf("%d\n", count); }
答案:
1: N * N
2: log2 N
3. 2*N
4. 100
则我们可以抽象出这样的数学公式:
test(N)=N2 +log2 N+2N+100
大O的渐进表示法
计算机的运行速度是很快的,对于时间复杂度的计算,没有必要追求那么精确,对于那些对结果影响不大的项,我们可以忽略不计.如果我们只保留N2这一起决定因素的项.
大O阶方法计算方法:
1、用常数1取代运行时间中的所有加法常数。
2、在修改后的运行次数函数中,只保留最高阶项。
3、如果最高阶项存在且不是1,则去除与这个项目相乘的常数。得到的结果就是大O阶。
使用大O的渐进表示法以后,test1的时间复杂度为:
(O)N ^ 2
即使是100N系数也应当去掉,因为当数据足够大的时候100的影响并不大.
只要是常数,都应当是1.
也许你会认为100很大或者100000很大.但是,要看和谁比,如果是和10亿比呢?一万亿比呢?
那我们打个比方:
你觉得你们学校大吗?还行.
你所在的城市大吗?算大吧!
你觉得我们的祖国大吗?地大物博,确实大.
但是,与太阳系相比呢?与银河系相比呢?这就显得很渺小了,沧海之一粟罢了.
所以当数据量足够大的时候,常数项和那些影响不大的忽略不计.
时间复杂度的练习:
1.1 常见的时间复杂度:
例1:
// 计算Test2的时间复杂度? void Test2(int N) { int count = 0; for (int i = 0; i < 2 * N ; i++) { ++count; } int M = 10; while (M--) { ++count; } printf("%d\n", count); }
例2:
// 计算Test3的时间复杂度? void Test3(int N, int M) { int count = 0; for (int i = 0; k < M; i++) { ++count; } for (int i = 0; i < N ; i++) { ++count; } printf("%d\n", count); }
例3:
// 计算Test4的时间复杂度? void Test4(int N) { int count = 0; for (int i = 0; i < 10000; i++) { ++count; } printf("%d\n", count); }
答案:
这三个例子的时间复杂度还是很好计算的.
例1:
2N+10用大O表示法表示时间复杂度为O(N).
例2:
基本操作执行了M+N次,有两个未知数M和N,时间复杂度为 O(N+M)
如果m和n相等则可以表示为O(N),如果一方远大于另一方,则可以用大的一方表示,记住是远大于,即不在一个量级.
例3:
基本操作语句被执行了常数次.这用大O表示法表示时间复杂度为O(1).
那试着分析TargetNum函数的时间复杂度.
TargetNum函数是用于在一个数组中查找目标值的函数,找到了就返回目标值的地址,没找到就返回NULL.
int* TargetNum(int* arr,int n, int num) { for (int i = 0; i < n; i++) { if (arr[i] == num)//找到目标数字则返回数字的地址 { return arr + i; } } return NULL; } int main() { int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; int sz = sizeof(arr) / sizeof(arr[0]); int num = 0; scanf("%d", &num); int*ret =TargetNum(arr, sz, num); if (ret == NULL) { printf("该目标值不存在"); } printf("%d ", *(ret)); if (ret + 1 <= &arr[sz - 1]) { printf("%d ", *(ret + 1)); } }
此时就让人有些疑惑了,这个函数的时间复杂度似乎不是固定的.在1~n的范围之间的那如何确定它的时间复杂度呢?
分析一下:
最好情况时(第一个数就是目标值): O(1).
平均情况时:O(n/2).
最坏情况时:O(n).
那我们选择哪种情况比较合理呢?
那我们讲一个小故事吧.
假如你是一名高中生,你刚经历期末考试,晚上,老师只公布了部分答案.
你可以确保自己可以拿到60分,有剩余的40分中,你按照以往的每次考试的经验来看,不出意外20分是可以拿到的.
此时回到家中,老爸问你能考多少分?考多少分老爸就奖励你多少钱,嘿嘿.🍭🍭🍭
你会说80(平均情况)分吗?还是会选择100分(最好情况)呢?
万一食言了呢?
咱一般都会选择最坏的情况,那样即使出现了意外,我们也没有说错,而不出意外时,无论是平均还是最好情况,我们都会比较高兴的.
这里牛牛也想告诉大家,结局未定之前,不要过分高看自己,降低期望,当然也不要自卑,继续努力,继续前行,保持对生活的热爱,生活也会拥抱你的!🍭🍭🍭
回到正题,此时我们会选择最坏的情况作为时间复杂度,即TargetNum函数的时间复杂度是O(n).
1.2 冒泡排序的时间复杂度
大家还记得c语言时学的冒泡排序吗?
// 计算BubbleSort的时间复杂度? void BubbleSort(int* a, int n) { assert(a); for (size_t end = n; end > 0; --end) { int exchange = 0; for (size_t i = 1; i < end; ++i) { if (a[i-1] > a[i]) { Swap(&a[i-1], &a[i]); exchange = 1; } } if (exchange == 0) break; } }
那么最好情况和最坏情况时的时间复杂度分别是多少呢?
对于冒泡排序不熟悉的友友们可能会以为最好的情况时O(0)或者O(1).
首先呢,没有O(0)这一说法,这几乎不可能,其次这里最好的情况也不是O(1),为什么呢?
如果数组有序,那不就不需要排序吗?那不就是O(1)吗?
其实,即使数组有序,我们也需要循环遍历一遍这个数组,才能知道有序,计算机不是人哦,他不能看一眼就知道有序,而且就算是人,当数据量比较大的时候人一眼也看不出来是否有序吧!
总结:
最好情况:O(N)
最坏情况:O(N2)
1.3 "二分查找"的时间复杂度
int BinarySearch(int* a, int n, int x) { assert(a); int begin = 0; int end = n-1; while (begin <= end) { int mid = (begin+end)/2;//找到中间值 if (a[mid] < x)//如果该值比中间值大,则直接从中间值的后半部分里面找 { begin = mid+1; } else if (a[mid] > x)//如果该值比中间值小,则直接从中间值的前半部分里面找 { end = mid-1; } else//找到了 return mid; } //没找到返回-1,这里设置为-1也许有些不合理,可以使用逻辑值. return -1; }
判断一次(与中间值比较),就可以去掉一半的值.即该算法一次N的值就会等于N/2.
不难得出该算法的时间复杂度是O(logn2).
补充知识:logn2经常省略写成log2甚至lg2
"二分查找"看起来平平无奇,但其实是个隐藏的大佬啊!
大家知道这些量级的差距有多大吗?log2是很可怕的量级,速度极快.
看图感受一下吧!
友友们感受到二分查找的厉害了吧!
遗憾的是,二分查找的前提是数据得是有序的,否则他无法实现一次排除一半.而数据往往是无序的,并且有些特殊的数据还不允许排序,排序会破坏数据的.
这也就让二分查找无计可施了,纸老虎罢了.😂😂😂
1.4 递归的时间复杂度:
// 计算阶乘递归Fac的时间复杂度? int Fac(int N) { if(0 == N)//递归的结束条件 { return 1; } return Fac(N-1)*N; }
递归的时间复杂度计算主要是根据其递归的层数来决定.
该算法每次递归N就-1,则递归的次数为N.
故算法的时间复杂度为:O(N).
// 计算斐波那契递归Fib的时间复杂度? int Fib(int N) { if(N < 3) return 1; return Fib(N-1) + Fib(N-2); }
此算法,每次经过递归都需要再递归2 * N次.
则此算法的时间复杂度为O(2n).
常见量级的比较图
大O的渐进表示法:
二、空间复杂度
空间复杂度并不是重点,现如今,一般情况下我们的时间的价值比空间的价值要高的多.
定义:
空间复杂度也是一个数学表达式,是对一个算法在运行过程中临时占用存储空间大小的量度 。
空间复杂度不是程序占用了多少bytes的空间,因为这个也没太大意义,所以空间复杂度算的是变量的个数。
空间复杂度计算规则基本跟实践复杂度类似,也使用大O渐进表示法。
注意:函数运行时所需要的栈空间(存储参数、局部变量、一些寄存器信息等)在编译期间已经确定好了,因此空间复杂度主要通过函数在运行时候显式申请的额外空间来确定
例题:
还是拿冒泡排序来举例吧!
void BubbleSort(int* a, int n) { assert(a); for (size_t end = n; end > 0; --end) { int exchange = 0; for (size_t i = 1; i < end; ++i) { if (a[i-1] > a[i]) { Swap(&a[i-1], &a[i]); exchange = 1; } } if (exchange == 0) break; } }
为了实现冒泡排序我们定义了int exchange = 0;以及Swap(&a[i-1], &a[i]);函数中的int tmp.
这项常数个临时变量,故空间复杂度为O(1).
例2:
int Fac(int N) { if(0 == N)//递归的结束条件 { return 1; } return Fac(N-1)*N; }
同样递归的空间复杂度由开辟的栈帧个数(每次递归开辟一次栈帧)决定,开辟了N个栈帧,每个栈帧使用了常数个空间。空间复杂度为O(N).
好了,数据结构的初步认识就到这里啦!后续牛牛会继续更新数据结构的相关知识.