《漫画算法》总结
唠嗑区: 这本书应该是大四时候(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[]变量,同时调用:
所以会创建新的变量,避免对老字符串value[]的引用。从而解决了内存泄露问题。)this.value = Arrays.copyOfRange(value, offset, offset + count);
- 遍历数字为外循环,借助栈(用数组实现栈)来实现:从左向右向遍历数字,如果栈顶元素大于即将入栈的元素,则栈顶元素出栈且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)个分割点
以上便是整本书的内容总结,仅为了之后方便快速回顾本书内容,因此只记录了比较重要的点,且在语言表述上可能不太恰当,还望大家海涵。