标签: 算法

  • 树状数组学习笔记

    树状数组

    推荐文章:https://leetcode.cn/problems/range-sum-query-mutable/solutions/2524481/dai-ni-fa-ming-shu-zhuang-shu-zu-fu-shu-lyfll/
    推荐视频:https://www.bilibili.com/video/BV1ce411u7qP/?vd_source=9916020e44ecb0118b4b1ad9cd262997

    总体功能上来说就是一个快速对区间进行求和和修改的数据结构,推荐文章里讲的真的很好,想要查的快多做缓存能到O(1)的效率,但是修改就会是O(n)的效率,那树状数组就是综合查找和修改的一个数据结构,能做到O(logn)的效率。
    具体的算法细节就不再多赘述
    是一个真的很好用的工具!

    我专项的做了一些训练,出题的人还是挺厉害的,树状数组本身是求下标之间数量和的结构,但题目里的使用场景被扩充到了求某一些值之间的数量。

    具体变换

    求某些值之间的数量

    题目:将元素分配到两个数组中 II

    这个题目是一个模拟题,但是题目核心的复杂度在greaterCount函数的实现,如果直接遍历O(n)的复杂度,那么这个题目最大的复杂度就去到了O(n^2),肯定是不够快的。

    那有什么结构能够快速实现数组内大于等于某个数量的查询和插入呢?

    答案:树状数组。

    离散化
    那要怎么做呢?将数组内的值作为横坐标,每个数据的纵坐标都是1,也就是创造了一个记录值数量的结构,那么对[1, 5]求和就能够表示:在原数组内值为1~5之间的总数,这就能够满足原问题了,但这会引入一个新的问题:

    3 <= n <= 10^5
    1 <= nums[i] <= 10^9

    那就是树状数组的坐标会变得非常大,这时就有另外一个办法,我们将原数组进行一次离散化的压缩,例如:
    100, 701, 504, 10201
    可以被压缩成:1,3,2,4,其中需要记录1~100, 2~504, 3~701, 4~10201
    它也可以被压缩成:1,2,3,4,其中1~100, 2~701, 3~504, 4~10201
    这二者有什么差别吗?前者保留了映射数组的大小关系,但代价是一次O(nlogn)的排序,我们需要的当然是前者,但后者也是离散化。

    >, >=, <和<=

    题目:通过指令创建有序数组

    树状数组很像是一个公式,它的公式本身只能求解<=值的数量,但是如何扩展到另外四个维度呢?

    小于等于: 原本公式化的Get(v)即可;
    小于: 原本公式化的Get(v-1)即可;
    大于: 当前总数 – Get(v)
    大于等于: 大于 + num[v]

    置换

    题目: 统计数组中好三元组数目

    原本的题目是比较复杂的,明显是需要遍历A数组,找到三个数(a,b,c),然后在B数组中寻找a,b,c是否存在,且仍然保序。

    置换是一个排列到另一个排列的双射。所有全排列的数组之间求前后顺序相关的题目都可以考虑这个方案。
    置换即直接将AB数组做一个关于A数组的坐标映射:

    A: [2,0,1,3], B: [0,1,2,3]
    映射成:
    A: [1,2,3,4], B: [2,3,1,4]
    
    A: [4,0,1,3,2], B: [4,1,0,2,3]
    映射成:
    A: [1,2,3,4,5], B: [1,3,2,5,4]

    因为做了置换,问题就从要在A, B中寻找一个三元数,变成了在B数组中找三个升序排列的子数组总共有多少个。
    如何遍历呢?
    三元数是(a, b, c),只需要在B数组中遍历b就可以,然后在左边寻找有小于b的数量x,右边是否有大于b的数量y,sum(x * y)就是结果。
    接下来的问题就是如何快速求解左边小于b, 右边大于b的数量呢?
    依然是遍历,从左到右维护一个树状数组的结构,在遍历过程中就可以快速求解<x的数量,那如何求右边是否有>x的数量呢?因为是全排列,总共会有(n-x)个大于x的数量,那左边有l个的话,右边就有n-x-l个,综上。

    额外的知识

    离散化

    离散化的方案其实很简单,就是排序之后做一个映射,但是可以直接对有序数组进行遍历,无需关心是否是完全连续,只需要保证离散化之后的坐标保留值本身的大小关系,代码如下:

    sort(sortedNums.begin(), sortedNums.end());
    unordered_map<int, int> indexMap;
    for(int i = 0; i < sortedNums.size(); i++)
    {
        indexMap[sortedNums[i]] = i + 1;
    }

    lower_bound

    看到其他人的题解里用到了std::lower_bound,深入看了一下,参考文章。

    除了lower_bound,标准库还包含upper_bound, equal_range和binary_search这4个查找函数,底层都是二分查找。

    在那些同学的实现里,就是用这个替代前一小节的indexMap的映射关系。我的实现是用O(n)的时间建立了一个unordered_map,换取每次O(1)的读取。而他们的实现是不用O(n)的时间以及空间建立额外的哈斯表(unordered_map)或者红黑树(map),直接用排序的数组进行二分查找。

    这里C++标准库提供了四种查找:
    lower_bound: 左到右查找第一个大于等于目标值的元素的迭代器
    upper_bound: 左到右查找第一个大于目标值的元素的迭代器
    equal_range: 从左到右找出等于目标值的迭代器范围, [first, last)
    binary_search: 只返回是否存在,不返回迭代器

    同时我还挺惊讶的,直接用lower_bound似乎比用unordered_map要更快?是因为反复扩充hash表吗?

    vector::unique, vector::remove, vector::erase

    这里是需要注意的!
    vector::remove和vector::unique都对vector数组进行了改动,将待删除数组挪到了数组末尾,但并没有执行删除操作。可能是考虑到类似于栈的top和pop的读和删除的操作分离。

    所以想要实际删除需要:

    std::erase(std::unique(v.begin(), v.end()), v.end());
    std::erase(std::remove(v.begin(), v.end(), val), v.end());

    拆开的目的应该就是在删除之前还能保留一定读的能力。

    本节参考:
    https://blog.csdn.net/Vcrossover/article/details/106243627
    https://cloud.tencent.com/developer/article/1022341
    https://en.cppreference.com/w/cpp/algorithm/unique

    End.

  • 垃圾回收算法总结

    垃圾回收算法笔记

    一、标记清除:

    分为两个阶段:标记阶段和清除阶段;从根节点标记所有未被引用的垃圾对象,然后清除阶段删除。

    优点:

    • 存活对象较多的情况下高效
    • 适用于年老代
    • 实现简单, 容易和其他算法组合

    缺点:

    • 容易产生内存碎片
    • 分配速度不理想,每次分配都需要遍历空闲列表找到足够大的分块
    • 与写时复制技术不兼容,因为每次都会在活动对象上打上标记
    • 大量的内存碎片会导致大对象分配时可能失败,从而提前触发了另一次垃圾回收动作
    • 具有引用关系的对象可能会被分配在堆中较远的位置,这会增加程序访问所需的时间,即「访问的局部性(Locality)」较差。

    二、复制算法:

    从根节点扫描,标记所有存活对象,并且复制到新的内存,回收旧内存。现在的商业虚拟机都采用这种收集算法来回收新生代。

    优点:

    • 存活对象比较少的情况下高效
    • 良好的局部性
    • 不会发生碎片化
    • 适用于年轻代(即新生代):基本98%的对象都是“朝生夕死”

    缺点:

    • 最多只能使用一半的堆空间,因此堆的使用率不高;
    • 需要一块空的内存空间;需要复制移动对象;

    三、标记压缩

    标记压缩算法是一种老年代的回收算法,它在标记清除算法的基础上做了一些优化。从根节点对所有可达对象做一次标记,然后将存活的对象压缩到内存的一段,之后清理边界空间。

    优点:

    • 这种算法避免了碎片产生
    • 也不需要两块相同的内存空间

    缺点:

    • 压缩的过程需要多次搜索

    四、分代收集算法

    分代收集算法是目前JVM虚拟机使用的回收算法。它将内存分为各个年代。一般情况下将堆区划分为老年代和新生代,在堆区之外还有一个代就是永久代。

    在不同的代使用不同的算法,从而使用最适合的算法。

    • 新生代 = 生成空间 + 2 * 幸存区 复制算法
    • 老年代 标记-清除算法

    五、引用计数

    引用计数,就是记录每个对象被引用的次数,每次新建对象、赋值引用和删除引用的同时更新计数器,如果计数器值为0则直接回收内存。

    优点:

    • 可即可回收垃圾
    • 最大暂停时间短

    缺点:

    • 计数器的增删处理重,除了数量多,而且在删除容器的时候,需要遍历所有的对象修改引用计数。
    • 计数器需要占用很多空间
    • 循环引用无法回收

    六、增量式GC

    将GC的阻塞任务拆分成少量多次,减少暂停用户程序的执行。

    三色标计算法:

    • 白色:还未搜索过的对象
    • 灰色:正在搜索的对象
    • 黑色:搜索完成的对象

    三色过程:

    • 根查找阶段:对能直接引用的对象打上标记,堆放到标记栈里
    • 标记阶段:从标记栈中取出对象,将其子对象涂成灰色;这个阶段不是一下子处理所有的灰色对象;
    • 清除阶段:将没被标记的白色连接到空闲链表,并重置已标记的对象标记。

    读屏障和写屏障

    原因:

    • 多标:在对象被标记为灰色之后,父节点解除了引用关系,那本来应该被删除的对象就会被延迟删除;
    • 漏标:在对象被标记之后,对象断开子节点A同时A被父节点引用,但父节点已经被标记为黑色,不会再重新访问,因此A节点会被标记成白色删除,这是不可接受的。

    漏标的充分必要条件是:

    • 应用线程插入了一个从黑色对象(A)到白色对象(E)的新引用。
    • 应用线程删除了从灰色对象(C)到白色对象(E)的直接或者间接引用。

    避免漏标打破上述任何一个条件即可:

    • 这里在第一个条件触发时增加写屏障,修改黑色或者白色为灰色;并且在渐进式GC里对白色对象触发度屏障。
    • 第二个条件触发写屏障,当删除关系时触发写屏障,将这些关系都记录下来,最后重新扫一次。

    优点:缩短最大暂停时间

    缺点:降低了吞吐量

    JVM垃圾回收有两种类型:Minor GC和Full GC。

    Minor GC

    只回收新生代,这里非常频繁且对象大多数死亡频繁,所以选择速度快效率高的算法。

    Full GC

    对整个堆进行回收,包括新生代和老年代,所以比Minor GC要慢,应该尽可能减少Full GC的次数。

    python垃圾回收

    python分配<512btype的内存时使用内存池,释放时不会还给系统;分配>512字节的对象时会直接使用C的allocate分配和管理。

    标准CPython实现的垃圾收集器有两部分组件构成,一部分为引用计数模块(reference counting collector), 另一部分为分代垃圾回收器(generational garbage collector)。

    引用计数是本意,但是无法处理循环引用,所以补充了分代垃圾回收器。

    有三种情况会触发垃圾回收:

    • 调用gc.collect(),需要先导入gc模块。
    • 当gc模块的计数器达到阀值的时候。
    • 程序退出的时候。

    当循环引用的两个对象都有del时,python不知道先调用哪个导致不会释放,把它们标记为:uncollectable。

      这个方法涉及到之前说过的分代回收的策略。python中默认把所有对象分成三代。第0代包含了最新的对象,第2代则是最早的一些对象。在一次垃圾回收中,所有未被回收的对象会被移到高一代的地方。

    gc.get_threshold()
    这个方法返回的是(700,10,10),这也是gc的默认值。这个值的意思是说,在第0代对象数量达到700个之前,不把未被回收的对象放入第一代;而在第一代对象数量达到10个之前也不把未被回收的对象移到第二代。可以是使用gc.set_threshold(threashold0,threshold1,threshold2)来手动设置这组阈值。

    Unreal GC

    虚幻的GC挺复杂的,因为虚幻本身提供了一套非常复杂且自由的系统。

    只看UObject类的话就会简单一些,它是Unreal的基础类,它使用的是标记-清除算法,而不是传统类似于shared_ptr这样的引用计数。

    学习文档:

    https://aijishu.com/a/1060000000080557

    https://www.jianshu.com/p/a8a04fd00c3c

    https://developer.aliyun.com/article/777750

    https://zhuanlan.zhihu.com/p/83251959

    https://dreamgoing.github.io/Python%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6.html

    https://testerhome.com/topics/16556