首页 > 技术文章 > 数据结构

LZXX 2022-06-13 16:12 原文

数据结构

线性结构:数据在内存上连续摆放.一开始就分配了空间,支持索引访问 元素之间的关系是一对一的 如:数组,链表  

非线性结构:不是连续摆放,不支持索引访问,不限制长度空间,每个元素可能连接0或多个元素 增加删除简单,访问慢。如树,图。

Array数组:一开始就分配空间,因为有索引所以查询快,但是增加删除就慢,这种操作会重新开辟1个空间,coppy过去然后再把增删的数据放过来。

ArrayList动态数组:在堆中,第一次的时候分配4个元素空间,但是如果第一次新增五个进去,它就会翻倍的增加空间变成10个,这样就会比Array浪费空间,有装箱拆箱操作因为数据默认保持在 object?[]里面

泛型动态数组:和动态数组区别在于泛型,避免装箱拆箱

sortList排序数组,在add的时候就会做好排序,相对性能也会有影响

stack<T>栈:先进后出,使用的保持也是数组,使用Push增加数据,初始化分配4个元素空间,超过就翻倍。场景:回文验证,abcbca,把数据放栈,然后取出来直接和原文比对,就可以判断这个单词是不是回文

query队列:底层也是数组结构,先进先出,优先级队列C#10.0加的 PridityQyeye,插入优先。

以上其实都是对数组的包装,之所以有这么多不同的数据结构,也就是为了满足不同的需求

链表和数组对比:

1.链表可以遍历,但不能索引访问

2.数组增删慢,查询快,链表增删快,查询慢,因为查询某个元素,只能重头查找, 

3.数组比链表节省空间,因为链表里面会存引用指向下一个位置

4.数组可以二分查询

哈希表:在C#中底层存储在数组+线性结构 。开辟一个数组,新增的时候通过对Key哈希再取余(哈希计算定位),找到数组中的位置,存进去,出现哈希冲突之后进行双重散列,数组+链表方式

字典:增删改查都很快,泛型,有线程安全问题

集合:里面的成员是无序的,去重。所以可以做数据随机,IP投票

数据结构在内存中的分配

一个进程就是分配一个堆,堆里面的数据是连续紧密摆放的,线性or非线性,其实内存都不一定在一起,只有数组是连续的。链表-树-图的对象其实都是随机分布的--而数据结构只是封装了他的访问方式。认为组织了一下数据的关系,而不是物理上的关系。

数组-在物理上是在一起的,但是封装了不同的API,如堆栈队列等都是对数组的封装所以在物理上是在一起的,人为封装了API,所以调用的时候就是有先进先出等访问方式

所谓数据结构,跟物理存储没关系-只是为了简化算法。就像预制菜, 提前组装好菜品,为了更方便的使用 微波炉,蒸,煮快速出菜。

 

二叉树:二叉查找,左边小,右边大,查MAX,查min,顺序查,删除,插入

AVL树:二叉排序树升级>二叉平衡树,左右两个字树的高度差不能大于1怎么保证呢,添加节点完后做节点旋转

红黑树:AVL树升级>红色黑树,相对于AVL树少了选择,相对于二叉树,是黑色完美平衡树,任务节点去到叶节点,经历过的黑色树的数量都是一样的,但是删除和插入麻烦,又要旋转,又要着色。一切都是为了更快的查找 就像Redis一样为了查找快,所以插入麻烦

 

跳跃表:多层的有序链表 2.由高层到底层,3.任何节点都包含首尾节点4.最底层节点包含所有元素,5.每个节点都包含两个指针,一个是指向同一层下个节点和下一层的下一个节点。

插入从底层开始插入,定位位置,确定数据插入的层数(搞个随机数,满足条件就插入),然后自下往上一层层插入即可,最高32层

单层链表:有序

有序链表:有序,先分段

多层链表:

 

算法

  • 字符串:暴力匹配、BM、KMP、Trie等。
  • 查找:二叉查找、遍历查找等。对热点数据查找优化:自组织查找,每次查到该数据就往前移,热点数据查的越多,那么位置越靠前,查询的速度就越快,可以加个28原则,如果热点数据索引前面20%就不动,如果在80%就往前移动
  • 排序:冒泡排序、快排、计数排序、堆排序等。 每个排序算法都有相适应的场景,排序的时候对原数据移动越少,性能也就越高

二叉查找树:左树不为空,所有节点值<跟节点。右树不为空,所有节点值>跟节点.可以快速的找到MAX,min的值,因为 一直往左是最小的,一直往右是最大的

 

 

2 如何衡量算法好坏?

  • 时间复杂度:运行时间长短。
  • 空间复杂度:占用内存大小。

3 怎么计算时间复杂度?

大O表示法(渐进时间复杂度):把程序的相对执行时间函数T(n)简化为一个数量级,这个数量级可以是n、n^2、logN等。

推导时间复杂度的几个原则:

  • 如果运行时间是常数量级,则用常数1表示。
  • 只保留时间函数中的最高阶项。
  • 如果最高阶项存在,则省去最高项前面的系数。

时间复杂度对比:O(1) > O(logn) > O(n) > O(nlogn) > O(n^2)。

不同时间复杂度算法运行次数对比:

 

image.png

 

4 怎么计算空间复杂度?

常量空间 O(1):存储空间大小固定,和输入规模没有直接的关系。

线性空间 O(n):分配的空间是一个线性的集合,并且集合大小和输入规模n成正比。

二维空间 O(n^2):分配的空间是一个二维数组集合,并且集合的长度和宽度都与输入规模n成正比。

递归空间 O(logn):递归是一个比较特殊的场景。虽然递归代码中并没有显式的声明变量或集合,但是计算机在执行程序时,会专门分配一块内存空间,用来存储“方法调用栈”。执行递归操作所需要的内存空间和递归的深度成正比。

5 如何定义算法稳定性?

稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面。

不稳定:如果a原本在b的前面,而a=b,排序之后 a 可能会出现在 b 的后面。

6 有哪些常见算法?

首先要明确:特定算法解决特定问题。

  • 字符串:暴力匹配、BM、KMP、Trie等。
  • 查找:二分查找、遍历查找等。
  • 排序:冒泡排序、快排、计数排序、堆排序等。
  • 搜索:TFIDF、PageRank等。
  • 聚类分析:期望最大化、k-meanings、k-数位等。
  • 深度学习:深度信念网络、深度卷积神经网络、生成式对抗等。
  • 异常检测:k最近邻、局部异常因子等。
  • ......

其中,字符串、查找、排序算法是最基础的算法。

四 常见数据结构

1 数组

1)什么是数组?

数据是有限个相同类型的变量所组成的有序集合。数组中的每一个变量被称为元素。

 

image.png

 

2)数组的基本操作?

读取O(1)、更新O(1)、插入O(n)、删除O(n)、扩容O(n)。

2 链表

1)什么是链表?

链表是一种在物理上非连续、非顺序的数据结构,由若干个节点组成。

单向链表的每一个节点又包含两部分,一部分是存放数据的变量data,另一部分是指向下一个节点的指针next。

 

image.png

 

2)链表的基本操作?

读取O(n)、更新O(1)、插入O(1)、删除O(1)。

3)链表 VS 数组

数组:适合多读、插入删除少的场景。

链表:适用于插入删除多、读少的场景。

 

image.png

 

3 栈

1)什么是栈?

栈是一种线性逻辑数据结构,栈的元素只能后进先出。最早进入的元素存放的位置叫做栈底,最后进入的元素存放的位置叫栈顶。

一个比喻,栈是一个一端封闭一端的开放的中空管子,队列是两端开放的中空管子。

 

image.png

 

2)如何实现栈?

数组实现:

 

image.png

 

链表实现:

image.png

 

3)栈的基本操作

入栈O(1)、出栈O(1)。

4)栈的应用?

  • 回溯历史,比如方法调用栈。
  • 页面面包屑导航。

4 队列

1)什么是队列?

一种线性逻辑数据结构,队列的元素只能后进后出。队列的出口端叫做队头,队列的入口端叫做队尾。

 

image.png

 

2)如何实现队列?

数组实现:

 

image.png

 

链表实现:

 

image.png

 

3)队列的基本操作?

入队 O(1)、出队 O(1)。

4)队列的应用

  • 消息队列
  • 多线程的等待队列
  • 网络爬虫的待爬URL队列

5 哈希表

1)什么是哈希表?

一种逻辑数据结构,提供了键(key)和值(value)的映射关系。

 

image.png

 

2)哈希表的基本操作?

写入:O(1)、读取:O(1)、扩容O(n)。

3)什么是哈希函数?

哈希表本质上是一个数组,只是数组只能根据下标,像a[0] a[1] a[2] a[3] 这样来访问,而哈希表的key则是以字符串类型为主的。

通过哈希函数,我们可以把字符串或其他类型的key,转化成数组的下标index。

如给出一个长度为8的数组,则:

当key=001121时,

index = HashCode ("001121") % Array.length = 7

当key=this时,

index = HashCode ("this") % Array.length = 6

 

image.png

 

4)什么是哈希冲突?

不同的key通过哈希函数获得的下标有可能是相同的,例如002936这个key对应的数组下标是2,002947对应的数组下标也是2,这种情况就是哈希冲突。

image.png

 

5)如何解决哈希冲突?

开放寻址法:例子Threadlocal。

 

image.png

 

链表法:例子Hashmap。

 

image.png

 

6 树

1)什么是树?

树(tree)是n(n≥0)个节点的有限集。

当n=0时,称为空树。在任意一个非空树中,有如下特点:

    • 有且仅有一个特定的称为根的节点。

  • 当n>1时,其余节点可分为m(m>0)个互不相交的有限集,每一个集合本身又是一个树,并称为根的子树。

2)树的遍历?

(1)深度优先

前序:根节点、左子树、右子树。

 

image.png

 

中序:左子树、根节点、右子树。

 

image.png

 

后序:左子树、右子树、根节点。

 

image.png

 

实现方式:递归或栈。

(2)广度优先

层序:一层一层遍历。

 

image.png

 

实现方式:队列。

7 二叉树

1)什么是二叉树?

二叉树(binary tree)是树的一种特殊形式。二叉,顾名思义,这种树的每个节点最多有2个孩子节点。注意,这里是最多有2个,也可能只有1个,或者没有孩子节点。

2)什么是满二叉树?

一个二叉树的所有非叶子节点都存在左右孩子,并且所有叶子节点都在同一层级上,那么这个树就是满二叉树。

3)什么是完全二叉树?

对一个有n个节点的二叉树,按层级顺序编号,则所有节点的编号为从1到n。如果这个树所有节点和同样深度的满二叉树的编号为从1到n的节点位置相同,则这个二叉树为完全二叉树。

 

image.png

 

8 二叉查找树

1)什么是二叉查找树?

二叉查找树在二叉树的基础上增加了以下几个条件:

    • 如果左子树不为空,则左子树上所有节点的值均小于根节点的值。

  • 如果右子树不为空,则右子树上所有节点的值均大于根节点的值。
  • 左、右子树也都是二叉查找树。

 

image.png

 

2)二叉查找树的作用?

  • 查找==》二分查找。
  • 排序==》中序遍历。

3)二叉树的实现方式?

  • 链表。
  • 数组:对于稀疏二叉树来说,数组表示法是非常浪费空间的。

9 二叉堆

1)什么是二叉堆?

二叉堆是一种特殊的完全二叉树,它分为两个类型:最大堆和最小堆。

    • 最大堆的任何一个父节点的值,都大于或等于它左、右孩子节点的值。

  • 最小堆的任何一个父节点的值,都小于或等于它左、右孩子节点的值。

 

image.png

 

2)二叉堆的基本操作?

(1)插入:插入最末,节点上浮。

 

image.png

 

(2)删除:删除头节点,尾节点放到头部,再下沉。

 

image.png

 

(3)构建二叉堆:二叉树==》二叉堆,所有非叶子节点依次下沉。

 

image.png

 

3)二叉堆的实现方式?

数组:

image.png

 

五 常见排序算法

1 十大经典排序算法

 

image.png

 

2 冒泡排序

1)算法描述

冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。

2)实现步骤

 

image.png

 

 

image.png

 

    • 比较相邻的元素。如果第一个比第二个大,就交换它们两个。

  • 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数。
    • 针对所有的元素重复以上的步骤,除了最后一个。

  • 重复步骤1~3,直到排序完成。

3)优缺点

    • 优点:实现和理解简单。

  • 缺点:时间复杂度是O(n^2),排序元素多时效率比较低。

4)适用范围

数据已经基本有序,且数据量较小的场景。

5)场景优化

(1)已经有序了还再继续冒泡问题

  • 本轮排序中,元素没有交换,则isSorted为true,直接跳出大循环,避免后续无意义的重复。

(2)部分已经有序了,下一轮的时候但还是会被遍历

  • 记录有序和无序数据的边界,有序的部分在下一轮就不用遍历了。

(3)只有一个元素不对,但需要走完全部轮排序

  • 鸡尾酒排序:元素的比较和交换是双向的,就像摇晃鸡尾酒一样。

3 归并排序

1)算法描述

归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法的一个非常典型的应用。递归的把当前序列分割成两半(分割),在保持元素顺序的同时将上一步得到的子序列集成到一起(归并),最终形成一个有序数列。

2)实现步骤

 

image.png

 

图源:

    • 把长度为n的输入序列分成两个长度为n/2的子序列。

  • 对这两个子序列分别采用归并排序。
  • 将两个排序好的子序列合并成一个最终的排序序列。

3)优缺点

优点:

  • 性能好且稳定,时间复杂度为O(nlogn) 。
  • 稳定排序,适用场景更多。

缺点:

  • 非原地排序,空间复杂度高。

4)适用范围

大数据量且期望要求排序稳定的场景。

4 快速排序

1)算法描述

快速排序使用分治法策略来把一个序列分为较小和较大的2个子序列,然后递归地排序两个子序列,以达到整个数列最终有序。

2)实现步骤

 

image.png

 

    • 从数列中挑出一个元素,称为 “基准值”(pivot)。

  • 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
  • 递归地对【小于基准值元素的子数列】和【大于基准值元素的子数列】进行排序。

3)优缺点

优点:

  • 性能较好,时间复杂度最好为O(nlogn),大多数场景性能都接近最优。
  • 原地排序,时间复杂度优于归并排序。

缺点:

  • 部分场景,排序性能最差为O(n^2)。
  • 不稳定排序。

4)适用范围

大数据量且不要求排序稳定的场景。

5)场景优化

(1)每次的基准元素都选中最大或最小元素

  • 随机选择基准元素,而不是选择第一个元素。
  • 三数取中法,随机选择三个数,取中间数为基准元素。

(2)数列含有大量重复数据

  • 大于、小于、等于基准值。

(3)快排的性能优化

  • 双轴快排:2个基准数,例子:Arrays.sort() 。

5 堆排序

1)算法描述

堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。

2)实现步骤

 

image.png

 

    • 将初始待排序关键字序列(R1,R2….Rn)构建成最大堆,此堆为初始的无序区。

  • 将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区(Rn),且满足R[1,2…n-1]<=R[n]。
  • 由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,……Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。

3)优缺点

优点:

  • 性能较好,时间复杂度为O(nlogn)。
  • 时间复杂度比较稳定。
  • 辅助空间复杂度为O(1)。

缺点:

  • 数据变动的情况下,堆的维护成本较高。

4)适用范围

数据量大且数据呈流式输入的场景。

5)为什么实际情况快排比堆排快?

堆排序的过程可知,建立最大堆后,会将堆顶的元素和最后一个元素对调,然后让那最后一个元素从顶上往下沉到恰当的位置,因为底部的元素一定是比较小的,下沉的过程中会进行大量的近乎无效的比较。所以堆排虽然和快排一样复杂度都是O(NlogN),但堆排复杂度的常系数更大。

6 计数排序

1)算法描述

计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。

2)实现步骤

 

image.png

 

    • 找出待排序的数组中最大元素。

  • 构建一个数组C,长度为最大元素值+1。
    • 遍历无序的随机数列,每一个整数按照其值对号入座,对应数组下标的值加1。

  • 遍历数组C,输出数组元素的下标值,元素的值是几就输出几次。

3)优缺点

优点:

  • 性能完爆比较排序,时间复杂度为O(n+k),k为数列最大值。
  • 稳定排序。

缺点:

  • 适用范围比较狭窄。

4)适用范围

数列元素是整数,当k不是很大且序列比较集中时适用。

5)场景优化

(1)数字不是从0开始,会存在空间浪费的问题

  • 数列的最小值作为偏移量,以数列最大值-最小值+1作为统计数组的长度。

7 桶排序

1)算法描述

桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。实现原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序)。

2)实现步骤

 

image.png

 

    • 创建桶,区间跨度=(最大值-最小值)/(桶的数量-1)。

  • 遍历数列,对号入座。
    • 每个桶内进行排序,可选择快排等。

  • 遍历所有的桶,输出所有元素。

3)优缺点

优点:

  • 最优时间复杂度为O(n),完爆比较排序算法。

缺点:

  • 适用范围比较狭窄。
  • 时间复杂度不稳定。

4)适用范围

数据服从均匀分布的场景。

8 性能对比

随机生成区间0 ~ K之间的序列,共计N个数字,利用各种算法进行排序,记录排序所需时间。

 

image.png

 

参考内容及图源

推荐阅读