不管是在公众号还是在社区或者技术讨论群、各种岗位要求相信大家都会看到“数据结构与算法“,一说起数据结构和算法相信大家都会有那么一丝说不清道不明的感觉,。我之前在大学中接受过数构的课程,在后来的工作面试中包括工作中都用到了很多的数据机构比如List、Set、Queue、Map等,但是要是让我给一个人讲解这些数据结构和实现原理的时候,却发现一直自己都是出于懵懵懂懂的状态,趁着最近工作有时间系统的学一下数构。闲话到此,上正文。
数据(Data)是描述客观事物的数值、字符以及能输入到机器被处理的各种符号的集合。
是不是感觉很绕口,其实数据的定义非常广泛,除了数值数据、字符、字符串是数据以外,声音、图像等一切可以输入到计算机并且能被处理的都是数据。
例如人的名字、身高、体重等的字符、数字是数据,人的照片、指纹、语音、三维模型也算是数据。到此是否对"数据"是不是有了初步的认识呢。
数据由三部分组成:
数据项: 具有原子性是数据的最小数据单位,相当于一条记录的一个个字段(年龄、性别、姓名)
数据元素:是数据的基本单位,数据集合的个体由多个数据项组成,在计算机程序中通常作为一个整体进行处理。例如描述一个学生的完整信息的数据记录就是一个数据元素。空间中的一点三维坐标也算是一个数据元素。
数据对象: 是性质相同的数据元素的集合,是数据在子集。例如一个学校的所有学生的集合就是数据对象。空间中所有点的结合也是数据对象。
由于信息可以存在于逻辑思维领域,也可以存在于计算机世界。因为作为信息载体的数据也可以存在这两个世界中,表示一组数据元素及其相互关系的数据结构同样有两种不同的表现形式。
一种是数据结构的逻辑层面,即数据的逻辑结构。
数据的逻辑结构 = 逻辑结构 + 存储结构
一种是存在于计算机的物理层面,即数据的存储结构
数据的存储结构 = 逻辑结构 + 存储结构 + 运算/操作
了解了数据的基本概念,我们先了解一下数据结构的三个层面,扫几眼熟悉一下。
线性结构: 有且只有开始节点和一个终端节点,并且所有的节点只有一个直接前驱和一个直接后继。
线性表就是一个典型的线性结构,他有四个基本特征:
简单粗暴来举个例子:就比如一串正常的糖葫芦,每颗糖葫芦前后只有一个,第一个和最后一个糖葫芦都只连接了一个(其他形状的糖葫芦除外,别抬杠啊)。
非线性的结构:概念与线性结构相反,元素之间不是一对一的关系,是一对多的关系。
比如:地铁线路图,交通图,windows或者Linux的文件系统等等都是非线性结构。
逻辑结构有四种基本类型:集合结构、线性结构、树状结构、网络结构
网络结构和树状结构是两种比较高效的数据结构,许多高效的算法都是用这两种数据结构。
集合结构:就是数学中所学习的集合。集合中的元素有三个特征:
1).确定性(集合中的元素必须是确定的)
2).唯一性(集合中的元素互不相同。例如:集合A={1,a},则a不能等于1)
3).无序性(集合中的元素没有先后之分),如集合{3,4,5}和{3,5,4}算作同一个集合
该结构的数据元素间的关系是"属于同一个集合",别无其它关系。
因为集合中元素关系很弱,数据结构中不对该结构进行研究
线性结构:数据结构中线性结构指的是数据元素之间存在着"一对一"的线性关系的数据结构。
特点是元素之间是1对1的联系
树状结构:除了一个数据元素(元素 01)以外每个数据元素有且仅有一个直接前驱元素,但是可以有 多个直接后续元素。
特点是数据元素之间是 1 对 多的联系
网络结构:每个数据元素可以有多个直接前驱元素,也可以有多个直接后续元素。
特点是数据元素之间是多对多的联系
数据的存储结构主要包括数据元素本身的存储和元素之间的对应关系表示,是数据的逻辑结构在计算机中的表示。
常见的存储结构:顺序存储、链式存储、索引存储、散列存储。
把逻辑上相邻的节点存储在物理位置上相邻的存储单元中,结点之间的逻辑关系由存储单元的邻接关系来体现。
由此得到的存储结构为顺序存储结构,通常顺序存储结构是借助于计算机程序设计语言(例如C/C++)的数组来描述的。
(数据元素的存储对应于一块连续的存储空间,数据元素之间的前驱和后续关系通过数据元素,在存储器中的相对位置来反映)
优点:
1.节省存储空间,因为分配给数据的存储单元全用存放结点的数据(不考虑c/c++语言中数组需指定大小的情况),结点之间的逻辑关系没有占用额外的存储空间。
2.采用这种方法时,可实现对结点的随机存取,即每一个结点对应一个序号,由该序号可以直接计算出来结点的存储地址。
缺点:
1.插入和删除操作需要移动元素,效率较低。
2.必须提前分配固定数量的空间,如果存储元素少,可能导致空闲浪费。
数据元素的存储对应的是不连续的存储空间,每个存储节点对应一个需要存储的数据元素。
每个结点是由数据域和指针域组成。 元素之间的逻辑关系通过存储节点之间的链接关系反映出来。 逻辑上相邻的节点物理上不必相邻。
缺点:
1、比顺序存储结构的存储密度小 (每个节点都由数据域和指针域组成,所以相同空间内假设全存满的话顺序比链式存储更多)。
2、查找结点时链式存储要比顺序存储慢。
优点:
1、插入、删除灵活 (不必移动节点,只要改变节点中的指针)。
2.有元素才会分配结点空间,不会有闲置的结点。
感觉每学习一个知识点都要从基础的开始吧,高层建筑还是需要底层维护的,温故而知新,每次都有新的收获。说起算法(algorithm),很多人都感觉这是个很高大上的word,它确实很高大上,咱们先学点基础的。算法到底是个啥,我们看看官方定义“它是指令的集合,是为解决特定问题而规定的一系列操作。”,然后说点大白话,算法就是针对某一类特定的问题,我们想到一个方法去解决它,而这个解题过程,其实就是在使用某种算法。
比如:1 + 2 + 3 + 4 + 5 + …100=?
这些解决方案其实一个个算法,可以看出用不同的算法可以得出同一个结果,那么就可以得出算法的几个特性:
但是这里就会有一个问题,解决方案那么多,我们怎么去判断哪个算法最好呢?
好吧,不卖关子了,评价一个算法好坏的依据是复杂度(时间和空间复杂度)
时间复杂度:是指执行算法所需要的计算工作量,就是我们从1加到100加了多少次。
空间复杂度:是指执行这个算法所需要的内存空间,在代码中我们执行这个加法占了多少内存,如果我们在纸上算,用了多少张纸,哈哈。
有的时候看到这些概念,真的很头疼,大致扫几眼就去看自认为核心的内容,其实在其后面的内容中都是基于前面的几个特性,死记硬背不是好的方式,最好是深入人心吧,反复看几遍。
一个算法执行所耗费的时间,从理论上是不能算出来的,必须上机运行测试才能知道。 但我们不可能也没有必要对每个算法都上机测试。 一个算法花费的时间与算法中语句的执行次数成正比例,哪个算法中语句执行次数多,它花费时间就多。
一个算法中的语句执行次数称为语句频度或时间频度,表示为T(n),n表示问题的规模,比如上个栗子,从1加到100,加了多少次,那么这个就是这个算法的时间频度
但有时我们想知道它变化时呈现什么规律,想知道问题的规模,而不是具体的次数,此时引入时间复杂度。
一般情况下,算法中基本操作重复执行的次数是问题规模n的某个函数,用T(n)表示,
若有某个辅助函数f(n),使得当n趋近于无穷大时,T(n)/f(n)的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数。
记作T(n)=O(f(n)),称O(f(n)) 为算法的渐进时间复杂度,简称时间复杂度。
T(n)=O(f(n))
或者说:时间复杂度就是时间频度去掉低阶项和首项常数。
注意:时间频度与时间复杂度是不同的,时间频度不同但时间复杂度可能相同。
如果没有数学基础的同学,看起来这个可能会有点迷糊,举个栗子:
比如某两个算法的时间频度为:
T(n) = 100000n2+10n+6 (T(n)就是执行次数)
T(n) = 10n2+10n+6
那么时间频度为T(n) = n2,为什么呢,因为如果n越来越大的时候,10n+6的影响就越来越小,所以这里我们去除它的低阶项10n和常数项6。
但是时间复杂度都是T(N)=O(n2),这里就对应了上面标红的话。
最坏时间复杂度和平均时间复杂度
最坏情况下的时间复杂度称最坏时间复杂度。一般不特别说明,讨论的时间复杂度均是最坏情况下的时间复杂度。
这样做的原因是:最坏情况下的时间复杂度是算法在任何输入实例上运行时间的上界,这就保证了算法的运行时间不会比任何更长。
在最坏情况下的时间复杂度为T(n)=O(n),它表示对于任何输入实例,该算法的运行时间不可能大于O(n)。拿上面的从1加到100的栗子来说,我们最坏的情况就是f(n)=1+2+3+4+…+n(无穷无尽的加啊,难受不?),那么它的时间频度就是T(n)=n,而T(n)=O(f(n)),也就是T(n)=O(n),所以他就是最坏的时间复杂度(考虑到一些同学的数学基础不是很好,我说的详细点)
然后为了更好的说明时间复杂度,定了三个符号。
时间复杂度计算
根本没有必要计算时间频度,即使计算处理还要忽略常量、低次幂和最高次幂的系数,所以可以采用如下简单方法:
终于可以上代码了。。。。。。。。。。。。。
一个简单语句的时间复杂度为O(1)。
int count = 0 ;
100个简单语句的时间复杂度也为O(1)。(100是常数,不是趋向无穷大的n)
int count=0;
一个循环的时间复杂度为O(n),T(n)=O(n)。
int n=8, count=0;
for (int i=1; i<=n; i++)
count++;
时间复杂度为O(log2 n)的循环语句。
int n=8, count=0;
for (int i=1; i<=n; i*=2)
count++;
1 2 4 8 16 32
230=1024 * 1024 * 1024 = 1000 * 1000 * 1000=10亿
时间复杂度为O(n2)的二重循环。
int n=8, count=0;
for (int i=1; i<=100n; i++)
for (int j=1; j<=10n; j++)
count++;
时间复杂度为O(nlog2n)的二重循环。
int n=8, count=0;
for (int i=1; i<=n; i*=2)
for (int j=1; j<=n; j++)
count++;
时间复杂度为O(n2)的二重循环。
int n=8, count=0;
for (int i=1; i<=n; i++)
for (int j=1; j<=i; j++)
count++;
1+2+3+4…+n=(1+n)*n/2
需要复杂些数学运算:1+2+3+…+n=(n+1)*n/2 时间复杂度是 O(n2)
查找和排序算法时会大量的设计时间复杂度,作为选择查找和排序算法的重要依据
常用时间复杂度级别:
常数阶O(1)
对数阶O(log2n)
线性阶O(n)
线性对数阶O(n*log2n)
平方阶O(n2)
立方阶O(n3)
…
k次方阶O(nk)
指数阶O(2n)
阶乘阶O(n!)
上面的各种时间复杂度级别,执行效率越来越地下。
算法的存储量包括:
程序本身所占空间
输入数据所占空间;
辅助变量所占空间
输入数据所占空间只取决于问题本身,和算法无关,则只需要分析除输入和程序之外的辅助变量所占额外空间。空间复杂度是对一个算法在运行过程中临时占用的存储空间大小的量度,一般也作为问题规模n的函数,以数量级形式给出,记作:
S(n) = O(g(n))
空间复杂度分析1:
int fun(int n){
int i,j,k,s;
s=0;
for (i=0;i<=n;i++)
for (j=0;j<=i;j++)
for (k=0;k<=j;k++)
s++;
return(s);
}
由于算法中临时变量的个数与问题规模n无关,所以空间复杂度均为S(n)=O(1)。
空间复杂度分析2:
void fun(int a[],int n,int k)
//数组a共有n个元素
{ int i;
if (k==n-1)
for (i=0;i<n;i++)
printf("%d\n",a[i]); //执行n次
else
{ for (i=k;i<n;i++)
a[i]=a[i]+i*i; //执行n-k次
fun(a,n,k+1);
}
}
每次调用本身都要给数组分配空间,fun(a,n,0)的空间复杂度为O(n)。
注意:
1.空间复杂度相比时间复杂度分析要少
2.对于递归算法来说,代码一般都比较简短,算法本身所占用的存储空间较少,但运行时需要占用较多的临时工作单元;
若写成非递归算法,代码一般可能比较长,算法本身占用的存储空间较多,但运行时将可能需要较少的存储单元。
限于篇幅限制,暂时就分享这些。本博客文章皆出于学习目的,个人总结或摘抄整理自网络。引用参考部分在文章中都有原文链接,如疏忽未给出请联系本人。另外,作为一名还在处于摸索的攻城狮,如文章内容有错误,欢迎各方大神指导交流。下一章主要分享一下常用的数据结构和实现代码,