《漫画算法》读书笔记 2023.03.30~2023.04.10

《漫画算法》总结 

唠嗑区: 这本书应该是大四时候(2022年10月份)就买了,当时也是为了找实习,同时之前也看过一些程序员小灰的文章,觉得不错所以买了一本。然而事与愿违,买了之后只看了一章,就开始在书架吃灰了。现在又到了准备找实习和找工作的阶段了,遂从书架上抽了出来并花了十天时间(中间还摸鱼了好几天(・ω・`ll))读完了一遍。这本书还是比较基础的,帮我把之前学过的算法和数据结构等内容回想了起来,美中不足的是书中有一小部分讲的不是很细致,需要自己认真思索才能想明白,还有大概两三处笔误吧,但是瑕不掩瑜,内容容易理解,基础不是很牢固的同学值得一读。

        写此总结的目的主要有两个:1)借助写这篇博客的机会,重新复习一遍书本的内容,把不熟悉的地方再看一遍。2)作为这十天的学习产出。以后大概每周做一次总结,重新回顾一周所学的内容,同时记录下自己的成长。

 第1章 算法概述

        本章主要讲了时间复杂度和空间复杂度,即算法的评级指标。内容比较基础,并且已经读过两边,所以并没有做什么总结。

第2章 数据结构基础 

数组:

  • 随机访问,读操作方便
  •  写操作效率低,数组的插入和删除,需要大量数组元素的移动
  •  数组扩容:创建新数组,并将原数组复制,所以数组地址会改变

链表:

  • 在内存中随机存储(对比于顺序存储)
  • 节点的删除和插入比较方便,读取不方便

栈: 

  •  栈底和栈顶(先出)
  • 先入后出FILO
  • 应用:面包屑导航(浏览页面时可以回溯到上一级)、递归

队列: 

  • 先入先出FIFO、队头和队尾(后出)
  • 循环队列,数组实现时,队尾指向的位置为空(不能存储元素,否则无法判断队列满还是空)
  • 双端队列:队头和队尾都可以入队和出队(结合了栈和队列的优点)
  • 优先队列:不是线性数据结构,基于二叉堆来实现的,优先极高的先出队

 哈希表/散列表:

  • key- value 
  • HashMap为例,每个对象都有hashcode(整型变量),经过哈希函数(取模、位运算等)得到数组下标
  • 哈希冲突:开放寻址法(在数组中往后找空位置,也有其他寻址方法)、链表法(Java8的HashMap是红黑树)
  • 扩容:先扩容2倍,再重新hash
  • 散列表可以说是数组和链表的结合
  • HashMap和HashSet的区别(键值对or只有键)、以及java8和java7中对HashMap实现的区别(红黑树or链表)

 第3章 树

树:

  • 二叉树、二叉搜索/排序/查找树、红黑树、AVL树(平衡二叉树)、满二叉树、完全二叉树
  • 二叉树的存储:数组(不适合稀疏二叉树)、链表(常见,指向左右孩子节点)
  • 二叉树的遍历(针对root节点顺序命名的):深度优先:前序、中序、后序(递归,栈);广度优先:层序(队列,递归)
  • 二叉堆:用数组存储,是实现堆排序和优先队列的基础
  • 二叉堆:插入节点(单节点上浮O(logn))、删除节点(单节点下沉O(logn))、构建二叉堆(非叶子结点依次下沉)O(n):熟悉三个过程
  • 优先队列:用二叉堆(数组)实现,入队相当于在二叉堆尾部插入后上浮;出队相当于取出二叉堆堆顶,然后尾部赋值到堆顶,再进行下沉。(因为始终存储在数组的前size个,所以不用考虑循环队列)

第4章 排序算法 

 冒泡排序:

  • 时间复杂度O(n2),稳定排序,想等的元素不进行交换
  • 优化:1)j<array.length-i-1
               2)isSorted=true 只要这轮有交换,就变为false,没有交换就break
               3)sortBorder 最后一次交换的位置的右边肯定是有序的,且是数组中较大的一些元素,所以下一轮只需要比较sortBorder之前的元素

 鸡尾酒排序:

  • 在冒泡排序优化2)的基础上,第一轮从左往右,第二轮从右往左……
  • 适用于大部分元素已经有序的情况(例:[2,3,4,5,6,7,8,1],比较冒泡排序和鸡尾酒排序的过程)

 快速排序(重点):

  • 采用分治法,从冒泡排序演变而来,不稳定,交换时会改变相同元素的前后顺序(双边循环法中,left和right指针遇到等于基准元素时,left指针继续右移,right指针继续左移,所以相同的元素相对位置会发生改变)
  • 平均O(nlogn),最坏O(n2)(逆序时,基准元素的一侧为空),空间复杂度O(logn)(无论递归or非递归)
  • 基准元素的选择一般是第一个,也可以随机选(随机选一个与第一个元素交换,然后再以第一个当基准元素)
  • 元素的交换可以分为:(熟悉两种方法的过程)(递归分治,也可非递归,用栈)
        1)双边循环法:第一个为pivot,然后left指针从左往右,right指针从右往左,两个指针所指元素交换位置,最后left和right重合位置的元素,再和pivot交换
        2)单边循环法:第一个为pivot,从第二个依次向右遍历,比pivot大的继续遍历,小的话mark++(一开始mark在第一个元素,即下标为0),然后遍历的位置与mark的位置互换元素值,遍历完后,pivot和mark位置互换(mark左边的位置个数是小于基准元素的个数)

 堆排序:

  • 先构建二叉堆O(n),再循环删除堆顶元素(将元素移到集合尾部,所以小顶堆最后得到的顺序是从大到小)最坏和平均时间复杂度都是O(nlogn)
  • 空间复杂度O(1),不稳定排序

 计数排序:

  • 线性时间复杂度,不基于元素比较,适用于一定范围内的整数(范围不能过大,且只能是整数)
  • 用新数组(size为数组最大最小值的差+1)记录每个值出现的次数。(数组下标为该元素大小-数组内最小值)
  • 为保证元素相等时保持固有顺序:得到统计数字后,对数组做变形,后面的元素等于前面的元素之和;再倒序遍历原数组,找到该元素的排名并放到输出结果数组中。
  • 时间复杂度O(n+m),n是原始数列的规模,m是最大和最小整数的差值。
  • 稳定的,计数排序专门为了稳定进行了优化。

桶排序: 

  • 线性时间复杂度O(n),当频率分布很不均匀时退化为O(nlogn),即一个桶内的排序(归并排序),空间复杂度O(n)(n个元素要有n个桶)
  • n个桶,前n-1个均匀分配,最后一个桶仅存放最大元素
  • 稳定的,放进桶里之后用链表存储,相同元素的相对位置不发生改变

 第5章 面试中的算法

如何判断链表有环:

  • 遍历法(遍历第k个节点时,依次比较它和之前遍历过的(k-1)个节点是否相同。时间复杂度为O(n2),空间复杂度为O(1)
  • 哈希表HashSet(每遍历一个节点,判断哈希表中是否存在相同节点,存在的话则有环,不存在的话则把该节点加到哈希表中)
  • 双指针(两个指针每次分别走1步、走两步,如果两指针相遇,则有环,若p2是空或者p2下一个节点是空则无环)
  • 环的长度:从第一次相遇节点开始,继续往前走,到第二次相遇节点,p1走的步数即为环的长度。
  • 入环点(推导公式):第一次相遇节点时,把其中一个节点放到头节点,两个节点每次都走一步,再一次相遇节点即为入环点。

最小栈的实现:

  • 一个栈存储(入栈和出栈),另一个栈存储最小值。入栈时,如果入栈元素小于最小栈的栈顶元素,则也入栈最小栈;出栈时,若出栈元素等于最小栈栈顶元素,则也出栈。 

如何求出最大公约数:

  • 暴力求解,从small/2遍历到1
  • 辗转相除法:a和b的最大公约数等于a%b的取余与b的最大公约数
  • 更相减损术:a和b的最大公约数等于a-b的值与b的最大公约数(b小于a)
  • 更相减损术与移位相结合:ab均为偶数时,gcd(a,b)=2*gcd(a>>1,b>>1);a偶数b奇数时,gcd(a,b)=gcd(a>>1,b);ab均为奇数时(b小),gcd(a,b)=gcd(a-b,b),当ab相等时,最大公约数为a或b

一个数是否是2的整数次幂:

  • 暴力破解:从1开始循环乘2,若等于这个数则是整数次幂,若大于这个数则不是。(位运算左移比乘法更能提升性能)O(logn)
  •  num&(num-1) == 0(与运算,true则是整数次幂) 时间复杂度O(1)

 无序数组排序后的最大相邻差:

  • 先排序O(nlogn)再遍历求最大差
  • 基于计数排序思想:先遍历原数组,形成统计数组,再遍历统计数组,最大相邻差为最大连续出现0值个数+1(不适用1、10000这样的)
  • 基于桶排序思想:n个桶,先遍历原数组,将各个元素放入桶中,再遍历每个桶,求最大的bucket[i].min - leftMax(不用考虑单桶内的最大差,因为不可能在单桶内,原因在书的p181)

如何用栈实现队列: 

  • 两个栈,一个A入栈用,一个B出栈用,入栈时压到A中,出栈时从B中取,如果B中是空的,则把A中的依次取出压入B中,再取B的栈顶
  • 入栈:O(1) 出栈:O(1)或者O(n)(看涉不涉及AB的元素迁移),均摊时间复杂度为O(1)

寻找全排列的下一个数: 

  • 1)从后向前查看,找到逆序区域
  • 2)逆序区域前一位和逆序区域中大于它的最小的数字交换位置(从后向前遍历,第一个大于它的数就是。因为是逆序的)
  • 3)将逆序区域变成顺序(交换位置之后,逆序区域仍为逆序!)(具体详见P195)
  • 以上三步操作均为O(n),所以整个算法时间复杂度也是O(n)
  • 此算法又名字典序算法

删去k个数字后的最小值: 

  • 没有思路时可以大量举例子找规律
  • 从左向右遍历,如果左边的一位大于右边的一位,则删掉左边这位,剩下的即为k=1时的最小值。
  • 循环k次上述过程,依次求得局部最优解,最终得到全局最优解,叫做贪心算法
  • k外循环,遍历数组内循环时,时间复杂度为O(kn),且subString函数本身性能不高(详见subString函数的底层实现:https://blog.csdn.net/qq_41720578/article/details/124170035
    • jdk6中,String类中有vaue[],offset,count变量,调用subString()函数时,value[]不变,仅改变offset和count,所以还是会引用原来较长的字符串,如果之前的字符串很长,又要一直引用他,得不到释放,所以可能会产生内存泄露的问题。
    • jdk7中,String类中仅有value[]变量,同时调用:
      this.value = Arrays.copyOfRange(value, offset, offset + count);
      所以会创建新的变量,避免对老字符串value[]的引用。从而解决了内存泄露问题。)
  • 遍历数字为外循环,借助栈(用数组实现栈)来实现:从左向右向遍历数字,如果栈顶元素大于即将入栈的元素,则栈顶元素出栈且k-1,如果小于,则入栈。时间复杂度为O(n)

如何实现大整数相加: 

  • 数字大到整数越界时,不能直接用int或long类型相加
  • 用两个数组存储两个大整数,每一个元素都是一位数字(倒序存放在数组中),然后从左向右,对应数组下标的数字依次相加……最后再用StringBuilder将数组逆序变为大整数
  • array[i] = str.charAt(str.length()-1-i) - ‘0’;
  • 创建数组、按位计算、结果逆序的时间复杂度均为O(n),所以综合也为O(n)
  • 其实只要把大整数拆分成可以被直接计算的程度就够了(int类型的取值范围为-2147483648~2147483647,为了防止溢出,就可以把大整数的每9位作为数组的一个元素,进行加法运算)
  • BigInteger和Bigdecimal的底层实现类似上述思路。(详见csdn文章:http://t.csdn.cn/yiMRo

*** 如何求解金矿问题: 

  • 优先选择性价比高的(×)(贪心算法,局部最优解未必是整体最优)
  • 类似背包问题,动态规划,求状态转移方程(递推公式),自底向上
  • 把复杂的问题简化成规模较小的子问题,再从简单的子问题自底向上一步一步递推
  • 找到全局问题的两个最优子结构(第n个挖或者不挖),找到问题的边界(0个金矿或者0个工人)
  • 递归实现时,会做许多重复的计算(所以从底向上,用dp数组记录下每一步的值,就不用重复计算了)
  • 两个循环,金矿个数n和工人个数w,时间复杂度和空间复杂度都为O(nw)
  • 但因为每一行数据都是来源于上一行的数据,所以只需要O(n)的空间复杂度(从右向左遍历,否则从左向右遍历时,左边用到的数据已被覆盖)
  • 遍历顺序十分重要,为什么金矿个数在外层,工人数在内层?(//TODO,动态规划还需要系统地进行学习,此处并不是很理解,还需要多做题巩固

寻找缺失的整数: 

  • 1-100范围内的99个不重复的数,找出那个缺失的数:
    • 哈希表法:存储1-100这100个key,然后遍历整个数组,每遍历一个就在哈希表中删除一个,剩下的那个即为缺失的数。O(n),O(n)
    • 排序法:用O(nlogn)排序算法进行排序,然后遍历排序后的数组,不连续的位置就是缺失的元素。O(nlogn),O(1)(堆排序)
    • 求和再减法:求1-100的和,再依次减去数组各个元素。O(n),O(1)
  • 1-100范围内的99个整数出现了偶数次,只有一个整数出现了奇数次,求这个整数?总个数未知
    • 异或法:所有元素依次遍历,所得结果就是要求的数。A xor A = 0  ; 0 xor A = A  O(n),O(1)
  • 上面基础上,改为两个数出现奇数次,求这两个数?
    • 分组异或法:全部异或,所得结果一定会有一位是1(否则两个数相同),按照这一位将数组分成两部分,每部分再依次异或,所得的两个结果即为所求。O(n),O(1)

 

 算法的实际应用:

Bitmap的巧用:

  • 用户信息的标签化:可以用关系型数据库来实现和存储,每一个标签占一列,但标签过多时,表会变得很长,且sql语句也很长,并且用distinct去重的性能也很差。
  • Bitmap算法:主要用于大量整数做去重和查询操作。从右向左下标依次增加,对应的用户ID放到对应的下标中(该bit改为1)。每个标签对应一个位图,位图下标即为用户ID,然后对位图进行与或非异或等位操作来实现求交集并集补集等功能。
  • 与哈希表相比,哈希表每一个用户ID都要存成int或long类型,而Bitmap只用一位bit来存,所以位图算法使用了更少的内存,性能更高。
  • 读Java JDK中的BitSet类源码

LRU算法的应用: 

  • 为了提高系统性能,不能每一次请求时都访问数据库,应该利用缓存,并尽量提高缓存命中率
  • 可以在内存中创建一个哈希表,每次访问数据库时,把数据存储在哈希表中,但数据过多时会出现内存溢出。这就可以使用LRU算法(内存管理算法,删掉哈希表中最近最少使用的数据)
  • 数据结构:哈希链表。可以借助LinkedHashMap类(HashMap加双向链表)来实现,(accessOrder=true时,若访问的数据已经在哈希表中,则把该节点加到链表末尾,并将原位置的节点删除,当超过缓存容量时,将链表头部的节点删掉,再在末尾加新的节点)
  • 读Java的LinkedHashMap类源码(csdn文章:http://t.csdn.cn/ForXu

什么是A星寻路算法 :

  • A* search algorithm:用于寻找有效路径的算法 openList,closeList,F=G+H,g为从起点走到当前格子的成本;h为不考虑障碍时,从当前格子走到目标格子的距离;f为g和h的综合评估
  • 步骤:把起点放到openList中,然后从openList中移出f值最小的点,放入到closeList中,然后将该点的邻居点们(且不在openList和closeList中)放入到openList中,该点为邻居点们的父节点,并分别计算g(parent.g+1),h(该点到终点的距离),f(g+h),然后循环上述步骤……直到openList中出现终点,则直接移出终点到closeList中,并结束算法。(根据父节点可以找到最短路径)
  • 像这样以估值高低来决定搜索优先次序的方法,被称为启发式搜索。

如何实现红包算法: 

  • 为了避免高并发引起的问题,不能在领的时候才计算领多少,必须先计算好每个红包拆出的金额,并把它们放在一个队列里。
  • 二倍均值法:保证了每次随机金额的平均值是相等的,不会受先后顺序影响。
  • 线段切割法:n个人抢红包时,确定(n-1)个分割点

         以上便是整本书的内容总结,仅为了之后方便快速回顾本书内容,因此只记录了比较重要的点,且在语言表述上可能不太恰当,还望大家海涵。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值