数据结构与算法

文章发布时间:

最后更新时间:

数据结构概述

数据结构是指相互之间存在一种或多种特定关系数据元素的集合

逻辑结构

数据对象中数据元素之间的相互关系
集合结构:数据元素之间的唯一关系就是属于同一个集合
线性结构:数据元素之间存在一对一的关系(除首尾元素均存在前驱和后继)
树形结构:数据元素之间存在一对多的关系
图形结构:数据元素之间存在多对多的关系

  • (每一个数据元素看做一个节点,元素之间的逻辑关系用连线表示,如果关系有方向则连线带箭头)

物理结构(存储结构)

数据的逻辑结构关系在计算机中的存储形式
顺序存储结构:把元素分别放在地址连续的存储单元中的存储方式

  • 也就是说:元素一个一个有序的排好队,各自占据一定的空间,例如定义一个含有6个浮点型数据的数组:然后内存中的一块大小为6个浮点型数据大小空间就会被计算机所开辟,然后数据存入时,依次顺序摆入


链式存储结构:把元素存储在任意的存储单元中的存储方式

  • 因为数据元素位置不确定,所以需要通过指针指向到元素的存储地址,从而确定不同数据元素之间的位置


散列 (哈希) 存储方式:是一种力图将数据元素的存储位置与关键码之间建立确定对应关系的查找技术

  • 它的原理就是,将一个节点的关键字key作为自变量,通过一个确定的函数运算f(key),其函数值作为节点的存储地址,将节点存入到指定的位置上,查找的时候,被搜索的关键字会再次通过f(key)函数计算地址,然后读取对应数据

索引存储方式:存储时,除了存储节点,还附加建立了索引表来表示节点的地址

算法概述

算法的特征

  • 输入:算法具有零个或者多个输入(零个的情况例如打印输出字符串,或者算法自身已经给定了初始条件)
  • 输出:算法具有一个或者多个输出,用来反映算法对输入数据加工后的结果
  • 有穷性:算法必须在执行有限个步骤后终止,“有限” 的定义不是绝对的,而是实际应用中合理的可接受的
  • 确定性: 算法的每一步骤都具有确定的含义,不会出现二义性
    • 也就是说,唯一的输入只有唯一的输出
  • 可行性:算法的每一步都是可行的,通过有限步骤可以实现

算法的设计要求

  • 正确性:合理的数据输入下,最终可以输出能解决问题需求的正确答案
    • 对正确的理解:
      • 无语法错误
      • 输入合法和非法的数据均可以得到正确答案
      • 输入刁难的数据依旧可以输出满足需要的答案
  • 可读性:算法便于阅读和理解
    • 算法应该层次分明,易读易懂,方便二次调试和修改
    • 复杂一些的算法,变量的命名尽量恰当一些,用阿里的开发手册中的一句话就是说:“正确的英文拼写和语法可以让阅读者易与理解避免歧义”“为了达到代码自解释的目标,任何自定义编程元素在命名时,使用尽量完整的单词组合来表达其意”
  • 健壮性:当数据不合理的时候,算法也能对各种情况作出处理,而不是报出异常,或者输出错误的答案
  • 高效性:尽量满足时间效率高,存储率低的需求

算法的时间复杂度

时间复杂度通常用来衡量算法的运行时间
算法的时间复杂度是一个函数 T(n),它定性描述该算法的运行时间,通常用大O表示法。
记作:T(n) = O(f(n)) 例如3n² + 2n + 1 的时间复杂度为 O(n²)
常见的时间复杂度量级有:

  • 常数阶O(1)
  • 对数阶O(logN)
  • 线性阶O(n)
  • 线性对数阶O(nlogN)
  • 平方阶O(n²)
  • 立方阶O(n³)
  • K次方阶O(n^k)
  • 指数阶(2^n)

上面从上至下依次的时间复杂度越来越大,执行的效率越来越低。

常数阶O(1)

无论代码执行了多少行,只要是没有循环等复杂结构,那这个代码的时间复杂度就都是O(1),如:

1
2
3
4
5
int i = 1;
int j = 2;
++i;
j++;
int m = i + j;

线性阶O(n)

1
2
3
4
5
for(i=1; i<=n; ++i)
{
j = i;
j++;
}

这段代码,for循环里面的代码会执行n遍,因此它消耗的时间是随着n的变化而变化的,因此这类代码都可以用O(n)来表示它的时间复杂度。

对数阶O(logN)

1
2
3
4
5
int i = 1;
while(i<n)
{
i = i * 2;
}

在while循环里面,每次都将 i 乘以 2,乘完之后,i 距离 n 就越来越近了。那么假设循环x次之后,i 就大于 2 了,此时这个循环就退出了。也就是说有 x 个 2 相乘后大于 n,则会退出循环。表示出来就是 2^x=n,得到 x = log2^n。因此这个代码的时间复杂度为:O(logn)

线性对数阶O(nlogN)

1
2
3
4
5
6
7
8
for(m=1; m<n; m++)
{
i = 1;
while(i<n)
{
i = i * 2;
}
}

将时间复杂度为O(logn)的代码循环N遍的话,那么它的时间复杂度就是 n * O(logN),也就是了O(nlogN)。

平方阶O(n²)

1
2
3
4
5
6
7
8
for(x=1; i<=n; x++)
{
for(i=1; i<=n; i++)
{
j = i;
j++;
}
}

这段代码其实就是嵌套了2层n循环,它的时间复杂度就是 O(n*n),即 O(n²)
如果将其中一层循环的n改成m,即:

1
2
3
4
5
6
7
8
for(x=1; i<=m; x++)
{
for(i=1; i<=n; i++)
{
j = i;
j++;
}
}

那它的时间复杂度就变成了 O(m*n)

立方阶O(n³)K次方阶O(n^k)

参考上面的O(n²) 去理解就好了,O(n³)相当于三层n循环,其它的类似。

算法的空间复杂度

空间复杂度是对一个算法在运行过程中临时占用存储空间大小的一个量度,同样反映的是一个趋势,我们用 S(n) 来定义。
空间复杂度比较常用的有:O(1)、O(n)、O(n²)

时间复杂度大小排序

O(1) < O(logn) < O(n) < O(nlogn) < O(n²) < O(n³) < O(2^n) < O(n!) < O(n^n)

空间复杂度 O(1)

如果算法执行所需要的临时空间不随着某个变量n的大小而变化,即此算法空间复杂度为一个常量,可表示为 O(1)

1
2
3
4
5
int i = 1;
int j = 2;
++i;
j++;
int m = i + j;

代码中的 i、j、m 所分配的空间都不随着处理数据量变化,因此它的空间复杂度 S(n) = O(1)

空间复杂度 O(n)

1
2
3
4
5
6
int[] m = new int[n]
for(i=1; i<=n; ++i)
{
j = i;
j++;
}

这段代码中,第一行new了一个数组出来,这个数据占用的大小为n,这段代码的2-6行,虽然有循环,但没有再分配新的空间,因此,这段代码的空间复杂度主要看第一行即可,即 S(n) = O(n)

image.png
image.png