漫画算法–笔记
数据结构补充知识
散列表
散列表也叫作哈希表(hash table),这种数据结构提供了键(Key)和值(Value)的映射关系。
计算key到index的转化
通过哈希函数,我们可以把字符串或其他类型的Key,转化成数组的下标index。
i
n
d
e
x
=
H
a
s
h
C
o
d
e
(
K
e
y
)
%
A
r
r
a
y
.
l
e
n
g
t
h
index = HashCode (Key) \% Array.length
index=HashCode(Key)%Array.length
散列表读写操作
-
写操作
- 通过计算得到index,然后将value进行存入,但是随着数量的增加,计算出相同的index,就会出现 哈希冲突
-
冲突解决方式
-
开放寻址法
当一个Key通过哈希函数获得对应的数组下标已被占用时,我们可以“另谋高就”,寻找下一个空档位置。
-
链表法
HashMap数组的每一个元素不仅是一个Entry对象,还是一个链表的头节点。每一个Entry对象通过next指针指向它的下一个Entry节点。当新来的Entry映射到与之冲突的数组位置时,只需要插入到对应的链表中即可。
-
-
读操作
第1步,通过哈希函数,把Key转化成数组下标2。
第2步,找到数组下标2所对应的元素,如果这个元素的Key是002936,那么就找到了;如果这个Key不是002936也没关系,由于数组的每个元素都与一个链表对应,我们可以顺着链表慢慢往下找,看看能否找到与Key相匹配的节点。
-
扩容
当经过多次元素插入,散列表达到一定饱和度时,Key映射位置发生冲突的概率会逐渐提高。
Capacity,即HashMap的当前长度
LoadFactor,即HashMap的负载因子,默认值为0.75f衡量HashMap需要进行扩容的条件如下。
H a s h M a p . S i z e > = C a p a c i t y × L o a d F a c t o r HashMap.Size >= Capacity×LoadFactor HashMap.Size>=Capacity×LoadFactor-
步骤
1.扩容,创建一个新的Entry空数组,长度是原数组的2倍。
2.重新Hash,遍历原Entry数组,把所有的Entry重新Hash到新数组中。为什么要重新Hash呢?因为长度扩大以后,Hash的规则也随之改变。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-V4XecqpM-1667801087716)(https://cdn.jsdelivr.net/gh/hututu-tech/IMG-gongfeng@main/2022/03/10/62295abcba928.jpg)]
经过扩容,原本拥挤的散列表重新变得稀疏,原有的Entry也重新得到了尽可能均匀的分配。
-
树
补充知识
树(tree)是n(n≥0)个节点的有限集。当n=0时,称为空树。在任意一个非空树中,有如下特点。
有且仅有一个特定的称为根的节点。
当n>1时,其余节点可分为m(m>0)个互不相交的有限集,每一个集合本身又是一个树,并称为根的子树。
二叉树遍历
归类
- 深度优先遍历(前序遍历、中序遍历、后序遍历)。
- 广度优先遍历(层序遍历)。
解析
- 前序遍历
输出顺序是根节点、左子树、右子树。
- 中序遍历
输出顺序是左子树、根节点、右子树
- 后序遍历。
输出顺序是左子树、右子树、根节点
深度优先
这3种遍历方式的区别,仅仅是输出的执行位置不同:前序遍历的输出在前,中序遍历的输出在中间,后序遍历的输出在最后。
public static void inOrderTraveral(TreeNode node){
if(node == null){
return;
}
//System.out.println(node.data);//前序遍历
inOrderTraveral(node.leftChild);
//System.out.println(node.data);中序遍历
inOrderTraveral(node.rightChild);
//System.out.println(node.data);//后序遍历
}
非递归方式
使用栈的进出进行模拟
public static void preOrderTraveralWithStack(TreeNode root){
Stack<TreeNode> stack = new Stack<TreeNode>();
TreeNode treeNode = root;
while(treeNode!=null || !stack.isEmpty()){
//迭代访问节点的左孩子,并入栈
while (treeNode != null){
System.out.println(treeNode.data);
stack.push(treeNode);
treeNode = treeNode.leftChild;
}
//如果节点没有左孩子,则弹出栈顶节点,访问节点右孩子
if(!stack.isEmpty()){
treeNode = stack.pop();
treeNode = treeNode.rightChild;
}
}
}
广度优先
使用队列进行访问
对每一层将其子节点加入队列当中,然后将当前节点推出队列
- 层序遍历。
二叉堆
二叉堆本质上是一种完全二叉树,它分为两个类型。
- 最大堆。
- 最小堆。
二叉堆的自我调整
对于二叉堆,有如下几种操作。
- 插入节点。
- 删除节点。
- 构建二叉堆。
-
插入节点
当二叉堆插入节点时,插入位置是完全二叉树的最后一个位置。
“上浮”
通过与上父节点的大小比较判断是否需要进行交换取值
-
删除节点
二叉堆删除节点的过程和插入节点的过程正好相反,所删除的是处于堆顶的节点。
这时,为了继续维持完全二叉树的结构,我们把堆的最后一个节点临时补到原本堆顶的位置。
“下沉”
从堆顶开始进行与子节点的判断大小
- 两者都满足情况
- 最小堆–交换最小的
- 最大堆–交换最大的
- 两者都满足情况
构建二叉堆
构建二叉堆,也就是把一个无序的完全二叉树调整为二叉堆,本质就是让所有非叶子节点依次“下沉”。
代码实现
二叉堆虽然是一个完全二叉树,但它的存储方式并不是链式存储,而是顺序存储。换句话说,二叉堆的所有节点都存储在数组中。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-90Wi57yv-1667801087716)(https://cdn.jsdelivr.net/gh/hututu-tech/IMG-gongfeng@main/2022/03/10/622965474f0e1.jpg)]
假设父节点的下标是parent,那么它的左孩子下标就是2×parent+1;右孩子下标就是2×parent+2
优先队列
- 最大堆的堆顶是整个堆中的最大元素。
- 最小堆的堆顶是整个堆中的最小元素。
因此,可以用最大堆来实现最大优先队列,这样的话,每一次入队操作就是堆的插入操作,每一次出队操作就是删除堆顶节点。
排序算法
主流的排序算法
-
时间复杂度为
O ( n 2 ) O(n^2) O(n2)
的排序算法
冒泡排序
选择排序
插入排序
希尔排序(希尔排序比较特殊,它的性能略优于O(n 2 ),但又比不上O(nlogn),姑且把它归入本类) -
时间复杂度为
O ( n l o g n ) O(nlogn) O(nlogn)
的排序算法
快速排序
归并排序
堆排序 -
时间复杂度为线性的排序算法
计数排序
桶排序
基数排序
根据稳定性分类:
排序后,相同值的块内顺序是否保持不变
- 稳定排序–不变
- 不稳定排序
冒泡算法
public static void sort(int array[]) {
for (int i = 0; i < array.length - 1; i++) {
for (int j = 0; j < array.length - i - 1; j++) {
if (array[j] > array[j + 1]) {
int temp = array[j];
array[j] = array[j + 1];
array[j + 1] = temp;
}
}
}
}
时间复杂度:n^2
优化
-
如果没有交换的操作–意味着已经排好序,后面的循环就没有必要了
-
登记有序区,不进行排序
public static void sort(int array[]) { int sortBorder = array.length - 1;// int lastIndex = 0; for (int i = 0; i < array.length - 1; i++) { boolean isSorted = true; for (int j = 0; j < sortBorder; j++) { if (array[j] > array[j + 1]) { int temp = array[j]; array[j] = array[j + 1]; array[j + 1] = temp; lastIndex = j; isSorted = false; } } sortBorder = lastIndex; if (isSorted) break; } }
鸡尾酒排序
鸡尾酒排序的元素比较和交换过程是双向的。
先从左到右,再从右到左
每一轮进行判断是否有进行排序,循环的次数相当原来冒泡的一半
public static void sort(int array[]) {
int tmp = 0;
for (int i = 0; i < array.length / 2; i++) {
boolean isSorted = true;
for (int j = i; j < array.length - i - 1; j++) {
if (array[j] > array[j + 1]) {
tmp = array[j];
array[j] = array[j + 1];
array[j + 1] = tmp;
isSorted = false;
}
if (isSorted) break;
}
isSorted = true;
for (int j = array.length - i - 1; j > i; j--) {
if (array[j] < array[j - 1]) {
tmp = array[j];
array[j] = array[j - 1];
array[j - 1] = tmp;
isSorted = false;
}
if (isSorted) break;
}
}
}
快速排序
分治法
快速排序则在每一轮挑选一个基准元素,并让其他比它大的元素移动到数列一边,比它小的元素移动到数列的另一边,从而把数列拆解成两个部分。
基准元素的选择
随机选择一个元素作为基准元素,并且让基准元素和数列首元素交换位置。
元素的交换
- 双边循环法。
- 单边循环法。
双边循环
-
设置基准元素
-
设置左右指针
-
单轮
- 从右指针开始,遍历到小于基准的数停下
- 到左指针,遍历到大于基准的数停下
- 交换左右指针
- 当左右指针重合,设置基准到重合点
- 以基准点分割成两部分,进行递归
- 当数组长度==1,进行返回。
public static void quickSort(int arr[], int startIndex, int endIndex) { //判断返回条件 if (startIndex >= endIndex) return; //进行操作 int pivotIndex = partition(arr, startIndex, endIndex); // 更新参数状态进行递归使用 quickSort(arr, startIndex, pivotIndex - 1); quickSort(arr, pivotIndex + 1, endIndex); } private static int partition(int[] arr, int startIndex, int endIndex) { int left = startIndex, right = endIndex, pviot = arr[startIndex]; while (left < right) { while (arr[right] > pviot) right--; while (arr[left] < pviot) left++; if (left < right) { int temp = arr[left]; arr[left] = arr[right]; arr[right] = temp; } } arr[startIndex] = arr[left]; arr[left] = pviot; return left; }
单边循环
- 设置基准位置,mark指针标记小于基准部分边界
- 对元素进行遍历
- 当发现小于基准时,mark指针右移,且与当前元素进行交换
- 最后mark成为基准
private static int partition(int[] arr, int startIndex, int endIndex) {
int mark = startIndex, pviot = arr[startIndex];
for (int i = startIndex + 1; i <= endIndex; i++) {
//判断当前元素是否比基准小
if (arr[i] < pviot) {
mark++;
int temp = arr[mark];
arr[mark] = arr[i];
arr[i] = temp;
}
}
//交换基准位置
arr[startIndex] = arr[mark];
arr[mark] = pviot;
return mark;
}
堆排序
使用最大堆或者最小堆的**“上升”以及“下沉”**进行删除的操作实现数组大小的排序过程
步骤
-
把无序数组构建成二叉堆。需要从小到大排序,则构建成最大堆;需要从大到小排序,则构建成最小堆。(从下往上看)
-
循环删除堆顶元素,替换到二叉堆的末尾,调整堆产生新的堆顶。
计数排序
根据数的范围创建一个统计型数列,
数列的下标指的是数值,值对应这个值出现的次数。
public static int[] countSort(int[] array) {
int max = 0;
for (int i : array) {
if (i > max) max = i;
}
int[] tempArray = new int[max + 1];
for (int item : array) {
tempArray[item]++;
}
int index=0;
int[] res = new int[array.length];
for (int i = 0; i < tempArray.length; i++) {
for (int j = 0; j < tempArray[i]; j++) {
res[index++] = i;
}
}
return res;
}
优化
最小值不一定是0,而且如果是90–100,索引数组的前部分就会被浪费
因此应该通过最大最小值进行数组的创建,并且使用偏移值进行下标的对应计算
public static int[] countSort(int[] array) {
int max = 0;
int min = 0;
for (int i : array) {
if (i > max) max = i;
else min = i;
}
int[] tempArray = new int[max - min + 1];
for (int item : array) {
tempArray[item-min]++;
}
int index = 0;
int[] res = new int[array.length];
for (int i = 0; i < tempArray.length; i++) {
for (int j = 0; j < tempArray[i]; j++) {
res[index++] = i+min;
}
}
return res;
}
以上的问题是一种不稳定排序,因为同一分数下需要进行进行数据对象的区分,此时就需要进行优化
进行了统计之后,对统计数组进行变形,从第二个元素开始,数值=当前数值+前面的和
- 关键
- 进行遍历插入的时候,需要从原数组后面向前进行访问
- (同一分数下,存在多个)在进行数据的访问之后,需要对该位置的数值进行减一
局限
- 最值区间过大
- 元素不是整数
桶排序
将数组范围进行均等平分,分成n-1个区间,将数据遍历装载到每个区间内,遍历对每个区间进行内部的排序。
区间跨度 = (最大值-最小值)/ (桶的数量 - 1)
面试中的算法
链表相关
链表是否有环
-
使用hashSet进行对经过路径的保存
-
使用快慢指针,因为有环的话,快的必定会再遇到慢的。
-
注意
在遍历的过程当中,针对快指针是否有值来判断该链是否有尾部。
-
public boolean hasCycle(ListNode head) {
ListNode slow = head;
ListNode fast = head;
int length = 0;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (fast == slow) {
slow = slow.next;
//第一次进入相遇点,开始从这里开始进行步长的判断
fast = fast.next.next;
length++;
while (fast != slow) {
slow = slow.next;
fast = fast.next.next;
length++;
}
//再次相遇,进行推出
}
}
return false;
}
拓展
-
环的长度
-
当第一次相遇后开始计算,到达第二次相遇停止
-
环的长度=速度差*前进次数
-
-
入环点的计算
最小栈的实现
-
通过备用栈进行最小值的记录
class MinStack { Stack<Integer> stackA; Stack<Integer> stackB; public MinStack() { stackA = new Stack<>(); stackB = new Stack<>(); } public void push(int val) { stackA.push(val); if (!stackB.isEmpty()) {//有就和栈顶比较 //判断是否比栈顶要小 if (stackB.peek() >= val) { stackB.push(val); } } else {//没有就直接入栈 stackB.push(val); } } public void pop() { int temp = stackA.pop(); if (stackB.peek() == temp) { stackB.pop(); } } public int top() { return stackA.peek(); } public int getMin() { return stackB.peek(); } }
求出最大公约数
计算两个数之间的最大公约数
-
辗转相除法
a % b = c − − − > b % c = d − − > c % d = e a\%b=c--->b\%c=d-->c\%d=e a%b=c−−−>b%c=d−−>c%d=e -
更相减损法
a − b = c − − > b − c = d − − > c − d = e a-b=c-->b-c=d-->c-d=e a−b=c−−>b−c=d−−>c−d=e
- 综合使用,当两者是偶数时,可以对两者进行移位(左/右 1位)(乘/除 2),因为此时肯定是可以得到是2的某个次数
- 通过&位的操作判断奇偶数
2的整数次幂
通过位运算的使用,2的整数次幂最高位都是为1,因为乘以2就相当于进行左移一位
那么通过位运算就可以得到是否为2的整数次幂
r
e
t
u
r
n
(
n
u
m
&
n
u
m
−
1
)
=
=
0
;
return (num\&num-1) == 0;
return(num&num−1)==0;
- 位运算可以实现更快的速度并且契合数据在计算机中的储存形式。
无序数组排序后的最大相邻差
-
计数排序
面对无序的数组,通过构建新的索引统计数组,可以实现数组的有序化。
但是不能处理范围摆动过大的数组,不然会浪费很多空间
-
桶排序
-
通过划分桶的划分,在装入data的时候更新该桶的最值,那么在桶范围内的元素并就自然失去了决定性作用(实际上也不需要,因为起决定作用的是桶内的最值)
-
从第二个桶开始,比较当前桶的最大值和前一个的最小值的差距,更新返回值
-
public static int getMaxSortedDistance(int[] array) {
int max = array[0], min = array[0];
for (int i : array) {
if (i > max) max = i;
else if (i < min) min = i;
}
int d = max - min;
if (d == 0) return 0;
int bucketNum = array.length;
Bucket[] buckets = new Bucket[bucketNum];
for (int i = 0; i < bucketNum; i++) {
buckets[i] = new Bucket();
}
for (int j : array) {
//计算当前数在哪个桶内
int index = ((j - min) * (bucketNum - 1) / d);
//更新该桶的最值情况
if (buckets[index].min == null || buckets[index].min > j) {
buckets[index].min = j;
}
if (buckets[index].max == null || buckets[index].max < j) {
buckets[index].max = j;
}
}
int res = 0;
//从第二个桶开始,进行计算更新返回值
for (int i = 1; i < buckets.length; i++) {
if (buckets[i].min == null) continue;//如果该桶没有最小值,那么没有比较,直接跳过说明没有值
res = Math.max(res, buckets[i].max - buckets[i - 1].min);
}
return res;
}
private static class Bucket {
public Integer min;
public Integer max;
}
用栈实现队列
- 使用备用栈进行来回的存储来取出原来在栈低的数值,取完再放回去,如果是存就直接加入栈
class MyQueue {
private Stack<Integer> stack1;
private Stack<Integer> stack2;
private int front;
/** Initialize your data structure here. */
public MyQueue() {
stack1 = new Stack<>();
stack2 = new Stack<>();
}
/** Push element x to the back of queue. */
public void push(int x) {
if (stack1.isEmpty())
front = x;
stack1.push(x);
}
public int pop(){
if(stack2.isEmpty()){
while(!stack1.isEmpty()){
stack2.push(stack1.pop());
}
}
return stack2.pop();
}
/** Get the front element. */
public int peek() {
if(!stack2.isEmpty()){
return stack2.peek();
}
return front;
}
/** Returns whether the queue is empty. */
public boolean empty() {
return stack1.isEmpty()&&stack2.isEmpty();
}
}
寻找全排列的下一个数
在一个整数所包含数字的全部组合中,找到一个大于且仅大于原数的新整数
- 要点
- 尽量保持最高位的不变,因此需要对低位进行遍历获取低位上的逆序部分
- 将将逆序当中比较小的但是比高位大的那个与前一位高位进行交换
- 逆序部分进行排序
这样就能最小地变换最小的那个高位,保证逆序区域的最大
class Solution {
public void nextPermutation(int[] nums) {
//返回要交换的高位
int index = findTransferPoint(nums);
//如果为0,说明已经是最小的那个,进行翻转
if (index == 0) {
nums = reverse(nums, 0);
return;
}
//交换高位
nums = exchangeHead(nums, index);
//将逆序部分进行排序,就是翻转
nums = reverse(nums, index);
}
private int[] reverse(int[] exnums, int index) {
for (int i = index, j = exnums.length - 1; i < j; i++, j--) {
int temp = exnums[i];
exnums[i] = exnums[j];
exnums[j] = temp;
}
return exnums;
}
private int[] exchangeHead(int[] nums, int index) {
int head = nums[index - 1];
for (int i = nums.length - 1; i > 0; i--) {
if (nums[i] > head) {
nums[index - 1] = nums[i];
nums[i] = head;
break;
}
}
return nums;
}
private int[] reverseCopy(int[] nums) {
int[] newCopy = new int[nums.length];
for (int i = 0; i < nums.length; i++) {
newCopy[i] = nums[nums.length - 1 - i];
}
return newCopy;
}
private int findTransferPoint(int[] nums) {
for (int i = nums.length - 1; i > 0; i --) {
if (nums[i] > nums[i - 1]) {
return i;
}
}
return 0;
}
}
删去k个数字后的最小值
- 针对正序的第一个小于前一个的数进行删除
- 要点
- 通过栈进行可用字符的装入
- 判断是否要将前一个数进行删除
- 查找偏移值(第一个非0的位置)
class Solution {
public String removeKdigits(String num, int k) {
//删除后的字符串长度
int newLength = num.length() - k;
//用来保存每个字符的栈
char[] stack = new char[num.length()];
int top = 0;
//对每个字母
for (int i = 0; i < num.length(); i++) {
//取出这个字母
char c = num.charAt(i);
//当未删除完但是已经比前一个数组要小
//符合条件
while (top > 0 && stack[top - 1] > c && k > 0) {
//索引回退一位,相当于删除最后一个大的数
top--;
//可用次数减小一个
k--;
}
//不符合删除条件就加入
stack[top++] = c;
}
//找到第一个非零数字的数值
int offset = 0;
while (offset < newLength && stack[offset] == '0') {
offset++;
}
//从偏移位置开始生成新的字符串
return offset==newLength?"0":new String(stack,offset,newLength-offset);
}
}
如何实现大整数相加
- 针对超大数字的计算,此时数据类型已经没有办法进行保存和记录
- 使用字符串进行对数字的保存,用过构建数组进行数值的加减计算,结果数组需要比较长数组的位数多一位,保持进位的空位。
缺失的整数
通过异或的操作
算法的实际使用
Bitmap的使用
- 要点
- 类似于热编码一样,通过构建字典来完成数据标签的索引
- 通过唯一标识的主键id,将数据用位的1和0进行表征,代表这个标签的有无。
- 通过位运算进行标签的聚合以及对象属性的计算
LRU算法的应用
在日常的运作当中,很多信息都会进行读取,但是当访问量大的时候就会形成一定的问题
因此通常会考虑将访问较为频繁的信息数据保存在缓存当中,不需要进入数据库进行读取
那么就需要考虑如果进行数据的存放
Least Recently Used
通过链式哈希表进行实现
- 取用
- 如果原来有—将数据调到最前
- 如果没有—在头部进行创建
- 如果已经超出了范围,将尾部删除