通过前几期的介绍,我们对C语言已经有了大体的认识。但可能在一些稍微复杂的问题处理方面处理的不是很好,例如对数据的插入、删除等操作该如何选择合适的方式进行,如何做才能效率高呢?我们目前好像没有考虑过!从本期开始小编将在本专栏分享自己学习数据结构和算法的学习笔记以及心得!希望对诸君有用~!
什么是数据结构?
什么是算法?
为什么要学数据结构和算法?
如何学好数据结构和算法?
算法效率
时间复杂度详解
空间复杂度详解
前言
本期内容介绍
一、数据结构和算法介绍
1.1什么是数据结构?
1.2什么是算法?
1.3为什么要学数据结构和算法?
1.4如何学好数据结构和算法?
二、算法效率
2.1时间复杂度详解
2.1.1大O渐进表示法
2.1.2常见的时间复杂度例题分析
2.2空间复杂度详解
2.2.1常见的空间复杂度例题分析
2.3常见的算法复杂度对比
数据结构即,Data Structure 是计算机在内存中存储和组织数据的方式!指相互之间存在一种或多种特定的关系的数据元素的集合!也就是说数据结构是一种让计算机更好地在内存中管理数据的方式~!数据结构一般有三要素:逻辑结构、物理结构、运算~!我们后面会详解!
算法即,Algorithm 就是定义良好的计算过程,输入一个或一组值,会输出一个或一组值。也就是说算法是一系列的计算步骤,用来将输入数据转换为输出结果~!
1、面试和笔试的时候会考和问~!
2、如果考研是的小伙伴基本上数据结构必考~!
3、现在一般笔试都在线上,人家一般会先让你做3~5道算法题,过了面试否则凉凉~!
4、未来工作会用到~!
总结下来就三点:多写代码、多画图、多思考!
大概代码写到这种程度就算入门了~!哈哈!
我们平时经常听说," 你这个代码的没有我的效率高~!我的算法比较好~!" 等词,他们说的好坏以及效率是指什么?如何来评估一个程序或算法的好坏呢?其实评估一个代码的好坏我们无非会从两方面进行比较:速度和所占空间的大小~!描述他们的概念叫复杂度~!我们前面也说过,时间换空间和空间换时间的词~!那时间和空间该如何体现以及如何评估一个程序或算法的时间和空间呢?这就要引入两个概念:时间复杂度和空间复杂度~!
时间复杂度是来衡量一个算法的运行快慢的方式~!在计算机科学中,时间复杂度是一个函数(大O函数),他定量的描述了一个程序或算法的运行时间。这里你肯定会想这有啥难的!算法运行的快慢,我掐个表不就知道了吗还整个函数?这样想你可就有点天真了!一个程序或算法的具体的执行时间在不同的环境下是不一样的!你用C/C++在Win11上写的代码,拿到以前16位的机器上或单片机上的运行的时间肯定是不一样的!!!你能用简单的掐表来定性的说这个算法不行吗?不行吧!所以时间复杂度不是通来掐表的~!这里说了时间复杂度是一个函数是什么意思呢?假设一个程序的问题规模是n,那他的执行次数是F(n),随着n的增长我们发现算法的运行时间与其执行次数F(n)成正比!此时我们对这个函数F取极限后的结果就约等于该算法的运行时间~算法导论中把这种极限用O渐进表示法表示~(O是上界,最坏的情况)!举个栗子:假设一个算法的规模为是n,它的执行次数是F(n),它的时间复杂度就是O(F(n)) !说了半天就是:你找到一个语句与问题规模n之间的数学表达式,你就算出了时间复杂度~!
OK!总结一下:
时间复杂度是衡量一个算法快慢的方式,他是一个函数O,定量描述算法的运行时间。由于问题规模N增大时,算法的运行时间和算法的执行次数差别很小,所以用O渐进表示法来表示时间复杂度(大概的执行次数),即,算出一个算法的大概执行次数就算出了它的时间复杂度~!
这里说到了O渐进表示法!什么是大O渐进表示法呢?
大O符号(Big O notation ):是用来描述函数的渐进行为的数学函数!
推导大O阶法:
1、用常数1来表示运行常数次(运行次数固定的)
2、在可变的运行次数的函数表达式中,只保留最高阶的那一项
3、如果最高阶项的前面的项数不是1,则直接忽略项数
这个思路就和求极限的那个抓大头很像~!
OK我们来个栗子看看:
void test(int M)
{
int count = 0;
for (int i = 0; i < M; i++)
{
for (int j = 0; j < M; j++)
{
count++;
}
}
for (int i = 0; i < 2 * M; i++)
{
printf("hehe\n");
}
int N = 10, i = 0;
while (N--)
{
printf("%d ", i++);
}
}
这就是用大O表示将法求时间复杂度~!其实算法的复杂度一般包含最好、最坏、平均。
最好情况:任意规模的输入最小的运行次数(下界)
最坏情况:任意规模的输入最大的运行次数(上界)
平均情况:任意规模的输入中间的运行次数
我们一般所说的复杂度为最坏情况~!
后面的与这个一样,我就不这样慢慢的计算分析了~!我们就直接说大概了~!
void fun1(int n)
{
for (int i = 0; i < n; i++)
{
printf("hehe\n");
}
}
这个代码的时间复杂度是多少?我们上面说过计算出该算法的问题规模n的基本执行次数就是该算法的时间复杂度!这个代码的问题规模就是n!它的执行次数是0~n-1,n次执行,所以代码的时间复杂度是O(N);
void fun(int n)
{
int count = 0;
for (int i = 0; i < n; i++)
{
for (int j = 0; j < n; j++)
{
count++;
}
}
}
这个代码的问题规模就是统计两个循环下的次数。他会执行多少次呢?内存循环走完一遍是n次, 外层循环每一个i就走n次有n个,所以会执行n^2次,所以它的时间复杂度即使O(N^2);
void func2(int n)
{
for (int i = 0; i < n; i++)
{
for (int j = 0; j < n; j++)
{
printf("haha\n");
}
}
int count = 0;
for (int i = 0; i < 2 * n; i++)
{
count++;
}
}
这个代码的规模是两部分,前一部分是刚刚计算好的n^2,另一部分是,统计0~2n的个数~,它的执行次数是2*n所以它的总执行次数是n^2 + 2*n,我们前面说过只需要大概执行次数,即在n增大的情况下,后面的2*n可以忽略~!所以这个代码的时间复杂度是O(N^2);
void fun3(int M, int N)
{
for (int i = 0; i < M; i++)
{
printf("hehe\n");
}
int count = 0;
for (int i = 0; i < N; i++)
{
count++;
}
}
这个代码还是分为两部分,前部分是执行M次,后半是执行N次。所起执行总次数是:M + N ,由于M和N的大小不知道,所以时间复杂度得分情况讨论~!当M = N 时总执行次数:2*N即O(N),当N != M时总执行次数:N+M所以时间复杂度为:O(N+M)
void func4(int N)
{
int count = 0;
for (int i = 1; i < 100; i++)
{
count++;
}
printf("%d", count);
}
这个代码的的N无论传几,都是执行100次,所以它的执行次数是不变的~!所以它的时间复杂度是:O(1)
void BubbleSort(int* a, int n)
{
assert(a);
for (int i = 0; i < n; i++)
{
int flag = 1;
for (int j = 0; j < n - 1 - i; j++)
{
if (a[j] > a[j + 1])
{
int tmp = a[j];
a[j] = a[j + 1];
a[j + 1] = tmp;
flag = 0;
}
}
if (flag == 1)
break;
}
}
冒泡排序的时间复杂度是多少?它的本质是交换,每执行一次内层循环,要交换的数字就减少一个~!外层循环是交换的趟数~!所以它的执行次数:n - 1 + n - 2 + n - 3 ... + n - n总执行次数:((n-1 + 0)*n)/ 2 也即是等差数列求和公式~!化简后:n^2 / 2 - n / 2根据大O渐进表示法的时间复杂度为:O(n^2)。这个栗子也告诉我们求时间复杂度不要只看到循环也要看实现的思想~!
void BinSearch(int* a, int n, int x)
{
assert(a);
int left = 0;
int right = n - 1;
while (left <= right)
{
int mid = left + ((right - left) >> 2);
if (a[mid] > x)
{
right = mid - 1;
}
else if (a[mid] < x)
{
left = mid + 1;
}
else
return mid;
}
return -1;
}
二分查找的时间复杂度是多少?二分查找地思想就是每次砍一半,检查一次少一半~!它的执行次数就是他折半的次数~!如何计算他折半的次数呢?因为他查找一次折一半,所以可以设执行次数为x,他总长度为n所以有: 2^x = n ---> x = log 2(n),所以它的时间复杂度就是O(log2(n))
long long Fac(size_t N)
{
if (N == 0)
return 1;
return Fac(N - 1) * N;
}
递归的时间复杂度是多少?我们知道函数的递归是要开辟栈帧的,执行一次调用开辟一个栈帧,也就是说它的执行次数就是栈帧开辟的个数。如何求栈帧的个数呢?看N和它的终止条件,这里是N = 0, 也就是说他会执行0~N次总共N + 1次,即时间复O(N)
再来看一个和这个很像的:
long long Fac(size_t N)
{
if (N == 0)
return 1;
for (int i = 0; i < N; i++)
{
printf("hehe\n");
}
return Fac(N - 1) * N;
}
这里它的执行次数是多少?你可能会想这把不就是,每个栈帧里面执行n次呵呵吗,而前面算过,他的递归次数是N+1,所以它的总执行次数是N^2 + N,即O(N^2),虽然结果是对的~!但,它的每个执行的栈帧次数是错的,他每次N在变,所以他应该是个等差数列,N - 1 + N-2+N-3...N-N,总执行次数N^2/2 - N /2即O(N^2)
long long Fib(size_t N)
{
if (N < 3)
return 1;
return Fib(N - 1) + Fib(N - 2);
}
这个代码和上面的很像,但这个是斐波那契的递归实现~!上面的那个是递归求阶乘~不一样的~!而且我们在函数递归那一期介绍过这可不是一般的递归这是一个双路递归~!他的每一个F(N)下面都会有两个岔口(除了结束的);看下图:
假设已满的情况下(最坏情况),每一个节点有1个函数栈帧,也就是执行次数,第一层一个节点就是:2^0,第二层 2个节点:2^1....最后一层2^n-1个节点,所以它的总执行次数:2^0 + 2^1+ 2^2...2^(n-1)这是一个等比数列求和问题:带公式得:2^n - 1所以它的时间复杂度是:O(2^N)
和时间复杂度一样,空间复杂度也是一个数学表达式的函数(O)空间复杂度描述的是:算法在运行过程中临时(额外)占用存储空间大小的量度~!空间复杂度当然也不是看占几个字节的!也数不出来,他和时间复杂度一样都采用大O渐进法表示~!由于在程序运行时,程序所必须需要的栈空间(局部变量、参数、寄存器信息等)都已在编译期间准备好,因此空间复杂度主要是看额外开辟的空间~!
注意:在以前内存比较贵的时候都会考虑空间内复杂度的,但随着计算机的发飞速发展内存已经不是那么贵了,现在一般的电脑都是4/8G的空间一般都够用,所以在有的情况下讨论算法效率是只说时间复杂度~!
OK,我们下来就看看几个空间复杂度的实例:
void BubbleSort(int* a, int n)
{
assert(a);
for (int i = n; i > 0; i--)
{
int flag = 0;
for (int j = 1; j < i; j++)
{
if (a[j - 1] > a[j])
{
int tmp = a[j-1];
a[j - 1] = a[j];
a[j] = tmp;
flag = 1;
}
}
if (!flag)
break;
}
}
冒泡排序的空间复杂度是多少?注意我们上面介绍的时间复杂度是:额外(临时)开的空间~!这里额外或者临时开的空间就两个flag和tmp常数个所以他的空间复杂度是O(1);
long long* Fibonacci(size_t n)
{
if (n == 0)
return NULL;
long long* fibArray = (long long*)malloc((n + 1) * sizeof(long long));
fibArray[0] = 0;
fibArray[1] = 1;
for (int i = 2; i <= n; i++)
{
fibArray[i] = fibArray[i - 1] + fibArray[i - 2];
}
return fibArray;
}
这个代码的功能是求前n项的斐波那契数并返回~!在这个代码中额外开的空间是返回的那个数组:fibArray大小是n+1,所以它的空间复杂度是O(N);
long long Fac(int N)
{
if (N == 0)
return 1;
return Fac(N - 1) * N;
}
这个递归的空间复杂度又是多少?递归的空间的复杂度实际上看他开辟的栈帧个数即可,这里他开辟了N+1个所以它的空间复杂度是O(N);
long long Fib(size_t N)
{
if (N < 3)
return 1;
return Fib(N - 1) + Fib(N - 2);
}
斐波那契额递归的空间复杂度是多少?我们前面说了它的时间复杂度是O(2^N)也就是最坏情况下他开了2^N个栈帧,上面也刚刚说完递归的空间复杂度就看他开辟栈帧的数量~!所以这里是O(2^N)???然而这里并不是的!!!!我们已在强调这个递归可不是一般的递归他是王维诗里的递归!他是上路递归!和上面的单路递归不一样,单路递归直接看开辟栈帧的个数即可双路递归可不一样!!!!他的遍历图我在画一遍如下:
这里他先把左边的跑完左边的空间还给操作系统了,右边的是不是还能用呀~!所以他是左右两边共用一块空间的,至于这个空间的大小就和递归最深的N有关了~!所以它的空间复杂度是O(N);
这个栗子其实告诉我们时间不可能叠加而空间会!!!
一些常见的算法复杂度如下:
这些复杂度算出来之后哪个更快呢?我们好像不知道,下面这里有副图:
O(1) > O(logN) > O(N) > O(NlogN) > O(N ^c) > O(c^N) > O(N!) > O(N^N)
所以我们平时在写代码是也要尽量注意复杂度问题,写出复杂度较低的好算法~!!
OK,本期分享就到这里,好兄弟,我们下期再见~!