数据结构与算法,出发!
成都创新互联公司成都网站建设按需网站制作,是成都网站建设公司,为纱窗提供网站建设服务,有成熟的网站定制合作流程,提供网站定制设计服务:原型图制作、网站创意设计、前端HTML5制作、后台程序开发等。成都网站改版热线:028-86922220数据结构与算法系列也将持续更新。
有C语言一定基础的读者可以来学习一下数据结构与算法~
如果对C语言还有遗憾或者是想再学习一下,请关注我的C语言初阶系列
目录
什么是数据结构?| 什么是算法?
时间复杂度
嵌套循环时间复杂度的计算
双重循环的时间复杂度的计算
常数循环的时间复杂度
strchar的时间复杂度
冒泡排序的时间复杂度
二分查找的时间复杂度
阶乘递归的时间复杂度
斐波那契递归的时间复杂度
在很多地方,数据结构总是和算法放在一起叫的。
比如很多课程叫做《数据结构与算法》,而不是把它们分成独立的课程。
在一些概念中,数据结构和算法被这样定义:
数据结构是计算机存储、组织数据的方式,指相互之间存在一定或多种数据元素的集合。
算法就是定义良好的计算过程,它取一个或一组的值为输入,并产生一个或一组值作为输出。简单来说算法就是一系列的计算步骤,用来将输入数据转化成输出结果。
看完这些概念,您理解吗?
形象地来说,数据结构就是一些项目在实现的过程中,我们可能需要在内存中把数据给存储起来。
比如说,要实现一个通讯录,我们要把每个人的信息去存储起来。
那怎么把它们存储起来呢?
如果学习过C语言就知道要用数组存储。
除了数组之外,还可以以链表、树、哈希表的形式存储......
不论是哪种形式,都有其特点。
用数组存储,它的优点就是很方便,缺点就是假如你数组有500个元素,那么存储过500个人的信息后就不能再存储了。
虽然C99标准可以使用动态数组,但还需要给数组扩容,比较麻烦。
而使用链表的话会更方便。只需要申请内存,然后用指针链接起来即可。
如果我们想去查找某个人,就可能要用到树了。日后细讲。
所以我们就要根据需求去选择不同的数据结构,要去考虑用那种数据结构更合适。
算法有很多很多,如排序、查找、去重。
这些天手机圈有很多惊喜,荣耀发布了MgicOS、小米13对标iPhone等等......
如果我想买一部手机,想买最贵的,我就需要用价格去降序排序,想买最新发布的,就要根据时间去排序,想看一下大家买的比较多的,就要根据销量去排序。
这就用到了排序算法。
还有很多很高级的算法,比如推荐算法。
刷短视频,短视频平台会根据你的偏好去推荐相关的视频。
数据结构和算法已经在日常生活中无处不在了。
要设计算法,很多时候就要用到数据结构。
数据结构和算法是不分家的,有些数据结构需要用到算法,有些算法需要用到数据结构。
什么是数据结构?什么是算法?笔者之所以把这两个问题放在一块,就是因为它们联系太紧密了。
在C语言阶段练习和总结并重,而在数据结构阶段更侧重好好写代码,还要学会画图和分析。
时间复杂度算法分析分为两个方面,一个是时间复杂度,另一个是空间复杂度。本节重点讲解时间复杂度。
对于一个算法,我们要去判断它的运行的好坏,最重要的是看它运行起来跑得快不快,除此之外还要看它占用多少空间。
时间复杂度主要衡量一个算法的运行快慢,空间复杂度主要衡量一个算法运行所需要的额外的空间。
早期的时候还比较关注空间,而现在基本上不关注空间了,因为现在内存越来越大了,能够满足内存占用较多的程序运行。
但是算法对时间还有一定的追求。
算法的时间复杂度是一个函数,但注意这里所指的函数是数学中的带有未知数的函数表达式。
时间复杂度不去计算它能跑多少秒,因为一个程序在配置很低的机器上和配置很高的机器上的时间是不一样的。
环境不同,具体运行时间不同。
算法中的基本操作的执行次数,是算法的时间复杂度。
嵌套循环时间复杂度的计算下面来计算下Func1基本操作执行了多少次?
void Func1(int N)
{
int count = 0;
for (int i = 0; i< N; ++i)
{
for (int j = 0; j< N; ++j)
{
++count;
}
}
for (int k = 0; k< 2 * N; ++k)
{
++count;
}
int M = 10;
while (M--)
{
++count;
}
printf("%d\n", count);
}
准确的次数是n^2+2n+10;
时间复杂度的函数式为
F(N)=N*N+2*N+10
我们看下这张图:
如果我们把F(N)=N*N+2*N+10简化一下,简化成F(N)=N*N
那么
N=10时, 复杂度为100
N=100时,复杂度为10000
N=1000时,复杂度为1000000
可以发现,N越大,后两项对结果的影响越小。
实际中我们计算时间复杂度时,我们其实并不一定要计算精确的执行次数,而只需要大概执行次数。
这就好比求极限,看下边这道题:
当x趋于无穷大时,(x^2-1)/(2x^2-x-1)~1/2。
计算时间复杂度就好比求极限,在F(N)=N*N+2*N+10中,因为当N足够大时,后边式子对复杂度的影响越来越小,那我们就把它化简成F(N)=N*N。
这里我们使用大O的渐进表示法。大O的渐进表示法就是估算。
大O符号(Big O notation):是用于描述函数渐进行为的数学符号。
推导大O阶方法:
1、用常数1取代运行时间中的所有加法常数。
2、在修改后的运行次数函数中,只保留最高阶项。
3、如果最高阶项存在且不是1,则去除与这个项目相乘的常数。得到的结果就是大O阶。
那么就得出此算法的时间复杂度为O(N^2)。
双重循环的时间复杂度的计算请计算Func2的时间复杂度
void Func2(int N)
{
int count = 0;
for (int k = 0; k< 2 * N; ++k)
{
++count;
}
int M = 10;
while (M--)
{
++count;
}
printf("%d\n", count);
}
精确的计算是2N+10。
那么当N无限大的时候,10对结果的影响很小。而当N无限大的时候N和2N是同一个级别。
在上文也提到如果最高阶项存在且不是1,则去除与这个项目相乘的常数。
所以可得出O(N)。
再来一道题,请计算Func3的时间复杂度
void Func3(int N, int M)
{
int count = 0;
for (int k = 0; k< M; ++k)
{
++count;
}
for (int k = 0; k< N; ++k)
{
++count;
}
printf("%d\n", count);
}
结果为O(M+N)。
一般情况下计算时间复杂度都用N,但是也可以用M、K等等其他的。
M和N都是未知数,但是我们不知道是M大还是N大。
所以M和N都要用。
如果这道题有了条件:
如果条件是M远大于N,那么结果为O(M);
如果条件是N远大于M,那么结果为O(N);
如果条件是M和N差不多大,那么既可以用O(M)表示,也可以用O(N)表示。
常数循环的时间复杂度请计算Func2的时间复杂度
void Func4(int N)
{
int count = 0;
for (int k = 0; k< 100; ++k)
{
++count;
}
printf("%d\n", count);
}
这里没有未知数,那就是100次。
根据推导大O阶的方法用常数1取代运行时间中的所有加法常数,那么这道题的时间复杂度就是O(1)。
如果有人要求你把某个算法优化到O(1),这并不意味着只让算法执行一次,而是运行常数次。
strchar的时间复杂度请计算strchar的时间复杂度:
const char* strchr(const char* str, int character);
这是一个库函数,不怎么常用的库函数,它的代码大概如下:
while (*str)
{
if (*str == character)
return str;
else
++str;
}
它的功能是在字符串里查找一个字符。
那么它的时间复杂度是多少?
假设我从“hello world”中查找字符:
假设我查找的是字符是h ----> O(1)
假设我查找的是字符是w ----> O(N/2)
假设我查找的是字符是d ----> O(N)
(我们不知道字符串的长度,那就用N。
那么这三种情况分别对应最好的情况、平均的情况、最坏的情况。
有些算法的时间复杂度存在最好、平均和最坏情况:
最好情况:任意输入规模的最小运行次数(下界)
平均情况:任意输入规模的期望运行次数
最坏情况:任意输入规模的大运行次数(上界)
当一个算法随着输入不同,时间复杂度不同,时间复杂度做悲观预期,看最坏情况。
我们一般最看重最坏的情况。只有极个别少数算法不需要考虑最坏和平均情况。
99%情况下都是看最坏情况!
冒泡排序的时间复杂度请计算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;
}
}
先算一个精确的,F(N)=(1+N)/2。
为什么是F(N)=(1+N)/2?
我们要去了解下冒泡排序的思想。有的算法不能只看代码。
我们做最悲观的考虑,那么每个数都参与了比较。
图中红色代表未比较过的数据,黄色方框代表已经排好序的数据,蓝色线条代表比较次数。
从最右边执行过的次数可以发现,比较的总次数为(N-1)+(N-2)+(N-3)+···+1。
这是个等差数列,循环了N-1次,比较了(N-1)+(N-2)+(N-3)+···+1次。
也就是说项数为N-1,那么总次数为N*(N-1)/2。
它的时间复杂度为O(N^2)。
二分查找的时间复杂度计算BinarySearch的时间复杂度:
int BinarySearch(int* a, int n, int x)
{
assert(a);
int begin = 0;
int end = n - 1;
while (begin< end)
{
int mid = begin + ((end - begin) >>1);
if (a[mid]< x)
begin = mid + 1;
else if (a[mid] >x)
end = mid;
else
return mid;
}
return -1;
}
算时间复杂度不能只去看几层循环,而要去看他的思想。
如果只去看几层循环,那么它的时间复杂度就是O(N),然而这是错的!
它的时间复杂度是O(log2N)。
关于二分查找的思想详见我之前的博客http://t.csdn.cn/kIg93
二分查找最好的情况是O(1),就是你要查找的数正好在中间。
事实上我们一般不关注最好情况,那么最坏情况是多少?
找不到是最坏的情况。
假设我们要查找X次也没找到,那么2^X=N。
可知X=log2N
二分查找是一个非常厉害的算法。
在1000个数中查找,大概需要10次;
在100w个数中查找,大概需要20次;
在10亿个数中查找,大概需要30次。
可以发现随着N急剧增大,需要查找的次数变化并不是特别明显。
用时间复杂度分析二分查找,才知道二分查找的神奇之处。
假设我们从14亿人中找一个人,最多需要31次(前提是排好序了......)。
在有序的前提下,二分查找是一个很厉害的算法。
阶乘递归的时间复杂度计算阶乘递归Fac的时间复杂度:
long long Fac(size_t N)
{
if (0 == N)
return 1;
return Fac(N - 1) * N;
}
递归算法:递归次数*每次递归调用的次数。
递归次数好理解,那么每次递归调用的次数是什么?
比如if判断是一次,然后return后边的Fac里的计算是一次......但是这些都可以理解为常数次!
所以关注递归次数就好。
那么该算法的时间复杂度就是O(N)。
斐波那契递归的时间复杂度计算斐波那契递归Fib的时间复杂度:
long long Fib(size_t)
{
if (N< 3)
return 1;
return Fib(N - 1) + Fib(N - 2);
}
我们来看斐波那契递归的过程:
可以发现,右边比左边更早地完成了递归。
但是当N足够大时,右边比左边少递归的次数X对整体递归次数的影响很小。
我们之前提到过:
递归算法:递归次数*每次递归调用的次数。
由于递归内调用次数可以理解为常数,那么
Fib(N)=2^0+2^1+2^3+······+2^(n-2)+2^(n-1)-X
不过X可以忽略不计,因为X前边的次数和远大于X。
那么用等比数列计算可得到
Fib(N)=2^N-1
1也可以忽略不计,所以时间复杂度为
O(2^N)
创作不易,这次码了将近五千字,求各位老铁三连支持下!
你是否还在寻找稳定的海外服务器提供商?创新互联www.cdcxhl.cn海外机房具备T级流量清洗系统配攻击溯源,准确流量调度确保服务器高可用性,企业级服务器适合批量采购,新人活动首月15元起,快前往官网查看详情吧
售后响应及时
7×24小时客服热线数据备份
更安全、更高效、更稳定价格公道精准
项目经理精准报价不弄虚作假合作无风险
重合同讲信誉,无效全额退款