什么是数据结构?什么是算法?
广义来讲,数据结构是相互之间存在一种或多种特定关系的数据元素的集合,它不仅存储数据,还支持访问和处理数据的操作,算法是为求解一个问题需要遵循的、被清楚指定的简单指令的集合。狭义来讲,是指计算机编程语言中使用的一些特定数据结构和算法,如:数组、链表、队列、栈、二叉树、图、二分查找、分治算法、动态归化等。
数据结构与算法的作用
数据结构和算法的目的是为了能够高效地帮我们解决很多实际开发中遇到的问题,尤其是对大批量数据的相关处理。
数据结构的定义
一般来讲,数据结构由逻辑结构、物理结构和数据的运算三大部分组成。数据的逻辑机构描述了对象与对象之间的关系,数据的物理结构是程序运行时必要的一种媒介,通常程序运行时必须先把这些逻辑结构转为物理结构。
常见逻辑数据结构:集合结构、线性结构、树形结构、图形结构。
数据的物理结构:顺序存储 、链式存储 (索引存储/散列存储在某种程度上也可以算作前两种)
数据结构是为算法服务,算法要作用在特定的数据结构上,一个合理的数据结构直接决定了该选择何种算法来进行处理,比如大家比较熟悉的二分查找法需要使用有序的数组来存储数据,其原因是因为数组有随机访问的特性。
常见数据结构场景:
算法的定义与衡量
算法是解决问题步骤的有限集合,通常用时间复杂度和空间复杂度来衡量算法的优劣。
算法的五大特征:输入、输出、有穷性、确定性、可行性。
- 输入:零个或多个输入
- 输出:一个或多个输出
- 有穷性:有限步骤后在可接受时间内完成
- 确定性:每个步骤都有确定含义,无二义性
- 可行性:每一步都是可行的
除了上面特征外我们还需要评鉴一个算法的好坏,那么一个算法的好坏到底该如何来衡量呢,我们可以通过下面几个方面来分析:
- 正确性
算法需要对于合法输入能够得到满足的结果,对非法输入处理,并得到合理结果;算法对于边界数据和压力数据都能得到满足的结果 - 可读性
算法要方便阅读,理解和交流,一种算法有多种实现思路,其关键点要利于理解与阅读就行日常代码开发 - 健壮性
算法不应该产生莫名其妙的结果,一会儿正确,一会儿又是其它结果,虽然有些算法可能会出现不同的统计结果,但这些概率性算法一般都有特定的业务场景,比如BloomFilter(布隆过滤)有一定的概率判断错误,不适合那些零错误的应用场景,但在能容忍低错误率的应用场合下,BloomFilter比其他常见的算法(如hash,二分查找)更能极大节省空间 - 高性价比
利用最少的时间和资源得到满足要求的结果,可以通过(时间复杂度和空间复杂度来判定)
算法复杂度分析
通常,我们对一个给定的算法需要进行两个方面的分析,第一是从数学上证明算法的正确性,这一步主要用到形式化证明的方法及相关推理模式是否符合预期输出(如:循环不变式、数学归纳法等)。另一方面是在数据准确输出的基础上分析算法的时间复杂度。算法的时间复杂度反映了程序执行时间随输入规模增长而增长的量级,在很大程度上能很好反映出算法的优劣与否。
算法执行时间需通过依据该算法编制的程序在计算机上运行时所消耗的时间来度量。而度量一个程序的执行时间通常有两种方法:
- 事后统计的方法
其方法是通过多次执行程式,记录每次的数值然后再统计分析,这种方法有两个缺陷:一是要想对设计的算法的运行性能进行评测,必须先依据算法编制相应的程序并实际运行;二是所得时间的统计量依赖于计算机的硬件、软件等环境因素,有时容易掩盖算法本身的优势。
- 事前分析估算的方法
在编写程序前,依据统计方法对算法进行估算。一个用高级语言编写的程序在计算机上运行时所消耗的时间取决于下列因素:
(1)算法采用的策略、方法 (2)编译产生的代码质量 (3)问题的输入规模 (4)机器执行指令的速度
一个算法是由控制结构(顺序、分支和循环3种)和原操作(指固有数据类型的操作)构成的,算法时间取决于两者的综合效果。为了便于比较同一个问题的不同算法,通常的做法是,从算法中选取一种对于所研究的问题(或算法类型)来说是基本操作的原操作,以该基本操作的重复执行的次数作为算法的时间量度。
- 时间复杂度
一个算法中的语句执行次数称为语句频度或时间频度我们可以记作T(n),所有代码的执行时间T(n)与代码的执行次数n成正,我们可以得到大O时间复杂度表示公式:
T(n)=O(f(n)) 其中f(n)是问题规模n的函数,也就是执行某个操作的次数。
虽然每个人写的代码千差万别,但其时间复杂度可以大致归纳为如下几个量级:常数阶Ο(1)、对数阶Ο(log2n) 、线性阶Ο(n) 、线性对数阶Ο(nlog2n) 、平方阶Ο(n2)、立方阶Ο(n3)、指数阶 Ο(2^n) 、阶乘阶Ο(n!)等
从上图我们可以看出算法时间复杂度由小到大依次为:
Ο(1) < Ο(logn) < Ο(n) < Ο(nlogn) < Ο(n^2) <Ο(n^3) < … < Ο(2^n) < Ο(n!)
在计算算法时间复杂度时有以下几个简单的程序分析法则:
- 对于一些简单的输入输出语句或赋值语句,近似认为需要O(1)时间
- 对于顺序结构,需要依次执行一系列语句所用的时间可采用大O下"求和法则"
求和法则:是指若算法的2个部分时间复杂度分别为 T1(n)=O(f(n))和 T2(n)=O(g(n)),则 T1(n)+T2(n)=O(max(f(n), g(n))) 特别地,若T1(m)=O(f(m)), T2(n)=O(g(n)),则 T1(m)+T2(n)=O(f(m) + g(n))
- 对于选择结构,如if语句,它的主要时间耗费是在执行then字句或else字句所用的时间,需注意的是检验条件也需要O(1)时间
- 对于循环结构,循环语句的运行时间主要体现在多次迭代中执行循环体以及检验循环条件的时间耗费,一般可用大O下"乘法法则"
乘法法则: 是指若算法的2个部分时间复杂度分别为 T1(n)=O(f(n))和 T2(n)=O(g(n)),则 T1T2=O(f(n)g(n))
- 对于复杂的算法,可以将它分成几个容易估算的部分,然后利用求和法则和乘法法则技术整个算法的时间复杂度
另外还有以下2个运算法则:(1) 若g(n)=O(f(n)),则O(f(n))+ O(g(n))= O(f(n));(2) O(Cf(n)) = O(f(n)),其中C是一个正常数
- 空间复杂度
类似于时间复杂度的讨论,空间复杂度(Space Complexity) 是对一个算法在运行过程中临时占用存储空间大小的度量。一个算法所耗费的存储空间我们可以记作S(n),算法的存储空间与问题规模n成正比。渐近空间复杂度也常常简称为空间复杂度。我们可以用大S空间复杂度表示公式:
S(n)=O(f(n)) 其中f(n)是问题规模n的函数。
一个算法在计算机存储器上所占用的存储空间,包括存储算法本身所占用的存储空间,算法的输入输出数据所占用的存储空间和算法在运行过程中临时占用的存储空间这三个方面。如果额外空间相对于输入数据量来说是个常数,则称此算法是原地工作。
算法的输入输出数据所占用的存储空间是由要解决的问题决定的,是通过参数表由调用函数传递而来的,它不随本算法的不同而改变。存储算法本身所占用的存储空间与算法书写的长短成正比,要压缩这方面的存储空间,就必须编写出较短的算法。
下面列举几个空间复杂度计算示例
1)复杂度Ο(1) 常数级别,将两个数相加
int i = 8;
int j = 6;
int sum = i + j;
2)复杂度O(n^2) 平方级别,双层循序
sum=0;
for(i=1;i<=n;i++) {
for(j=1;j<=n;j++) {
sum++;
...
}
}
3)复杂度O(n) 线性级别,数据交换
a=0;
b=1;
for (i=1;i<=n;i++)
{
s=a+b;
b=a;
a=s;
}
4)复杂度O(log2^n) 线性对数级别
i=1;
while (i<=n) {
i=i*2;
}
常见算法时间复杂度和空间复杂度列表
常见算法有查找和排序两种,其中查找是计算机数据处理经常用到的一种重要应用,当需要反复在海量数据中查找制定记录时,查找效率成为系统性能的关键。查找算法分为静态查找和动态查找,其中静态查找包括:顺序查找、二分查找和分块查找;动态查找包括:二叉排序树和平衡二叉树。
常用的排序算法有:插入排序、冒泡排序、堆排序、选择排序和归并排序等,其时间、空间复杂度如下表所示:
动图演示:
Tips:数据结构和算法动态可视化 https://visualgo.net/zh
具体实现示例:
- 冒泡排序
//冒泡排序
public static int[] bubbleSort(int[] array) {
int temp;
for (int i = 0; i < array.length - 1; ++i) {
for (int j = 0; j < array.length - i - 1; ++j) {
if (array[j] > array[j + 1]) {
temp = array[j];
array[j] = array[j + 1];
array[j + 1] = temp;
}
}
}
return array;
}
- 插入排序
//插入排序
public static int[] insertSort(int[] array) {
int i, j;
int target;
for (i = 1; i < array.length - 1; i++) {
j = i;
target = array[i];
while (j > 0 && target < array[j - 1]) {
array[j] = array[j - 1];
j--;
}
array[j] = target;
}
return array;
}
- 选择排序
//选择排序
public static int[] selectSort(int[] array) {
for (int i = 0; i < array.length; i++) {
//最小元素的下标,初始指向第一个元素
int k = i;
for (int j = i + 1; j < array.length; j++) {
//如果当前元素小于最小元素,则将此元素设置成为新的最小值
if (array[j] < array[k]) {
//下标指向k
k = j;
}
}
//交换位置
if (k > i) {
int temp = array[i];
array[i] = array[k];
array[k] = temp;
}
}
return array;
}
上述列表是《数据结构》当中非常基础的知识点,但只掌握这些基础知识往往还不够,需要更深入系统的学习才能把数据结构与算法融会贯通,下图是网络课程给的一张数据结构算法知识图谱,里面几乎涵盖了所有数据结构和算法书籍中都会讲到的知识点。
课程提到日常互联网公司常提到的10个数据结构和10个基本算法:
数据结构: 数组、链表、栈、队列、散列表、二叉树、堆、跳表、图、Trie 树;
算法:递归、排序、二分查找、搜索、哈希算法、贪心算法、分治算法、回溯算法、动态规划、字符串匹配算法
掌握了这些数据结构和算法,在学习其它更复杂的数据结构和算法会更容易些,更快些。
学习算法是一个长期的过程,需要不断巩固和练习,提供一个算法学习网站: http://leetcode.com 上面有一些常见的算法实现,可以根据需要自行检索。
内容比较多,本次就先总结这里,下次对具体数据结构和算法做分布拆解。