常见数据结构

数组:是一种线性表结构。它用一组连续的内存空间,来存储一组具有相同类型的数据。

链表:它并不需要一块连续的内存空间,他通过"指针"将一组零散的内存块串联起来使用,所以如果我们申请的是100MB大小的链表,根本不会有问题。三种常见链表是:单链表,双向链表,循环链表

链表算法练习:

1. 单链表反转

2. 链表总环的检测

3. 两个有序的链表的合并

4. 删除链表倒数第n个节点

5. 求链表的中间节点

栈:后进者先出,先进者后出。 如:浏览器的前进,后退;函数调用栈。 栈是一种“操作受限“”的线性表,只允许在一端插入和删除数据

我们知道,CPU资源是有限的,任务的处理速度与线程个数并不是线性正相关。相反,过多的线程反而会导致cpu频繁切换,处理性能下降。所以线程池的大小一般都是综合考虑要处理任务的特点和硬件环境,来事先设置的。

当我们向固定大小哦的线程池中请求一个线程时,如果线程池中没有空闲资源了,这个时候线程池如何处理这个请求?队列

队列:先进者先出。队列跟栈非常相似,支持的操作也很有限,最基本的操作也是两个:入队(放入队列尾部),出队(从队列头部取一个元素),所以队列也是一种操作受限的线性表数据结构

队列区分:循环队列(解决数组实现的队列操作时大量的数据迁移问题),阻塞队列(在基础队列的基础上做了阻塞操作,队列为空的时候从头部取数据会被阻塞,队列满了的时候插入操作会被阻塞 -- 生产者消费者模式),并发队列(解决多线程操作时的线程安全问题,通过对enqueue(), dequeue()方法加锁,同一时刻仅允许一个存或者取操作,可以实现非常高效的并发队列)

基于链表可以实现无限排队的无界队列,基于数组可以实现有界队列。对于大部分资源有限的场景,当没有空闲资源时,基本上都可以通过“队列”这种数据结构实现请求排队。

递归代码要警惕堆栈溢出:递归超过一定的深度直接返回报错。对于脏数据,可能会出现无限递归的问题,对于这个问题,可以通过自动检测A-B-C-A这种环的存在。

递归代码简洁,但是空间复杂度高,有堆栈溢出风险,存在重复计算,耗时过多等问题。实际开发中,要根据实际情况选择是否可以修改为非递归代码(通过for循环的方式)。

排序:冒泡排序,选择排序,插入排序,快速排序,归并排序,桶排序,计数排序,基数排序

排序的考虑因素:原地排序(是否需要新开辟内存,原地排序算法特指复杂度为O(1)的排序算法),稳定性(值相同的会不会被交换位置)

插入排序是稳定排序,是原地排序,实现简单,虽然时间复杂度是O(n^2), 对于数据量小的情况还是可以选择(实际数据量小的时候选择,冒泡,插入都可以选择)

数据量大的时候选择归并排序和快速排序(都是分治思想),(也可选择基于二叉树数据结构的堆排序

归并排序因为不是原地排序,所以没有快速排序那么应用广泛

桶排序和计数排序,基数排序对排序的数据都有比较苛刻的要求,应用不是很广泛,但是数据特征符合,应用会特别高效,时间复杂度可以达到O(n),  桶排序和计数排序的排序思想非常相似,都是针对范围不大的数据,讲数据划分成不同的桶来实现排序。

桶排序:将需要排序的数据分到几个有序的桶里,如按照年龄对100万用户排名,则可设计0-9, 10-19... 99-109十个桶。每个桶里的数据单独排序(可使用快排),桶内的数据排完序后,再把每个桶里的数据按照顺序依次取出,组成的序列就是有序的了。桶排序比较适合外部排序:数据存储在外部磁盘中,数据量大,内存有限无法将数据全部加载进内存 -- 先扫描一遍文件,根据订单金额所处的范围写到对应的文件(桶)里

计数排序:桶排序的一种特殊情况,当要排序的n个数据所处的范围并不大的时候,比如最大值为k,我们就可以把数据分成k个桶。每个桶内的数据都是相同的,省掉了桶内排序的时间,如根据高考成绩排名,满分900,最小分数0,则可以将100W考生划分到901个桶里,然后依次扫描每个桶,依次输出到一个数组里,就实现了100万考生的排序。(只能给非负整数排序,要是有负数,可统一加整数,如1000,全部变成非负整数)。

基数排序:针对,数据范围太大的情况,如手机号排序,可以使用基数排序,从第一位先排序,再使用第二位,依次到最后一位进行排序(长度不够就补0)。基数排序需要桶排序或者计数排序来完成每一位的排序

相比排序,如何用最省内存的方式实现快速查找在实际工程中则更为重要

二分查找算法(前提:顺序表结构--数组&&有序数据集合):确定中间元素,对半分,循环继续查找,直到找到元素返回。特点:1. 顺序表结构  2. 有序数据 3. 数据量太小不适合二分查找(数据量小直接遍历就行)  4. 数据量太大也不适合二分查找 (二分查找依赖的数组数据结构要求内存的连续性)

二分查找变形问题:查找一个值等于给定值得元素,查找最后一个值等于给定值得元素,查找第一个大于给定值得元素

跳表(redis有序集合有使用):可以快速实现插入,删除,查找操作。写起来也不复杂,甚至可以代替红黑树

跳表的实现:链表存储有序数据,增加索引层(即可使用分治思想)。查找某个节点时,先遍历索引缩小查询范围(空间换时间的设计思路)

跳表索引的动态更新问题:为了避免复杂度退化,如果链表中的节点多了,索引节点就相应的增加一些。我们可以通过随机函数选择将这个数据插入到部分索引层中

散列表(hash表或者哈希表):散列表用的是数组支持按照下标随机访问数据的特性,所以所以散列表其实就是数组的一种扩展,由数组演化而来。可以说如果没有数组就没有散列表

解决问题:将参赛选手的编号,或者姓名通过散列函数转化作为数组下标,来存取选手信息数据。当通过参赛编号或者名称来查询的时候,我们使用相同的散列函数,读取数组中的数据。

散列冲突问题:因为数组的存储空间有限,也会加大散列冲突的概率。1. 开放寻址法(如: java中的ThreadLocalMap):如果出现了散列冲突,我们就重新探测一个空闲位置,将其插入。重新探测的方法--线性探测。查找元素时也是先根据key找到角标,然后判断元素属性判断是否是需要查找的元素,要是不是则顺着角标继续往后查找,知道查到对应元素。如果没找到,则元素没有在列表中 2. 链表法(更加常用,如:java中LinkedHashMap):散步表中每一个槽(key)会对应一条链表,插入时通过散列函数计算到角标,然后插入到槽内对应的链表中,查找和删除时也是先找到槽位,然后对链表进行查找和删除。比较适合存储大对象,大数据量的散列表,也更加灵活,支持更多的优化策略

散步表的装载因子(元素个数/散列表长度):装载因子越大,说明空闲位置越小,冲突越多,散列表的性能会下降  (链表法也会有装载因子问题,剩余的槽位太少,短时间的连续插入操作需要扩容,连续的扩容很消耗资源)

 工业级的散列表:对于恶意的攻击者,精心构造的数据,使得数据经过散列函数都得到同一个槽位,最后,散列表就会退化成链表(查询时间复杂度增加)。

HashMap: 默认初始大小16, 最大装载因子0.75,到达0.75的时候就会启动扩容,每次扩为原来的两倍。使用链表解决散列冲突,链表节点大于6时会将链表转化为红黑树,小于6时,又会将红黑树转化为链表

LinkedHashMap是通过双向链表和散列表来实现的,这里的Linked值的是双向链表,实现了支持按照插入的顺序来遍历数据,而且支持按照访问顺序来遍历数据(和LRU缓存淘汰算法一个道理)。

哈希算法

特点:1. hash值不能反向推导出原始数据 2. 对输入数据非常敏感,哪怕原始数据只修改了一个Bit, 最后得到的hash值也大不相同 3. 散列冲突的概率要很小,对于不同的原始数据,哈希值相同的概率非常小 4. 哈希算法的执行效率要尽量高效,针对较长的文本,也能快速的计算出哈希值

应用场景:安全加密。唯一标识。数据校验(对文件hash签名从而确定文件是否被篡改)。散列函数(如散列表的应用)。(区块链)。负载均衡(通过客户端ip和目标节点或pod通过hash值对应,从而实现负载均衡以及session affinity特性)。数据分片(1. 大数据处理时,可使用hash对数据进行分片,然后n台机器同时处理 2. 如巨量图片存储,使用hash值然后和机器的个数n取余,从而得到对应的机器上存储)。分布式存储:存储时使用数据切片,需要扩容时,需要新加一台机器时使用一致性hash算法避免大量数据迁移

二叉树:存储方式:基于链表的链式存储,基于数组的顺序存储

满二叉树:叶子节点全都在最底层,除了叶子节点之外,每个节点都有左右两个子节点

二叉树查找树:

描述:在树的任意一个节点,其左子树的节点的值都小于这个节点的值,而右子树节点的值都大于这个节点的值。

查找方法:递归二分查找, 为了避免时间复杂度的退化(链表),又设计了一种更复杂的树,平衡二叉树查找树(ALV树)。

红黑树:是平衡二叉树查找树的性能妥协版本

堆:是一个完全二叉树,树的节点的值必须大于等于(或小于等于)每个节点的值,这个特点使得堆就是天然的一个从上往下排好序的。堆顶就是最大值或者最小值。排序方便,插入和删除就比较麻烦,插入就是一个比较堆化的过程。

函数递归也叫递归树

b+树:树有多个叶子节点,下面一层是数据,上面各层是索引(和跳表有点类似),如mysql的索引使用

图:

你可能感兴趣的:(常见数据结构)