数据结构与算法之美知识点总结

复杂度

数组

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

  • 数组支持随机访问,根据下标随机访问的时间复杂度为 O(1)。
  • (插入删除)最好情况时间复杂度为 O(1);如果删除开头的数据,则最坏情况时间复杂度为 O(n);平均情况时间复杂度也为 O(n)
  • 每次的删除操作并不是真正地搬移数据,只是记录数据已经被删除。当数组没有更多空间存储数据时,我们再触发执行一次真正的删除操作,这样就大大减少了删除操作导致的数据搬移。
  • 对于业务开发,直接使用容器就足够了,省时省力。毕竟损耗一丢丢性能,完全不会影响到系统整体的性能。但如果你是做一些非常底层的开发,比如开发网络框架,性能的优化需要做到极致,这个时候数组就会优于容器,成为首选。

a[i]_address = base_address + i * data_type_size

警惕数组的访问越界问题

容器能否完全替代数组?

  • 容器最大的优势就是可以将很多数组操作的细节封装起来,部分语言支持动态扩容,但是封装过程有一定的性能损耗。
  • 对于业务开发,直接使用容器就足够了,省时省力。毕竟损耗一丢丢性能,完全不会影响到系统整体的性能。
  • 但如果你是做一些非常底层的开发,比如开发网络框架,性能的优化需要做到极致,这个时候数组就会优于容器,成为首选

JVM标记清除算法:

大多数主流虚拟机采用可达性分析算法来判断对象是否存活,在标记阶段,会遍历所有 GC ROOTS,将所有 GC ROOTS 可达的对象标记为存活。只有当标记工作完成后,清理工作才会开始。

不足:1.效率问题。标记和清理效率都不高,但是当知道只有少量垃圾产生时会很高效。2.空间问题。会产生不连续的内存空间碎片。

链表

链表中的每个结点都需要消耗额外的存储空间去存储一份指向下一个结点的指针,所以内存消耗会翻倍。而且,对链表进行频繁的插入、删除操作,还会导致频繁的内存申请和释放,容易造成内存碎片。

为什么函数调用要用“栈”来保存临时变量呢?用其他数据结构不行吗?

其实,我们不一定非要用栈来保存临时变量,只不过如果这个函数调用符合后进先出的特性,用栈这种数据结构来实现,是最顺理成章的选择。

从调用函数进入被调用函数,对于数据来说,变化的是什么呢?是作用域。所以根本上,只要能保证每进入一个新的函数,都是一个新的作用域就可以。而要实现这个,用栈就非常方便。在进入被调用函数的时候,分配一段栈空间给这个函数的变量,在函数结束的时候,将栈顶复位,正好回到调用函数的作用域内。

队列

关于如何实现无锁并发队列

考虑使用CAS实现无锁队列,则在入队前,获取tail位置,入队时比较tail是否发生变化,如果否,则允许入队,反之,本次入队失败。出队则是获取head位置,进行cas。

递归

递归代码要警惕堆栈溢出

函数调用会使用栈来保存临时变量。每调用一个函数,都会将临时变量封装为栈帧压入内存栈,等函数执行完成返回时,才出栈。系统栈或者虚拟机栈空间一般都不大。如果递归求解的数据规模很大,调用层次很深,一直压入栈,就会有堆栈溢出的风险。

  • 我们可以通过在代码中限制递归调用的最大深度的方式来解决这个问题。递归调用超过一定深度(比如 1000)之后,我们就不继续往下再递归了,直接返回报错

为了避免重复计算

我们可以通过一个数据结构(比如散列表)来保存已经求解过的 f(k)

所有递归都可以改成迭代循环

递归有利有弊,利是递归代码的表达力很强,写起来非常简洁;而弊就是空间复杂度高、有堆栈溢出的风险、存在重复计算、过多的函数调用会耗时较多等问题。

排序

在这里插入图片描述

冒泡插入选择,都是基于数组实现的。如果数据存储在链表中,这三种排序算法还能工作吗?如果能,那相应的时间、空间复杂度又是多少呢?

觉得应该有个前提,是否允许修改链表的节点value值,还是只能改变节点的位置。一般而言,考虑只能改变节点位置,冒泡排序相比于数组实现,比较次数一致,但交换时操作更复杂;插入排序,比较次数一致,不需要再有后移操作,找到位置后可以直接插入,但排序完毕后可能需要倒置链表;选择排序比较次数一致,交换操作同样比较麻烦。综上,时间复杂度和空间复杂度并无明显变化,若追求极致性能,冒泡排序的时间复杂度系数会变大,插入排序系数会减小,选择排序无明显变化。

二分查找应用场景的局限性

  • 二分查找依赖的是顺序表结构,简单点说就是数组。
  • 二分查找针对的是有序数据
  • 数据量太小不适合二分查找
  • 数据量太大也不适合二分查找(内存吃力)

skipList 跳表

redis的zset就是使用跳表
跳表的时间复杂度O(logn)
-跳表是一种动态数据结构,支持快速地插入、删除、查找操作,时间复杂度都是 O(logn)。跳表的空间复杂度是 O(n)

跳表是不是很浪费内存?

在软件开发中,我们不必太在意索引占用的额外空间。在讲数据结构和算法时,我们习惯性地把要处理的数据看成整数,但是在实际的软件开发中,原始链表中存储的有可能是很大的对象,而索引结点只需要存储关键值和几个指针,并不需要存储对象,所以当对象比索引结点大很多时,那索引占用的额外空间就可以忽略了。

跳表是通过随机函数来维护前面提到的“平衡性”。

Redis 中的有序集合是通过跳表来实现的

  • 插入、删除、查找以及迭代输出有序序列这几个操作,红黑树也可以完成,时间复杂度跟跳表是一样的。但是,按照区间来查找数据这个操作,红黑树的效率没有跳表高。
  • 跳表更加灵活,它可以通过改变索引构建策略,有效平衡执行效率和内存消耗。跳表更容易代码实现。

散列表

散列冲突

1、开放寻址法

  • 线性探测
  • 链表法

当数据量比较小、装载因子小的时候,适合采用开放寻址法。这也是 Java 中的ThreadLocalMap使用开放寻址法解决散列冲突的原因

在你熟悉的编程语言中,哪些数据类型底层是基于散列表实现的?散列函数是如何设计的?散列冲突是通过哪种方法解决的?是否支持动态扩容呢?

  • Redis中的hash,set,hset,都是散列表实现,他们的动态扩容策略是同时维护两个散列表,然后一点点搬移数据

Redis 有序集合

  • 在有序集合中,每个成员对象有两个重要的属性,key(键值)和 score(分值)
  • 细化一下 Redis 有序集合的操作
    添加一个成员对象;
    按照键值来删除一个成员对象;
    按照键值来查找一个成员对象;
    按照分值区间查找数据,比如查找积分在[100, 356]之间的成员对象;
    按照分值从小到大排序成员变量;

hash 表 +跳表解决

LinkedHashMap 是通过双向链表和散列表这两种数据结构组合实现的。

  • LinkedHashMap 中的“Linked”实际上是指的是双向链表,并非指用链表法解决散列冲突。
  • 按照访问时间排序的 LinkedHashMap 本身就是一个支持 LRU 缓存淘汰策略的缓存系统。

工业中几个散列表和链表结合使用的例子里,我们用的都是双向链表。如果把双向链表改成单链表,还能否正常工作呢?为什么呢?

  • 改成单链表以后还能工作,但是效率上已经明显降低。主要体现在单链表中没有前去指针,当我们要删除某一个节点的时候,需要重新遍历一次链表,时间复杂度为O(n).或者重新申请一个空间保存一个前驱指针。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值