算法 梳理

这篇博客详细介绍了算法的基础知识,包括排序算法的时间复杂度分析,如冒泡、插入、选择和快速排序等。此外,还探讨了二分法、异或运算、链表操作、栈和队列、递归、哈希表、堆排序以及前缀树等。博主通过实例和代码展示了算法的实际应用,并讨论了排序算法的稳定性。博客最后总结了各种排序算法的特点和应用场景。
摘要由CSDN通过智能技术生成


算法
实现功能,设计流程
有些数据结构专属算法,加速算法

评估算法
时间复杂度
额外空间复杂度

常数时间的操作
算术运算
位运算:>>带符号右移,符号位右移,最高位补符号位;>>>不带符号右移,最高位补0
数组寻址

时间复杂度:衡量多少次常数操作

排序

选择
冒泡
插入

选择:
如果数组长度为空或长度小于2,则直接返回
外循环
0~n-1
1~n-1 …
内循环
i~n-1找最小值
最小值和i交换

冒泡:
如果数组长度为空或长度小于2,则直接返回
外循环
0~n-1
1~n-1 …
内循环
0、1
1、2…
e、e-1
i位置和i+1位置交换

插入
如果数组长度为空或长度小于2,则直接返回
外排序
0~0想有序的
0~1想有序的
内排序
0~i做到有序

问题的最优解
首先是时间复杂度尽量低,再使用最少的空间。

时间复杂度从好到坏

O(1)、O(logN)、O(N)、O(NlogN)、O(N2)、O(Nk)、O(2n)、O(Kn)、O(N!)

算法学习的大脉络

知道怎么算的算法
知道怎么试的算法:其实就是写递归的能力,包括贪心、动态规划、暴力递归…

对数器

要测的算法a
容易实现的算法b
一个随机样本产生器
a、b跑同一个样本,然后多跑一些样本。

产生一个随机数组,设定最大的长度和最大值
Math.random() 随机返回一个[0,1)内等概率的一个小数,注意在计算机内是有限个的。
Math.random() * N:[0, N)小数
(int)(Math.random() * N):[0, N-1]整数
创建一个数组,长度是最大长度内的随机一个值
数组赋值,正数和负数均可,每个位置是最大值内的随机值减最大值内的随机值

然后拷贝一个完全相同的数组,copyArray(arr1)

算法一跑样本一,算法二跑样本二

然后比较结果arr1和arr2,长度是否相等,每个值是否相等
是自己重写的isEqual()

最后测试50万次

如果结果不相同,就打印arr1和arr2

二分法:有序或无序数组,找一个数

局部最小值:不是一般的二分,是二分的二分
经典的二分是有序数组上的二分

时间复杂度:logN,砍一半,共砍了几次

1.在一个有序数组中,找某个数是否存在
2.在一个有序数组中,找>=某个数最左侧的位置
3.在一个有序数组中,找<=某个数最右侧的位置

mid=(L+R)/2
不安全,L+R可能会溢出
计算机中要写:mid=L+((R-L)>>1)
除2,等同于,带符号右移一位
乘2,等同于,带符号左移一位
加1,等同于,或1
N*2+1,等同于,N<<1|1

局部最小
三种,头小,尾小,中间小
arr无序,相邻不相等
找任意局部最小

先看首,尾是否是局部最小
若都不满足,则头下降趋势,尾上升趋势

二分,只要过程中是可以去掉一部分的,就可以考虑用二分法

递归算法
mid
如果mid>左,r=mid-1
如果mid>右,r=mid+1
否则返回mid

异或运算

记:无进位相加

性质
0^N=N,
N^N=0
满足交换律和结合律

如果不用额外变量,交换两个数
a=a^b
b=a^b
a=a^b

int a=6, int b=6,内存里是两个6,所以结果没问题
数组的位置相同,内存里是一个,结果会为0

数组中,有一种数出现了奇数次,其他种数出现了偶数次,找到出现了奇数次的数。

eor 异或

怎么把一个int类型的数,提取出最右侧的1
N & (~N+1)

数组中,有两种数出现了奇数次,其他种数出现了偶数次,找到出现了奇数次的数。
首先异或全部,eor=a^b
righOne,得到eor最右侧是1,其余都是0的数
则a和b分为两大阵营
只异或a的阵营计算出a,最后计算出b

链表

单向链表
节点结构,可以实现成泛型

public class Node {
	public int value;
	public Node next;

	public Node(int data){
		value=data;
	}
}

双向链表
节点结构

public class DoubleNode {
	public int value;
	public DoubleNode lastNode;
	public DoubleNode nextNode;

	public Node(int data){
		value=data;
	}
}

如何指定一个链表?只需给它的头节点head

单链表和双链表如何反转?
单链表算法
传入head节点,则给定了一个链表
设两个空节点,pre、next
head是当前节点,
next=head的下一个节点,
pre赋值给head的下一个节点
pre=head
让head向后移动一个节点
跳出循环是head为null

双链表算法
传入head
设定两个节点,pre、next
让next=下一个节点
head的两个指针重新定位
让pre=当前节点
head后跳一个

怎么删除所有的给定值?
算法
传入head和要删的num
先遍历找到第一个不是num的头节点,得到head,未来的返回值
设定pre和cur节点
pre的目的,上一个不等于num的位置
cur当前位置,让pre可以找到cur

java代码可能会产生内存泄漏
原因,变量的生存周期不同

jvm怎么释放内存
引用正在指向内存,则不会释放
释放没有被指着的内存

栈、队列

栈和队列的实现
可以是双向链表
可以是数组

怎么用数组实现不超过固定大小的栈和队列?
栈:正常使用
队列:环形数组

栈,数组
index,放入一个数的位置
拿数,先index–,再拿走

队列,数组
环状的数组
新进来的位置:putindex
要拿走的的位置:pollindex
size,占用的位置数

lru算法
内存替换算法

实现一个栈,有基本功能pop、push,也能返回栈中最小元素getMin,要求时间复杂度都是O(1)
设计一
准备两个栈,一个是正常栈data,一个是最小值栈min
每次加一个数,在正常栈中加它,但是最小栈中放入加的数和栈顶的最小值
最小值栈记录了,即便是弹出了一些数,当前栈的最小值

设计二
还是data栈,和min栈
如果加入的数,比min栈的栈顶大,则不加入。如果小于等于,则压入
弹出,相同,min弹,否则不弹

如何用栈结构,实现队列结构?
两个栈,先全部放入push栈,再依次弹出并放入pop栈。
要求1,pop栈必须为空,才能放入
要求2,push栈必须一次全部给pop栈,要倒完倒干净

如何用队列结构,实现栈结构?
两个队列,一个正着放,一个反着放。


宽度优先遍历用队列做
深度优先遍历用栈做

递归

从思想上理解递归

从实现上理解递归

例子
求数组arr[l…r]中的最大值,用递归实现
算法思想
1.将数组分成两半,左[L…mid],右[mid+1…R]
2.左部分求最大值,右部分求最大值
3.max(左部分的最大值,右部分的最大值)
注意,2是一个递归过程,当范围上只有一个数,就停止递归了。

递归的时间复杂度(特定一类)
在这里插入图片描述

子问题的规模一样,子问题调用了a次,除了子问题,剩余部分的时间复杂度是后面的部分
在这里插入图片描述

哈希表

哈希表在使用层面上,可以理解为一种集合的结构
包括HashSet、HashMap
如果只有key,没有value,是HashSet
如果既有key,也有value,是HashMap
除了有无value,两者实际结构是一回事
哈希表可以增、删、改、查,时间复杂度是O(1),但是常数时间比较大
放入哈希表的东西,如果是基础类型,内部按值传递,内存占用是这个类型的大小;如果不是基础类型,内部按引用传递,内存占用8个字节。

HashMap:map.containsKey(1),map.put()
HashSet:set.contains(“1”),set.add()

布隆过滤器、一致性哈希、哈希表的实现

有序表

TreeMap
用法和hashMap一样,增删改查一样

高级一点的是:可以乱序put,但是其内部是排好序的
缺点:时间复杂度O(logN)
很多种底层的实现:
AVL树
SB树
红黑树
跳表

AVL树、SB树、红黑树,是具备各自不同平衡性的搜索二叉树

搜索二叉树:任意节点,有小左和大右

归并排序:两种视角。左有序,右有序,整体有序;一个数,找它所有的特定右侧。

整体是递归。左边排好序,右边排好序,再整体排好序。
整体排序,用的是外排序
时间复杂度:master公式
当然可以用非递归实现

外排
拷贝一个一样长度的数组,两个指针,谁小拷贝谁,相同的拷贝左边的,一个越界另一个剩余全部拷贝,最后覆盖原来的地方。

base case:问题的最小规模。不用再划分了,直接return
f(0, 0)、f(1, 1)…所以base case中,if(l==r),return

非递归
实际操作的步骤。

过程描述:
外排merge(arr, L, M, R)
两个之间的外排,四个之间的外排…整体分成两个部分的外排。

特殊情况
如果最后右组长度不够,只需确定最后一个数的位置,其余操作一致。
如果最后只剩左组,则不参与融合,进入下一轮。

重要参数构思
左右组:L…M,M+1…R

L:左组开始的位置
每次从0开始,然后下次是R+1,范围<整个长度

M:左组结束的位置
M=L+mergeSize-1
mergeSize:1、2、4、8…,分组内的个数。
从1开始,每次<<1,范围<整个长度。

R:右组结束的位置
长度够:M+mergerSize,长度不够:整个数组结束的位置,两者取min

参数凑齐
调用merge(arr, L, M, R)

循环
L循环,每次外排的头
mergeSize循环,每次外排的长度

if(mergeSize > N/2){break;}

防止溢出
因为下一句mergeSize乘2,不让乘,提前跳出。

递归时间复杂度O(NlogN)
非递归也是

数组中,一个数左边所有比它小的数的和,求数组小和。
每一次merge时,产生小和
但看一个数,
放在右侧时,就不产生自己的小和
放在左侧时,就产生自己的小和,然后,不断扩充没有交集的右侧
注意,每次融合的都是排好序的不重复的右侧
看一个数,
就是找右侧所有比自己大的数

归并排序的两种视角:
1.左边有序,右边有序,整体有序
2.以某个数为参照物,找它的特定右边。

快速排序:荷兰国旗,<区,=区,>区

partition过程(切分)
给定一个数组arr,和一个整数num。把<=num的放在数组左边,>num的放在数组右边。
额外空间复杂度O(1),时间复杂度O(N)

分成左右两边就行,不要求有序

操作
一个指向当前数的指针,一个<=区,指定的数num
指针首先指向-1的位置,<=区在0位置之前
1)[i]<=num,[i]和<=区的下一个数交换,<=区扩充一个,i++
2)[i]>num,i++
结束:指针越界

实质:<=区扩充

荷兰国旗问题
分三块,<区,=区,>区
1)[i]==num,i++
2)[i]<num,[i]与<区的下一个数交换,<区扩充一个,i++
3)[i]>num,[i]与>区的前一个数交换,>区扩充一个,i原地不动
结束:i与>区的首位相遇
函数返回的是=区的首尾位置,组成长度为2的数组。

注意:num选取数组的最后一个数,>区的初始状态是包进它的,这样它的位置就不变了,在这一轮全部结束后,再安排这个数的位置。
最后,数组的最后一个数,和>区的第一个数做交换

以上,引出快排1.0

快排1.0
选取最后一个数,<=区,确定了最后一个数的位置
然后左边、右边各自递归。每次递归,都能确定一个数的位置
结束:切分到L>=R
递归返回的最后一个数排好序的正确位置

快排2.0
荷兰国旗
以数组的最后一个数做划分,<区,=区,>区
最后一个数一开始不动,结束完一轮,交换,确定位置。
<区,>区递归
直到L>=R
递归返回的是等于区的范围

快排1.0和快排2.0的时间复杂度都是O(N2)
考虑最坏情况,数组就是有序的,每一轮确定了一个数,然而数组没有任何变化,一共n轮。

快排3.0
随机选一个数,与最后一个数交换位置,然后作为num
时间复杂度O(NlogN),额外空间复杂度O(logN)
占用的空间,是排好序的那些位置,就是二分,砍几次。其实也是概率,最差O(N),数组每个位置都被记住。

堆:堆结构、heapInsert、heapify

结构:用数组实现的完全二叉树
完全二叉树:满,或叶子层左边满

任一节点i,根从0开始
左:2i+1
右:2
i+2
父:(i-1)/2

任一节点i,根从1开始
左:2i
右:2
i+1
父:i/2
因为,写着方便快速,i<<1,i<<1|1,i>>1

堆,要么是大根堆,要么是小根堆
大根堆
小根堆

大根堆push一个数
首先放在树的末尾,数组的末尾
heapSize记为数组长度
判断和父节点(i-1)/2的大小,如果自己大,就交换位置,然后index来到父亲位置,进行下一轮判断。
index记为变动的节点位置
判断结束条件,比父节点小,或者来到根节点index=0

N个数,logN的高度

插入一个数,调整,相当于走一个高度,所以时间复杂度是logN

pop
N个数,组成大根堆,拿走最大值[0],调整,依然成为大根堆

heapify
堆顶,和左右孩子2i+1、2i+2比较,大的调整为堆顶,index更改,进行下一轮判断。
左右孩子先找到最大的,再和父节点比较找最大的
结束条件,左孩子超过heapSize。

push()里调用heapInsert(),一节点上移的过程
pop()里调用heapify(),一节点下沉的过程

堆排序

认为是一个一个给数的,每一步heapInsert,最后成为大根堆。然后交换位置,heapSize-1,heapify,重复操作。
时间复杂度O(NlogN)

优化
认为一开始所有数组成了一个堆,然后从最后一个数开始,作为堆顶(只看它和它的子树,不看上面),都要heapify。
叶子节点都做了一次操作,倒数第二层看了自己一眼,然后下沉一位。最后一层有N/2个节点,倒数第二层有N/4个节点…,时间复杂度O(N)

优势:额外空间复杂度O(1)

系统提供的堆,自己手写的堆

系统实现的堆
比如优先级队列的底层就是堆,默认是小根堆
PriorityQueue

堆的题
已知一个几乎有序的数组。几乎有序的数组是指,如果把数组排好序的话,每个元素移动的距离一定不超过k,且k相对于数组长度比较小。
请选择一个合适的排序策略,对这个数组排序。

假设k=5,则[0,5]位置上肯定有一个最小值,则在[0,5]内创建小根堆,[0]的位置确定,加入[6],在[1,6]内创建小根堆,[1]确定位置…
直到没有数可插入,且小根堆内继续排完序。

在这里插入图片描述

比较器

面对新的数组类型,比较大小

实质就是重载比较运算符

定义
任一比较器,默认的规则:
comp(T o1, T o2)
返回负数,就是o1排在前面
返回正数,就是o2排在前面
返回0,就是o1和o2一样

简写
return o1.id - o2.id;

用法

Arrays.sort(students, new IdAscendingComparator());
PriorityQueue<Integer> heap = new Priority<>(new MyComp());

值变了

总结:
常规的,不需要改堆
如果上了堆的数,要再次改动,不要用系统提供的方法,自己手动改。

前缀树:pass、end、26个孩子节点

把多个字符串加到一棵多叉树上。字符放在路上,节点上是pass和end值。没有路就新建,有路就复用。沿途节点的pass值加1,字符串结束end值加1。

可以完成前缀相关的查询。
有几个“abd”字符串?字符串数组有多个a的前缀?

在这里插入图片描述
每一个字符串都是从头节点开始的

public static class Node1 {
    public int pass;
    public int end;
    public Node1[] nexts;

    public Node1() {
        pass = 0;
        end = 0;
        nexts = new Node1[26];
    }
}

节点初始化,创建了26个孩子节点。因为小写英文单词的总数是26个。

Trie实现
只需要头节点,然后初始化
(1)insert
加入一个单词,首先转为字符数组
准备一个引用node,指向头节点
pass++
index遍历字符串,取出字符
index=当前字符-‘a’,a 0,b 1,c 2…
有路就不动,没路就创建新节点
指向新节点
pass++
结尾end++
(2)delete,先查询是否存在,再删除
(3)search
(4)prefixNumbers 有多少个字符串以pre为前缀的?

第二种
不是a~z
改为哈希表
public HashMap<Integer, Node2> nexts;

桶排序

不基于比较的排序
计数排序、基数排序
时间复杂度O(N)

计数排序
准备一个数组,下标表示年龄,值表示个数。遍历并计数。

基数排序
前提:非负的10进制数
遍历找最大值,100,是3位,别的数补0
准备10个桶,[0~9],每个桶是一个队列,先进先出。
遍历,根据每个数的个位,数字进桶
然后按照桶的顺序,倒出全部数字
然后十位进桶,全部倒出
最后根据百位进桶,全部倒出,结束
总结:先按照个位排序,然后是十位排序,最后是百位排序

基数排序改进
没有准备10个桶

排序算法的稳定性

指排序后,相同的数字相对次序不变
对于基础类型来说,稳定性毫无意义;
但是对于引用型类型来说,稳定性有重要意义

同学的属性包括:班级、年龄
首先按照年龄排序
再在此基础上,按照班级排序
如果有稳定性,则在每个班级内部,年龄从小到大

选择排序没有稳定性,把后面的1,与前面的5,交换。
冒泡排序有稳定性,相等不交换。
插入排序有稳定性,后面的数想放到前面,是一个一个移动腾位置的。
归并排序有稳定性,左边1、1先拷贝完,才拷贝右边的1、1。
快排没有稳定性,当前数和<=区的后一个数作交换。
堆不稳定,子节点和父节点交换

排序算法总结

在这里插入图片描述
总结
1)不基于比较的排序,对样本数据有严格要求,不易改写
2)基于比较的排序,只要规定好两个样本怎么比大小就可以复用
3)基于比较的排序,时间复杂度的最好极限是O(NlogN)
4)时间复杂度O(NlogN)、额外空间复杂度低于O(N)、且稳定的,基于比较的排序是不存在的。
5)为了绝对的速度选快排、为了省空间选堆排、为了稳定性选归并。

链表面试题

笔试:不用在乎空间复杂度,一切为了时间复杂度
面试:时间复杂度第一,但是还要找到空间最省的方法

笔试需要处理输入、输出,需要流,不得不需要很多空间,此时,你可以申请额外空间赶紧做完。

常用的数据结构和技巧
1)使用容器(哈希表、数组等)
2)快慢指针

在这里插入图片描述
1)返回中点或上中点
如果没有节点,或者有一个节点,或者有两个节点,都直接返回head

慢指针一次走一步,快指针一次走两步。快指针到头,慢指针中点。

然后至少有3个节点
慢指针到第2个节点,快指针到第3个节点
如果前面还有两个节点,则慢走1,快走2
返回慢指针
1 2 3

2)返回中点或下中点
如果没有节点,或者只有一个节点,则返回head
如果至少有两个节点,则slow指针和fast指针,都指向第二个节点
while判断,如果还有2个节点,则slow指针移动一步,fast指针移动两步
返回slow指针
1 2

3)返回中点的前一个,或上中点的前一个
两个节点及其以下的,则返回null
至少3个节点,slow指针指向第一个节点,fast指针指向第三个节点
while判断,如果还有2个节点,则slow指针移动一步,fast指针移动两步
返回slow指针
1 2 3

4)返回中点的前一个,或下中点的前一个
一个节点及其以下的,则返回null
两个节点,则返回head
至少3个节点,slow指针指向head,fast指针指向第二个节点
while判断,如果还有2个节点,则slow指针移动一步,fast指针移动两步
返回slow指针
1 2 3

方法二,笔试,不考虑空间复杂度
1)中点/上中点
空,返回null
cur指向头节点
创建一个数组列表,全部放入
(arr.size()-1)/2,返回它

2)中点/下中点
空,返回null
同上
(arr.size()/2),返回它

3)中点的前一个/上中点的前一个
无、1个、2个,返回null
同上
(arr.size()-3)/2,返回它

4)中点的前一个/下中点的前一个
无、1个,返回null
同上
(arr.size()-2)/2,返回它

题目
判断链表是否为回文结构
回文:镜像的,1 2 3 2 1
笔试:栈,特别简单
面试:改动原链表,需要注意边界

笔试:链表全部放入栈中,依次弹出并比对

面试:快慢指针,指到中点,把右半部分的放入栈中,弹出并和前半部分比对
继续聊,最优解,不用容器
快慢指针,慢指针到中间,中间指向null,后面的都改变指针方向,左右指针一起移动并比对
比对完毕还要调整回去
(L指针)1 -> 2 -> 3 <- 2 <- 1(R指针)

题目
将单链表按某值划分成左边小、中间相等、右边大的形式

笔试:把链表放到数组中,在数组中做partition

面试:分成小、中、大三部分,再把各个部分之间串起来
只需要准备6个指针,<区的头尾指针、=区的头尾指针、>区的头尾指针
然后遍历链表,并用指针记录区的头尾,区内的节点连起来,最后把三个区串起来。
在这里插入图片描述
需要考虑<区、=区为空的情况

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值