早期人们都把计算机理解为数值计算工具,感受计算机就是为了解决复杂计算问题.因此计算机解决问题,应该是先从具体问题中抽象出一个适当的数据模型,设计出一个解决此数据模型的算法,而后才开始编写程序,从而实现一个解决问题的软件.算法
可是,现实开发中,咱们不单纯的只是解决数值计算问题,而是须要更加一些高效的手段来解决问题.例如须要更合适的数据结构,树,表,图等数据结构来更好的处理问题. 数据结构是一门研究非数值计算的程序设计问题中操做对象.后端
数据结构是一门独立的学科, 也能够看出它在计算机专业的地位. 它不是算法的附属品. 好的程序设计 = 数据结构 + 算法.数组
数据结构和算法是不可分割的. 在你前往高级开发者的路上,数据结构和算法必定是必须啃下来的硬骨头.bash
数据结构和算法不只是计算机学生的必修课,更是开发者们职业生涯必修课. 你须要不断的训练与学习来保持对算法的敏感度.网络
###1.2 基本概念与术语数据结构
在谈数据结构以前,咱们先来谈谈数据. 正所谓"巧妇难为无米之炊",再强大的计算机,若是没有数据. 也是毫无做用.数据结构和算法
数据结构中最基本的5个概念: 数据,数据元素,数据项,数据对象,数据结构;函数
数据: “是描述客观事物的符号,是计算机中能够操做的对象,是能被计算机识别,并输入给计算机处理的符号集合。数据不只仅包括整型、实型等数值类型,还包括字符及声音、图像、视频等非数值类型。”工具
例如,Mp3就是声音数据,图片就是图像数据. 而这些数据,其实只不过是"符号". 这些符号有2个前提:性能
对于从后端返回的整型,浮点型数据,能够直接进行数值计算. 对于字符串类型的数据,咱们就须要进行非数值的处理. 而对于声音,图像,视频这样的数据,咱们须要经过编码的手段将其变成二进制数据进行处理.
数据元素: 是组成数据的,且有必定意义的基本单位,在计算机中一般做为总体处理. 也被称做"记录"
例如,咱们生活的圈子里.什么叫数据元素了? 人Person ,汽车Car 等.
数据项: 一个数据元素能够由若干数据项组成.
好比,Person 数据元素,能够为分解为眼睛,耳朵,鼻子,嘴巴,手臂这些基本的数据项,也能够从另外的角度拆解成姓名,年龄,性别,出生地址,出生日期,联系电话等数据项. 那么你如何拆解数据项, 要看你的项目来定.
数据项是数据不可分割的最小单位. 在咱们的课程中,咱们把数据定位为最小单位.这样有助于咱们更好理解以及解决问题.
数据对象: 是性质相同的数据元素的集合,是数据的子集.
那么什么叫性质相同? 是指数据元素具备相同数量和类型的数项. 相似数组中的元素保持性质一致.
结构,简单理解就是关系. 好比分子结构,就是说组成分子原子的排列方式. 不一样数据元素之间不是独立的,而是存在特定的关系.咱们将这些关系成为结构. 那么数据结构是什么? 数据结构是相互之间存在一种或多种特定关系的数据元素的集合.
代码片断(1)
//
// main.c
// 001--数据结构基本术语
//
// Created by CC老师 on 2019/8/12.
// Copyright © 2019年 CC老师. All rights reserved.
//
/*
数据: 程序的操做对象,用于描述客观事物.
数据的特色: 1️⃣ 能够输入到计算机 2️⃣ 能够被计算机处理
数据项: 一个数据元素由若干数据项组成
数据元素: 组成数据的对象的基本单位
数据对象: 性质相同的数据元素的集合(相似于数组)
结构: 数据元素之间不是独立的,存在特定的关系.这些关系便是结构;
数据结构:指的数据对象中的数据元素之间的关系
*/
#include <stdio.h>
//声明一个结构体类型
struct Teacher{ //一种数据结构
char *name; //数据项--名字
char *title; //数据项--职称
int age; //数据项--年龄
};
int main(int argc, const char * argv[]) {
struct Teacher t1; //数据元素;
struct Teacher tArray; //数据对象;
t1.age = 18; //数据项
t1.name = "CC"; //数据项
t1.title = "讲师"; //数据项
printf("老师姓名:%s\n",t1.name);
printf("老师年龄:%d\n",t1.age);
printf("老师职称:%s\n",t1.title);
return 0;
}
复制代码
根据视角不一样,咱们将数据结构分为2种: 逻辑结构与物理结构;
逻辑结构: 指的是数据对象中的数据元素之间的相互关系. 逻辑结构分为四种: 集合结构,线性结构,树形结构,图形结构. 具体采用什么的数据结构,是须要根据实际业务需求来进行合理的设计.
集合结构: 集合结构中的数据元素除了同属于一个集合外,它们之间没有其余关系. 各个数据元素是"平等"的. 它们的共同属性是:"同属于一个集合".
例如: 动物园,就是一个集合; 河马,熊猫,狮子,老虎,长颈鹿他们就是数据元素. 它们共同的属性就是:"同属于动物这个集合";
线性结构: 线性结构中的数据元素之间是一对一的关系.经常使用的线性结构有:线性表,栈,队列,双队列,数组,串。
树型结构: 重要的非线性数据结构。树形数据结构能够表示数据表素之间一对多的关系.树型结构中的数据元素之间存在一种一对多的层次关系. 常见的树形结构: 二叉树,B树,哈夫曼树,红黑树等.
图形结构: 图形结构的数据元素是多对多的关系. 常见的图形结构: 邻近矩阵,邻接表.
物理结构,别称"存储结构". 顾名思义,指的是数据的逻辑结构在计算机的存储形式.
设计好逻辑数据结构以后,数据的存储也是很是重要的. 数据存储结构应该正确反映数据元素之间的逻辑关系.这才是关键! 如何存储数据元素之间的逻辑关系,是实现物理结构的重点和难点.
数据元素的存储结构形式有2种: 顺序存储和链式存储;
顺序存储结构: 是指把数据元素存放在地址连续的存储单元里,其数据间的逻辑关系和物理关系是一致的.
解读:
这样的存储方式,实际很是简单. 能够理解为排队占位. 按照顺序,每一个一段空间. 咱们所学习且使用的数组就是典型顺序存储结构. 当你在计算机创建一个有6个整型数据的数组时,计算机便在内存中找到一片空地.按照一个整型所占空间大小乘以6.开辟一段连续的内存空间. 而后将数据依顺序放在位置上,依次排放.
了解到顺序存储结构,若是一切数据都有规律且简单使用顺序存储结构固然是最合适不过的. 可是实际上,老是有复杂的状况发生. 对于常常且时差发生变化的数据结构,使用顺序存储就会很是不科学. 那该如何解决了?
链式存储结构: 是把数据元素放在任意的存储单元里,这组存储单元能够是连续的,也能够是不连续的. 数据元素的存储关系并不能反映逻辑关系,所以须要用一个指针存放数据元素的地址,这样经过地址就能够找到相关关联数据元素的位置.
显然,链式存储就灵活多了. 数据存储在哪里不重要的,只要有一个指针存放了相应的地址就能找到它了.
总结 逻辑结构是面向问题的,而物理结构就是面向计算机的. 其基本的目标就是将数据以及逻辑关系存储到计算机的内存中.
数据类型: 是指一组性质相同值的集合以及定义在此集合的一些操做的总称.
在C语言中,按照取值不一样,数据类型能够分为2类:
抽象,是抽取出事物具备的广泛性的本质. 它是抽出问题的特征而忽略非本质的细节,是对具体事物的一个归纳. 抽象是一种思考问题的方式,它隐藏繁杂的细节,只保留实现目标所必需的信息.
抽象数据类型: 是指一个数学模型以及定义在该模型上的一组操做; 例如,咱们在编写计算机绘图软件系统时,常常会使用到坐标. 也就是说,会常用x,y来描述横纵坐标. 而在3D系统中,Z深度就会出现. 既然这3个整型数字是始终出如今一块儿. 那就能够定义成一个Point的抽象数据类型. 它有x,y,z三个整型变量. 这样开发者就很是方便操做Point 数据变量.
抽象数据类型能够理解成实际开发里常用的结构体和类; 根据业务需求定义合适的数据类型以及动做.
算法: 是解决特定问题求解步骤的描述,在计算机中表现为指令的有限序列,而且每条指令表示一个或多个操做.
什么是算法: 解决特定问题的求解步骤的描述; 而数据结构若是脱离算法,或者算法脱离数据结构都是没法进行的. 因此在大学课程里,即使单独算法课程里也涵盖了数据结构的内容.
程序设计 = 数据结构 + 算法
你们在大学期间,学习C语言; 大多数遇到的第一个算法问题即是: 求解从1-N相加的结果; 而最简单的办法则是使用C语言模拟从1累积到N这个过程,从而获得结果;
这样的解决方案,是算法吗? 我能够确定告诉你们是的. 这就是算法; 但此时同窗们,也许会想到用数学的方式解决问题. 好比,使用等差数列解决.
故事: 使用这样的方式解决数学问题,是在18世纪德国的数学家高斯. 在小学时,被留校. 老师要求孩子们从1累积到100. 当时想出来的办法.
对比以上2种方式,若是不只仅是累积到100,而是一千. 第一种方式,显然须要计算机循环1千次来模拟数学计算,而第二种方式确定要比第一种来的快.
到底如何评价一个算法优劣了? 接下来咱们花时间来探讨.
什么是算法? 算法就是描述解决问题的方法; 经过刚刚的例子,咱们能够知道对于一个给定的问题,是能够有多种算法来解决;
那我想问问,有没有通用算法? 换过头来,有没有包治百病的药?
现实开发中会有成千上万的问题,算法也是变幻无穷,没有通用的算法能够解决全部问题. 甚至解决一个小问题,很优秀的算法却不必定很是适合它;
每一个人对问题的解决方案是不同的,可是咱们能够根据一些参考来评估算法的优劣;
算法必须具有几个基本特性: 输入,输出,有穷性,肯定性和可行性;
输入输出,很好理解. 在解决问题时必须有已知条件,固然有些算法可能没有输入. 可是算法至少有一个或多个输出.不然没有输出,没有结果.你用这个算法干嘛?
有穷性: 指的是算法在执行有限的步骤以后,自动结束而不会出现无限循环,且每个步骤在可接受的时间内完成.
肯定性: 算法的每个步骤都具备肯定的含义,不能出现二义性; 算法在必定条件下,只有一条执行路径,相同的输入只能有惟一的输出结果.
可行性: 算法的每一步都必须是可行的,换句话说,每一步都能经过执行有限次数完成.
算法的正确性是指算法至少应该具备输入,输出和加工处理无歧义性,能正确反映问题的需求,可以获得问题的正确答案;
正确分为4个层次: (1).算法程序没有语法错误; (2).算法程序对于合法的输入数据可以产生知足要求的输出结果; (3).算法程序对于非法的输入数据可以得出知足规格说明的结果;(4).算法程序对于精心选择的,甚至刁钻的测试数据都有知足要求的输出结果;
可读性: 算法设计的另外一个目的是为了便于阅读,理解和交流;
可读性高有助于人们理解算法,晦涩难懂的算法每每隐含错误,且不容易发现而且难于调试和修改;
注意, 不要犯初学者的错误; 认为代码越少,就越牛逼! 若是有这样的想法, 在团队协做的今天.再也不是我的英雄主义的时代!
可读性是算法好坏的很重要的标志!
一个好的算法还应该能对输入数据的不合法的状况作出合适的处理.考虑边界性,也是在写代码常常要作的一个处理; 好比,输入时间或者距离不该该是负数;
健壮性: 当输入数据不合法时,算法也能作出相关处理,而不是产生异常和莫名其妙的结果;
生活中,人们但愿花最少的钱,用最短的时间,办最大的事. 算法也是同样的思想. 用最少的存储空间,花最少的时间,办成一样的事.就是好算法!
使用高级程序语言编写的程序在计算机上运行时所消耗的时间取决于下列的因素:
第一条,确定是算法好坏的根本; 可是第2条以及第4条.都是要看编译器的支持以及硬件性能. 也就是说,抛开这些与计算机硬件,软件有关的因素,一个程序运行时间,依赖于算法的好坏和问题的输入规模. 所谓问题输入规模是指输入量的多少.
一个算法在执行过程当中所消耗的时间取决于如下的因素:
1)依赖于输入设备的性能,如果脱机输入,则输入数据的时间能够忽略不计。(2)(3)取决于计算机自己执行的速度和编译程序的性能
习惯上将算法语句重复执行的次数做为算法的时间量度
//x=x+1; 执行1次
void add(int x){
x = x+1;
}
//x=x+1; 执行n次
void add2(int x,int n){
for (int i = 0; i < n; i++) {
x = x+1;
}
}
//x=x+1; 执行n*n次
void add3(int x,int n){
for (int i = 0; i< n; i++) {
for (int j = 0; j < n ; j++) {
x=x+1;
}
}
}
复制代码
咱们在分析一个算法的运行时,重要的是把基本操做的数量与输入规模关联起来,便是基本操做的数量必须表示成输入规模的函数;
随着n值的愈来愈大,算法在时间效率上的差别会愈来愈大;
在进行算法分析时,语句的总执行次数T(n)是关于问题规模n的函数. 进而分析T(n)随着n变化状况并肯定T(n)的数量级. 算法的时间复杂度,也就是算法的时间量度. T(n) = O(f(n)).
“它表示随问题规模n的增大,算法执行时间的增加率和f(n)的增加率相同,称做算法的渐近时间复杂度,简称为时间复杂度。其中f(n)是问题规模n的某个函数。”
大写O( )来体现算法时间复杂度的记法,咱们称之为大O记法。
刚刚的3个求和算法时间复杂度分别为O(1),O(n),O(n^2);
//1+1+1 = 3
void testSum1(int n){
int sum = 0; //执行1次
sum = (1+n)*n/2; //执行1次
printf("testSum2:%d\n",sum);//执行1次
}
复制代码
这个算法的运行次数函数是f(n) = 3;根据咱们大O时间复杂度表示,第一步先把常数项改为1. 在保留最高阶时发现,它根本没有最高阶. 因此这个算法的视觉复杂度为O(1);
//1+1+1+1+1+1+1 = 7
void testSum2(int n){
int sum = 0; //执行1次
sum = (1+n)*n/2; //执行1次
sum = (1+n)*n/2; //执行1次
sum = (1+n)*n/2; //执行1次
sum = (1+n)*n/2; //执行1次
sum = (1+n)*n/2; //执行1次
printf("testSum2:%d\n",sum);//执行1次
}
复制代码
事实上,不管常数n是多少.以上的代码执行3次仍是7次的差别,执行时间恒定.咱们的都称之为具备O(1)的时间复杂度.又称为"常数阶";
线性阶的循环结构会复杂不少. 要肯定某个算法的阶次,咱们经常须要先肯定某个特定语句或某个语句集的运行次数. 所以,咱们要分析算法的复杂度,关键就是要分析循环结构的运行状况.
void add2(int x,int n){
for (int i = 0; i < n; i++) {
x = x+1;
}
}
复制代码
这段代码的循环的视觉复杂度为O(n).
int count = 1;
while(count < n){
count = count * 2;
}
复制代码
count = count * 2 ; 每次执行这句代码,就会距离n更近一步; 也就是说, 有多少个2相乘后大于n,则会退出循环.
因此,这个循环时间复杂度为: O(logn).
/x=x+1; 执行n*n次
void add3(int x,int n){
for (int i = 0; i< n; i++) {
for (int j = 0; j < n ; j++) {
x=x+1;
}
}
}
复制代码
以上代码的循环次数为O(n^2);
//n+(n-1)+(n-2)+...+1 = n(n-1)/2 = n^2/2 + n/2
void testSum4(int n){
int sum = 0;
for(int i = 0; i < n;i++) //执行n次
for (int j = i; j < n; j++) { //执行n-i次
sum += j;
}
printf("textSum4:%d",sum);
}
复制代码
因为当i = 0,内循环执行了n次. 当i=1时,执行n-1次,......当i=n-1次,就执行1次;因此总执行次数为:n+(n-1)+(n-2)+...+1 = n(n-1)/2 = (n^2) /2 + n/2
i = 0,循环执行次数是 n 次。
i = 1,循环执行次数是 n-1 次。
i = 2,循环执行次数是 n-2 次。
...
i = n-1,循环执行的次数是 1 次。
换算成:
result = n + (n - 1) + (n - 2) … + 1
被加数递减,抽象为一个等差数列求n项和的问题,公差为1,带入公式,Sn = n(a1 + an ) ÷2
result = (n(n+1))/2 = (n^2+n)/2 = (n^2)/2 + n/2
复制代码
那咱们采用大O阶方法,第一条,没有加法常数不予考虑; 第二条,只保留最高阶项,所以保留 (n^2) /2; 第三条,去除这个相乘的常数,也就是去除1/2. 最终这段代码的时间复杂度为O(n^2);
思考: 求得如下函数的时间复杂度
指数阶O( 2^n ) 和 阶乘阶 O(n!) 等除非是很是小的n值,不然哪怕n只有100,都会形成噩梦般的运行时间. 因此这种不切实际的算法时间复杂度,通常都不会考虑且讨论.
例如,你们在查找一个n个随机数字数组中的某个数字,最好的状况是第一个数字就是, 那么算法的时间的复杂度为O(1). 但也有可能这个数字就在最后一个位置上,也就是算法时间复杂度为O(n). 这是最坏的状况了.
最坏的状况运行时间是一种保证, 那就是运行时间将不会比这更坏了. 在应用中,这是一种最重要的需求,一般除非特别指定,咱们提到的运行时间都是最坏状况下的运行时间.
而,从平均运行时也就是从几率的角度来看,这个数字在每一个位置的可能性都是相同的. 因此平均的查找时间为n/2次后会发现这个目标元素.
平均运行时间是全部状况中最有意义的,由于它是指望的运行时间. 现实中,平均运行时间都是经过必定数量的分析估算出来.
对于算法的分析,一种方法就是计算全部状况的平均值,这种时间复杂度的计算的方法称为平均时间复杂度. 另外一种方法是计算最坏的状况下时间复杂度. 这种方法称为最坏时间复杂度. 通常没有特殊状况下,都是指最坏时间复杂度.
算法设计有一个重要原则,即空间/时间权衡原则(space/time tradeoff)。
算法的空间复杂度经过计算算法所需的存储空间实现,算法空间复杂度的计算公式记作: S(n) = n(f(n)),其中,n为问题的规模,f(n)为语句关于n所占存储空间的函数.
通常状况下, 一个程序在机器上执行时,除了须要寄存自己所用的指令,常数,变量和输入数据外,还须要一些对数据进行操做的辅助存储空间. 其中,对于输入数据所占的具体存储量取决于问题自己,与算法无关. 这样**==只须要分析该算法在实现时所须要的辅助空间就能够了==**.
若是算法执行时所须要的辅助空间相对于输入数据量是一个常数,则成这个算法原地工做,辅助空间为O(1).
程序空间计算因素:
1. 寄存自己的指令
2. 常数
3. 变量
4. 输入
5. 对数据进行操做的辅助空间
在考量算法的空间复杂度,主要考虑算法执行时所须要的辅助空间.
空间复杂度计算:
问题: 数组逆序,将一维数组a中的n个数逆序存放在原数组中.
复制代码
简单理解一下下面的程序段算法空间的复杂度;
int n = 5;
int a[10] = {1,2,3,4,5,6,7,8,9,10};
//算法实现(1)
/*
算法(1),仅仅经过借助一个临时变量temp,与问题规模n大小无关,因此其空间复杂度为O(1);
*/
int temp;
for(int i = 0; i < n/2 ; i++){
temp = a[i];
a[i] = a[n-i-1];
a[n-i-1] = temp;
}
for(int i = 0;i < 10;i++)
{
printf("%d\n",a[i]);
}
//算法实现(2)
/*
算法(2),借助一个大小为n的辅助数组b,因此其空间复杂度为O(n).
*/
int b[10] = {0};
for(int i = 0; i < n;i++){
b[i] = a[n-i-1];
}
for(int i = 0; i < n; i++){
a[i] = b[i];
}
for(int i = 0;i < 10;i++)
{
printf("%d\n",a[i]);
}
复制代码
算法(1),仅仅经过借助一个临时变量temp,与问题规模n大小无关,因此其空间复杂度为O(1);
算法(2),借助一个大小为n的辅助数组b,因此其空间复杂度为O(n).
注意,算法的空间复杂度指的并非整个算法在内存占用空间,而是指的是该算法在实现时所须要的辅助空间就能够
对一个算法,其时间复杂度和空间复杂度每每会互相影响. 当追求一个较好的时间空间复杂度时,可能会致使占用较多的存储空间. 便可能会使用空间复杂度的性能变差.反之亦然. 不过,一般状况下,鉴于运算空间较为充足,人们都以算法时间空间复杂度做为算法优先的衡量指标.