第一讲:数据结构和算法绪论
1.什么是数据结构?
数据结构是一门研究非数值计算的程序设计问题中的操作对象,以及它们之间的关系和操作等相关问题的学科。
数据结构就是关系,没错,就是数据元素相互之间存在的一种或多种特定关系的集合。
数据结构 + 算法 = 程序设计
2.数据结构分为哪两种结构?
(1)逻辑结构:
数据对象中数据元素之间的相互关系,也是后续的重点。
(2)物理结构:
数据的逻辑结构在计算机中的存储形式。
3.四大逻辑结构
①集合结构中的数据元素除了同属于一个集合外,之间没有啥“不三不四”的关系。
②线性结构中的数据元素之间是一对一的关系。
③树形结构中的数据元素之间存在一种一对多的层次关系。
④图形结构的数据元素是多对多的关系。就像人类社会,他和她,她又和他,产生了不可言传的关系~
4.两大物理结构
物理结构说白了就是:
如何把数据元素存储到计算机的存储器中(硬盘,软盘,光盘)
(1)顺序存储:
把数据元素存放在地址连续的存储单元里,数据间的逻辑关系和物理关系是一致的。
就像这个数组:
var a = [1,2,3,4,5];
从顺序存储结构我们想到了日常生活中我们的排队,有木有?
但现实生活中,我们发觉也并不完全如此。
例如:
有人排着排着她内急,她要被迫离开队伍去上洗手间,还有人不遵守基本基本道德规范他插队,这些情况会大破存储存储结构的基本原则。
面对这样时常要变化的结构,顺序存储是不科学滴~
那么就该让链式存储结构露面了!
(2)链式存储结构:
是把数据元素存放在任意的存储单元里,这组存储单元可以是连续的,也可以是不连续的。
说白了就是:
类似现在的排号系统,先领一个号码,在你被叫到之前,你想干啥干啥,只要叫到你的时候,在现场就可以
既然是数据结构,那么还是要有“逻辑约束”,因此需要用一个指针存放数据元素的地址。
这样子通过地址就可以找到相关联数据元素的位置。
第二讲:谈谈算法
1.为什么数据结构和算法要放在一起讲?
打个比方,其实数据结构和算法的关系就比好基友是一辈子的关系。
他们患难见真情,他们生死不相弃,他们荣辱与共,他们一生情一辈子……
说白了:
数据结构是锁,必须要有一把钥匙,即算法,插进去,才能开启一个神奇的事情!
小学学过珠算的应该很有印象,每天加法运算敲得手指都快断了就算:
1+2+…+99+100
在地球的某个地方,出现了一位神童,他为了早点回家吃午饭,发明了一个牛x的算法:
等差数列求和
(首项 + 末项)x项数 /2
数学表达:1+2+3+4+……+ n = n (n+1) /2
这位神童,就是后来家喻户晓的 Gauss (高斯),随着你的学术生涯提升,你会接二连三被他“搞死”
有些编程基础的,就会得瑟:
我会for循环,这么点数字能难得倒我?!
三下五除二,写下这段代码:
int i, sum = 0, n = 100;
for(i=1; i <= n; i++)
{
sum = sum + i;
}
printf(“%d”, sum);
祝贺你,写出来一段很有厉害(没卵用)的代码!
看看写的高斯算法代码:
int i, sum = 0, n = 100;
sum = (1+n)*n/2;
printf(“%d”, sum);
单纯的你可能会说:
以计算机CPU的运算神速,两个算法都可以秒杀解决掉求和问题!
但是,如果我们把条件换成1加到1千万,或者1加到1千亿,差距就可想而知了,甚至人脑都可以比电脑计算得快了。
说白了:
当你总资产只有几块时,几毛钱都很重要
2.什么是算法?
官方定义:(不用太认真)
算法是解决特定问题求解步骤的描述,在计算机中表现为指令的有限序列,并且每条指令表示一个或多个操作。
其实:
算法就是你泡妞儿的技巧和方式。
当你是一个单纯的以为写封情书,并告诉妹子,“我爱你”,就能拿啥的单纯boy,注定单身撸代码。
假如你是高傲,孤冷,套路很多的老司机,注定彩旗飘飘~
就像没有药可以包治百病一样
一个问题可以由多个算法解决
一个算法也不可能具有通解所有问题的能力
3.算法五大基本特征
①输入。算法具有零个或多个输入的接口。
尽管对于绝大多数算法来说,输入参数都是必要的。但是有些时候,像打印“I love fishc.com”,就不需要啥参数啦。
void print()
{
printf(“I love fishc.com\n”);
}
②输出。算法至少有一个或多个输出。
算法是一定要输出的,不需要它输出,那你要这个算法来干啥?
就像光吃,不拉,怎么能受得了呢?输出的形式可以是打印形式输出,也可以是返回一个值或多个值等。
③有穷性。指算法在执行有限的步骤之后,自动结束而不会出现无限循环。
并且每一个步骤在可接受的时间内完成。
一个永远都不会结束的算法,我们还要他来干啥?(忽略装x的理由)
④确定性。算法的每一个步骤都具有确定的含义,不会出现二义性。
算法在一定条件下,只有一条执行路径,相同的输入只能有唯一的输出结果。算法的每个步骤都应该被精确定义而无歧义。
一个你都不知道在干啥的算法,我们还要他干啥?(忽略你不懂的大牛算法,总有人懂)
⑤可行性。算法的每一步都必须是可行的,也就是说,每一步都能够通过执行有限次数完成。
一个自相矛盾,跑不了的算法,我们还要他干啥?
4.算法设计5大特性
①正确性
算法程序没有语法错误:
意味着你编写的算法,从程序语法规定上将不能有问题。
就像循环,你不能写出很没边的条件
for(var i=0; i>1 ; i++){}
算法程序对于合法输入能够产生满足要求的输出:
意味着,正确的输入一定要输出符合要求的结果。
算法程序对于非法输入能够产生满足规格的说明:
意味着,例如你要求输入数字,有人输入字符串,算法一定要报错。
算法程序对于故意刁难的测试输入都有满足要求的输出结果:
意味着,假如你的算法只需要用到百万量级,有的人,非要输入千万量级。
那么你的算法,也要能正确输出他该输出的结果。
②可读性
算法设计另一目的:是为了便于阅读、理解和交流。
写代码的目的:
一方面是为了让计算机执行,但还有一个重要的目的是为了便于他人阅读和自己日后“阅读”和“修改”。
③健壮性
当输入数据不合法时,算法也能做出相关处理,而不是产生异常、崩溃或莫名其妙的结果。
不管你怎么折腾,算法就是可以正常运行。
④时间效率
同样的一段代码,你的算法可以比别人跑得快,这就是优势。
往往是高手过招的关键点!
⑤存储量
同样的一段代码,你的算法比别人占用内存少,这就是优势。
往往是高手过招的关键点!
第三讲 时间复杂度和空间复杂度1
1.两大度量算法方法
①事后统计
因为计算机都有计时功能,所以可以通过计时比快慢的方法来衡量算法效率。
这种方法主要是通过设计好的测试程序和数据。
利用计算机计时器对不同算法编制的程序的运行时间进行比较。
但这种方法显然是有缺陷的:
必须依据算法事先编制好测试程序,通常需要花费大量时间和精力,完了发觉测试的是糟糕的算法,那不是功亏一篑?赔了娘子又折兵?
不同测试环境差别不是一般的大!
② 事先分析
在计算机程序编写前,依据统计方法对算法进行估算。
经过总结,我们发现一个高级语言编写的程序在计算机上运行时所消耗的时间取决于下列因素:
1. 算法采用的策略,方案
2. 编译产生的代码质量
3. 问题的输入规模
4. 机器执行指令的速度
由此可见,抛开这些与计算机硬件、软件有关的因素。
一个程序的运行时间依赖于算法的好坏和问题的输入规模。
(所谓的问题输入规模是指输入量的多少)
2.高斯(搞死)等差数列实战
第一种算法:
int i, sum = 0, n = 100; // 执行1次
for( i=1; i <= n; i++ ) // 执行了n+1次
{
sum = sum + i; // 执行n次
}
不同的变量执行次数不一样,核心算法循环了n次。
第二种算法:
int sum = 0, n = 100; // 执行1次
sum = (1+n)*n/2; // 执行1次
虽然只标注了1次,乍一看很牛x。
但是仔细想一想,那个n=100咋来的。。。
暂时抛开这个问题,只从这两段代码结构上来讲,显然第二种算法效率更高。只从执行次数维度来看:
第一种算法执行了1+(n+1)+n=2n+2次。
第二种算法,是1+1=2次
3.深入剖析
一段代码:
int i, j, x=0, sum=0, n=100;
for( i=1; i <= n; i++ )
{
for( j=1; j <= n; j++ )
{
x++;
sum = sum + x;
}
}
复制代码
这个例子中,循环条件i从1到100,每次都要让j循环100次。
如果非常较真的研究总共精确执行次数,那是非常累的。
另一方面,我们研究算法的复杂度,侧重的是:
研究算法随着输入规模扩大增长量的一个抽象整体。
而不是:精确地定位需要执行多少次
因为如果这样的话,我们就又得考虑回编译器优化等问题。
然后如同掉进一个“兔子洞”,然后就永远也没有然后了!
所以,对于刚才例子的算法,我们可以果断判定需要执行100^2次。(外层从1循环到100,每一次内层也是从1到100,100*100)
我们不关心编写程序所用的语言是什么,也不关心这些程序将跑在什么样的计算机上。我们只关心它所实现的算法。
4.效率的度量方法
在上面的例子中,我们通过度量“实现算法”,来衡量效率问题。
这样,不计那些循环索引的递增和循环终止条件、变量声明、打印结果等操作。
最终,在分析程序的运行时间时,最重要的是:把程序看成是独立于程序设计语言的算法或一系列步骤。
我们在分析一个算法的运行时间时,重要的是:
把基本操作的数量和输入模式关联起来。
横轴代表输入值,纵轴代表输出值,三条不同的函数,对应不同的曲线。
当输入量小的时候,三个函数(算法)增长量相差无几。
但随着输入值越大,n*n函数增长量瞬间秒杀其他两个。
这个通过操作数量和输入模式来分析算法的运行时间。
5.函数的渐进增长
当n=1时,算法A1效率不如算法B1,当n=2时,两者效率相同;
当n>2时,算法A1就开始优于算法B1了,随着n的继续增加,算法A1比算法B1逐步拉大差距。
所以总体上算法A1比算法B1优秀。
6.渐进增长定义
渐进增长:
给定两个函数f(n)和g(n),如果存在一个整数N,使得对于所有的n>N,f(n)总是比g(n)大。
那么,我们说f(n)的增长渐近快于g(n)
从刚才的对比中我们还发现,随着n的增大,后面的+3和+1其实是不影响最终的算法变化曲线的。
例如算法A2,B2,在图中他们压根儿被覆盖了。
所以,我们可以忽略这些加法常数。(维度不一样,攻击效果不一样)
最高次项的指数大的,函数随着n的增长,结果也会变得增长特别快。判断一个算法的效率时,函数中的常数和其他次要项常常可以忽略,而更应该关注主项(最高项)的阶数。
第四讲 时间复杂度和空间复杂度2
1.算法时间复杂度
官方定义:
在进行算法分析时,语句总的执行次数T(n)是关于问题规模n的函数,进而分析T(n)随n的变化情况并确定T(n)的数量级。算法的时间复杂度,也就是算法的时间量度,记作:T(n)= O(f(n))。它表示随问题规模n的增大,算法执行时间的增长率和f(n)的增长率相同,称作算法的渐近时间复杂度,简称为时间复杂度。其中f(n)是问题规模n的某个函数。
重点:
执行次数==时间
这样用大写O()来体现算法时间复杂度的记法,我们称之为大O记法。一般情况下,随着输入规模n的增大,T(n)增长最慢的算法为最优算法。
2.分析大O阶
如何分析一个算法的时间复杂度呢?
即如何推导大O阶呢?
我们给大家整理了以下攻略:
用常数1取代运行时间中的所有加法常数。
在修改后的运行次数函数中,只保留最高阶项。
如果最高阶项存在且不是1,则去除与这个项相乘的常数。
得到的最后结果就是大O阶。
3.实例搞懂大O
①常数阶
从高级语言维度来看(汇编级别就不考虑了),这段代码是O(8)?
int sum = 0, n = 100;
printf(“I love fishc.com\n”);
printf(“I love Fishc.com\n”);
printf(“I love fishC.com\n”);
printf(“I love fIshc.com\n”);
printf(“I love FishC.com\n”);
printf(“I love fishc.com\n”);
sum = (1+n)*n/2;
O(8)?
这是初学者常常犯的错误,总认为有多少条语句就有多少。
分析下,按照我们的概念“T(n)是关于问题规模n的函数”
纵观整段代码,只有最后一条语句涉及到n变量。
其余几条只是输出语句,跟运算没毛线关系~
攻略第一条也说明了所有加法常数给他个O(1)即可
②线性阶
线性阶就是随着问题规模n的扩大,对应计算次数呈直线增长。
int i , n = 100, sum = 0;
for( i=0; i < n; i++ )
{
sum = sum + i;
}
它的循环的时间复杂度为O(n),因为循环体中的代码需要执行n次。
③平方阶
平方阶,就是线性阶的线性阶(a*a)
即,循环嵌套:
int i, j, n = 100;
for( i=0; i < n; i++ )
{
for( j=0; j < n; j++ )
{
printf(“I love FishC.com\n”);
}
}
n等于100,也就是说外层循环每执行一次,内层循环就执行100次。
那总共程序想要从这两个循环出来,需要执行100*100次,也就是n的平方。所以这段代码的时间复杂度为O(n^2)。
有多少层循环嵌套,时间复杂度就是O(n^层)?
建议你千万不要这么想!
坑来了:
int i, j, n = 100;
for( i=0; i < n; i++ )
{
for( j=i; j < n; j++ )
{
printf(“I love FishC.com\n”);
}
}
注意第四行的j赋值。
分析下,由于当i=0时,内循环执行了n次,当i=1时,内循环则执行n-1次……
当i=n-1时,内循环执行1次,所以总的执行次数应该是:
n+(n-1)+(n-2)+…+1 = n(n+1)/2
大家还记得这个公式吧?
恩恩,没错啦,就是高斯(搞死)先生发明的算法丫。
那咱理解后可以继续,n(n+1)/2 = n^2/2+n/2
用我们推导大O的攻略:
第一条忽略,因为没有常数相加。
第二条只保留最高项,所以n/2这项去掉。
第三条,去除与最高项相乘的常数,最终得O(n^2)。
④对数阶
忘记了,也没事,咱分析的是程序为主,而不是数学为主,不怕。
int i = 1, n = 100;
while( i < n )
{
i = i * 2;
}
i每次循环,都会乘2.
假设有x个2相乘后大于或等于100,则会退出循环。
于是由2^x = n得到x = log(2)n,所以这个循环的时间复杂度为O(logn)。
其实理解大O推导不算难,难的是对数列的一些相关运算。
这更多的是考察你的数学知识和能力。
第五讲 时间复杂度和空间复杂度3
1.超级实战!
n++;
function(n);
for(i=0; i < n; i++) {
function(i);
}
void function(int count) {
int j;
for(j=count; j < n; j++) {
printf(“%d”, j);
}
}
for(i=0; i < n; i++) {
for(j=i; j < n; j++) {
printf(“%d”, j);
}
}
请问这段代码的时间复杂度是?
事实上,这和之前我们讲解平方阶的时候举的第二个例子一样。
前景回顾:
function内部的循环次数随count的增加(接近n)而减少.
所以根据游戏攻略算法的时间复杂度为O(n^2)。
答案揭晓:
2.装X术语总结
常用的时间复杂度所耗费的时间总结:
O(1) < O(logn) < (n) < O(nlogn) < O(n^2) < O(n^3) < O(2^n) < O(n!) < O(n^n)
3.最坏情况与平均情况
从心理学角度讲,每个人对将来要发生的事情都会有一个预期。譬如看半杯水,有人会说:哇哦,还有半杯哦!但有人就会失望的说:天,只有半杯了。
一般人常出于一种对未来失败的担忧,而在预期的时候趋向做最坏打算。这样,即使最糟糕的结果出现,当事人也有了心理准备,比较容易接受结果,假如结局并未出现最坏的状况,这也会使人更加快乐,瞧,事情发展的还不错嘛!
算法的分析也是类似,我们查找一个有n个随机数字数组中的某个数字。
最好的情况是第一个数字就是,那么算法的时间复杂度为O(1)。
但也有可能这个数字就在最后一个位置,那么时间复杂度