哦这是我三年前写的,我现在Java语法都快忘光了......
反正之前的博文也发一下好了。这个因为我当年是用有道云笔记而不是直接用CSDN编辑器写的,所以后面有些内容写乱了,因为我现在猛的一看有点看不懂,所以......等我过段时间看懂了再回来把排版重新改一下,酱。
文章目录
- Chapter1:算法与数据结构常识
- Chapter2:线性数据结构
- Chapter3:树
- Chapter4:排序算法
-
- [1. 冒泡排序](#1. 冒泡排序)
- [2. 快速排序](#2. 快速排序)
- [3. 堆排序](#3. 堆排序)
- [4. 计数排序](#4. 计数排序)
-
- [4.1 原始版与优化step1](#4.1 原始版与优化step1)
- [4.2 优化step2](#4.2 优化step2)
- [5. 桶排序](#5. 桶排序)
- Chapter5:面试中的算法
-
- [1. 如何判断链表有环](#1. 如何判断链表有环)
-
- [1.1 方法一:暴力遍历](#1.1 方法一:暴力遍历)
- [1.2 方法二:哈希表](#1.2 方法二:哈希表)
- [1.3 方法三:两个指针](#1.3 方法三:两个指针)
- [1.4 扩展问题:环的长度](#1.4 扩展问题:环的长度)
- [1.5 扩展问题:出入环节点](#1.5 扩展问题:出入环节点)
- [2. 最小栈](#2. 最小栈)
-
- [2.1 有问题的方法:把栈中的最小元素下标暂存起来](#2.1 有问题的方法:把栈中的最小元素下标暂存起来)
- [2.2 方法:存储栈中曾经的最小值](#2.2 方法:存储栈中曾经的最小值)
- [3. 最大公约数](#3. 最大公约数)
-
- [3.1 方法一:暴力枚举法](#3.1 方法一:暴力枚举法)
- [3.2 方法二:辗转相除法/欧几里得算法](#3.2 方法二:辗转相除法/欧几里得算法)
- [3.3 方法三:辗转相减法/更相减损法](#3.3 方法三:辗转相减法/更相减损法)
- [3.4 方法四:移位运算](#3.4 方法四:移位运算)
- [4. 2的整数次幂](#4. 2的整数次幂)
-
- [4.1 方法一:暴力枚举法](#4.1 方法一:暴力枚举法)
- [4.2 方法二](#4.2 方法二)
- [5. 无序数组排序后的最大相邻差](#5. 无序数组排序后的最大相邻差)
-
- [5.1 方法一:排序后求解](#5.1 方法一:排序后求解)
- [5.2 方法二:利用计数排序的思想](#5.2 方法二:利用计数排序的思想)
- [5.3 方法三:利用桶排序的思想](#5.3 方法三:利用桶排序的思想)
- [6. 用栈实现队列](#6. 用栈实现队列)
- [7. 寻找全排列的下一个数](#7. 寻找全排列的下一个数)
- [8. 删去k个数字后的最小值](#8. 删去k个数字后的最小值)
-
- [8.1 性能不好的代码](#8.1 性能不好的代码)
- [8.2 优化后的代码](#8.2 优化后的代码)
- [9. 如何实现大整数相加](#9. 如何实现大整数相加)
- [10. 如何求解金矿问题](#10. 如何求解金矿问题)
-
- [10.1 优化前的代码](#10.1 优化前的代码)
- [10.2 优化step1](#10.2 优化step1)
- [10.3 优化step2](#10.3 优化step2)
- [11. 寻找缺失的整数](#11. 寻找缺失的整数)
-
- [11.1 解法1](#11.1 解法1)
- [11.2 解法2](#11.2 解法2)
- [11.3 解法3](#11.3 解法3)
- [11.4 问题扩展step1](#11.4 问题扩展step1)
- [11.5 问题扩展step2](#11.5 问题扩展step2)
- Chapter6:算法的业务应用
Chapter1:算法与数据结构常识
- 线性结构:数组、链表 栈、队列、哈希表
树:二叉树 二叉堆
图 - 时间复杂度
- 基本操作执行次数 T n T_{n} Tn ( log 2 n \log_2{n} log2n)
- 渐进时间复杂度
大O表示法 (最高阶项)
若存在函数 f ( n ) f_{(n)} f(n),使得当 n → ∞ n\to\infty n→∞时, T ( n ) f ( n ) \frac{T_{(n)}}{f_{(n)}} f(n)T(n)的极限值为不等于0的常数,则称 f ( n ) f_{(n)} f(n)是 T ( n ) T_{(n)} T(n)的同数量级函数 。记作 T ( n ) = O ( f ( n ) ) T_{(n)}=O(f_{(n)}) T(n)=O(f(n)),称为 O ( f ( n ) ) O(f_{(n)}) O(f(n)), O O O为算法的渐进时间复杂度 ,简称为时间复杂度
- 空间复杂度 S ( n ) = O ( f ( n ) ) S_{(n)}=O(f_{(n)}) S(n)=O(f(n))
空间性质 | 空间复杂度 |
---|---|
常量空间 | O ( 1 ) O(1) O(1) |
线性空间 | O ( n ) O(n) O(n) |
二维空间 | O ( n 2 ) O(n^2) O(n2) |
递归空间 |
Chapter2:线性数据结构
- 数组array 顺序存储 顺序表
根据下标读取元素的方式:随机读取
读取和更新的时间复杂度: O ( 1 ) O(1) O(1)
插入/删除: O ( n ) O(n) O(n)
读操作多,写操作少 - 链表 随机存储
2.1 单向链表
2.2 双向链表
链表
- 查找节点:最坏的时间复杂度是 O ( n ) O(n) O(n)
- 更新节点: O ( 1 ) O(1) O(1)
- 插入节点:尾部/头部/中间 插入 O ( 1 ) O(1) O(1)
- 删除元素:尾部/头部/中间 删除 O ( 1 ) O(1) O(1) (Java自动化垃圾回收机制)
-
物理结构和逻辑结构
-
栈FILO
- 最早进入的元素存放的位置:栈底bottom
- 最后进入的元素存放的位置:栈顶top
- 入栈push
- 出栈pop
- O ( 1 ) O(1) O(1)
- 用于:对历史的回溯------递归、面包屑导航
-
队列FIFO
- 队列的出口端:队头front
- 队列的入口端:队尾rear
- 入队enque
- 出队deque(循环队列)
- O ( 1 ) O(1) O(1)
- 用于:对历史的回放------多线程中争夺公平锁的等待队列、爬虫实现网络抓取
-
双端队列deque
-
优先队列 谁的优先级最高,谁先出队
-
散列表/哈希表 Key-Value 本质:数组
- 哈希函数 Key和数组下标进行转换
取模
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 - 读get、写put(entry):接近 O ( 1 ) O(1) O(1)
- 写操作:哈希冲突
- 开放寻址法:下一个空档位置 ThreadLocal
- 链表法:HashMap
- 扩容resize:
- 哈希函数 Key和数组下标进行转换
扩容 重新哈希
Chapter3:树
- 树tree是n(n≥0)个节点的有限集。当n=0时,称为空树。在任意一个非空树中,有如下特点:
- 有且仅有一个特定的称为根的节点。
- 当n>1时,其余节点可分为m(m>0)个互不相交的有限集,每一个集合的本身又是一个树,并称为根的子树。
java
根节点root
叶子节点leaf
子树
层级
父节点parent
孩子节点child
兄弟节点sibling
最大层级数:高度或深度
- 二叉树:每个节点最多有两个孩子节点
java
左孩子left child
右孩子right child
- 满二叉树:所有非叶子节点都存在左右孩子,并且所有叶子节点都在同一层级上
- 完全二叉树:对一个有n个节点的二叉树,按层级顺序编号,则所有节点的编号为从1到n。如果这个树所有节点和同样深度的满二叉树的编号为1到n的节点位置相同,则这个二叉树为完全二叉树
完全二叉树是满二叉树缺最后几个叶子节点
-
二叉树的存储方式
3.1 链式存储结构
3.2 数组
parent 左孩子2parent+1 右孩子2parent+2
leftChild 父节点(leftChild-1)/2
应用:二叉堆 -
二叉查找树 (排序)
(1)如果左子树不为空,则左子树上所有节点的值均小于根节点的值
(2)如果右子树不为空,则右子树上所有节点的值均大于根节点的值
(3)左、右子树也都是二叉查找树
节点分布相对均衡的二叉查找树:搜索时间复杂度 O ( n ) O(n) O(n)
自平衡 -
二叉查找树的深度优先遍历 栈
- 前序遍历:根节点、左子树、右子树
- 中序遍历:左子树、根节点、右子树
- 后序遍历:左子树、右子树、根节点
-
二叉查找树的广度优先遍历 队列
-
二叉堆
- 本质:完全二叉树
- 分类
(1)最大堆:任何一个父节点的值都大于等于左、右子节点值
(2)最小堆:任何一个父节点的值都小于等于左、右子节点值 - 根节点:堆顶
- 数组存储
-
二叉堆的自我调整
- 插入 O ( log n ) O(\log{n}) O(logn):最后一个位置→上浮
- 删除 O ( log n ) O(\log{n}) O(logn):(删的是堆顶)最后一个节点临时补到堆顶→下沉
- 构建二叉堆 O ( n ) O(n) O(n):把无序的完全二叉树调整为二叉堆,本质就是让所有非叶子节点依次"下沉"(从最后一个非叶子节点开始)
-
优先队列
- 最大优先队列:当前最大的元素优先出队 最大堆
- 最小优先队列:当前最小的元素优先出队 最小堆
Chapter4:排序算法
分类一
时间复杂度 | 算法 |
---|---|
O ( n 2 ) O(n^2) O(n2) | 冒泡、插入、选择、希尔? |
O ( n log n ) O(n\log{n}) O(nlogn) | 快速、堆、归并 |
线性 | 计数、桶、基数 |
快速排序最坏时间复杂度是 O ( n 2 ) O(n^2) O(n2)
分类二
稳定性 | 算法 |
---|---|
稳定 | 值相同的元素在排序后仍然保持着排序前的顺序 |
不稳定 |
1. 冒泡排序
- 两两交换,最大到最小
- 稳定排序
- 优化
- 有序标记:isSorted
- 数列有序区(记录最后一次元素交换的位置)
- 鸡尾酒排序:双向
(大部分元素已经有序的情况)
2. 快速排序
(交换排序) 平均空间复杂度是 O ( log n ) O(\log{n}) O(logn)
- 基准元素的选择
- 元素的交换
- 双边循环法:左右两个指针(同时比成,则交换)一边与基准元素比一边相靠,最后与基准元素交换
java
public class Main {
//递归实现双边循环法的快速排序
public static void quickSort(int[] arr,int startIndex,int endIndex){
//递归结束条件:startIndex大于或等于endIndex时
if(startIndex>=endIndex){
return;
}
//得到基准元素位置
int pivotIndex=partition(arr,startIndex,endIndex);
//根据基准元素,分成两部分进行递归排序
quickSort(arr,startIndex,pivotIndex-1);
quickSort(arr,pivotIndex+1,endIndex);
}
/*
* 分治(双边循环法)
* @param arr 待交换的数组
* @param startIndex 起始下标
* @param endIndex 结束下标
* */
private static int partition(int[] arr,int startIndex,int endIndex){
//取第1个位置(也可以选择随机位置)的元素作为基准元素
int pivot=arr[startIndex];
int left=startIndex;
int right=endIndex;
while(left!=right){
//控制right指针比较并左移
while(left<right&&arr[right]>pivot){
right--;
}
//控制left指针比较并右移
while(left<right&&arr[left]<=pivot){
left++;
}
//交换left和right指针所指向的元素
if(left<right){
int p=arr[left];
arr[left]=arr[right];
arr[right]=p;
}
}
//pivot和指针重合点交换
arr[startIndex]=arr[left];
arr[left]=pivot;
return left;
}
public static void main(String[] args) {
int[] arr=new int[] {4,4,6,5,3,2,8,1};
quickSort(arr,0,arr.length-1);
System.out.println(Arrays.toString(arr));
}
}
2.2.2 单边循环法:用mark指针来标示小于基准元素的区域,把小于基准元素的都放到此区域。最后把基准元素放到mark位置
java
//仅有partition函数变化
private static int partition(int[] arr,int startIndex,int endIndex){
//取第1个位置(也可以选择随机位置)的元素作为基准元素
int pivot=arr[startIndex];
int mark=startIndex;
for(int i=startIndex+1;i<=endIndex;i++){
if(arr[i]<pivot){
mark++;
int p=arr[mark];
arr[mark]=arr[i];
arr[i]=p;
}
}
arr[startIndex]=arr[mark];
arr[mark]=pivot;
return mark;
}
2.2.3 不用递归的方法
java
//仅有quickSort方法变化
public static void quickSort(int[] arr,int startIndex,int endIndex){
//用一个集合栈来代替递归的函数栈
Stack<Map<String,Integer>> quickSortStack=new Stack<Map<String,Integer>>();
//整个数列的起止下标,以哈希的形式入栈
Map rootParam=new HashMap();
rootParam.put("startIndex", startIndex);
rootParam.put("endIndex", endIndex);
quickSortStack.push(rootParam);
//循环结束条件:栈为空时
while(!quickSortStack.isEmpty()){
//栈顶元素出栈,得到起止下标
Map<String,Integer> param=quickSortStack.pop();
//得到基准元素位置
int pivotIndex=partition(arr,param.get("startIndex"),param.get("endIndex"));
//根据基准元素分成两部分,把每一部分的起止下标入栈
if(param.get("startIndex")<pivotIndex-1){
Map<String,Integer> leftParam=new HashMap<String,Integer>();
leftParam.put("startIndex", param.get("startIndex"));
leftParam.put("endIndex", pivotIndex-1);
quickSortStack.push(leftParam);
}
if(pivotIndex+1<param.get("endIndex")){
Map<String,Integer> rightParam=new HashMap<String,Integer>();
rightParam.put("startIndex", pivotIndex+1);
rightParam.put("endIndex", param.get("endIndex"));
quickSortStack.push(rightParam);
}
}
}
3. 堆排序
3.1 把无序数组构建成二叉堆。需要从小到大排序,则构建成最大堆;需要从大到小排序,则构建成最小堆。
3.2 循环删除堆顶元素,替换到二叉堆的末尾,调整堆产生新的堆顶
java
public class Main {
/**
* "下沉"调整
* @param array 待调整的堆
* @param parentIndex 要下沉的父节点
* @param length 堆的有效大小
*/
public static void downAdjust(int[] array,int parentIndex,int length){
//temp保存父节点值,用于最后的赋值
int temp=array[parentIndex];
int childIndex=2*parentIndex+1;
while(childIndex<length){
//如果有右孩子,且右孩子大于左孩子的值,则定位到右孩子
if(childIndex+1<length&&array[childIndex+1]>array[childIndex]){
childIndex++;
}
//如果父节点大于任何一个孩子的值,则直接跳出
if(temp>=array[childIndex])
break;
//无须真正交换,单向赋值即可
array[parentIndex]=array[childIndex];
parentIndex=childIndex;
childIndex=2*childIndex+1;
}
array[parentIndex]=temp;
}
/**
* 堆排序(升序)
* @param array 待调整的堆
*/
public static void heapSort(int[] array){
//1.把无序数组构建成最大堆
for(int i=(array.length-2)/2;i>=0;i--){
downAdjust(array,i,array.length);
}
System.out.println(Arrays.toString(array));
//2.循环删除堆顶元素,移到集合尾部,调整堆产生新的堆顶
for(int i=array.length-1;i>0;i--){
//最后1个元素和第1个元素进行交换
int temp=array[i];
array[i]=array[0];
array[0]=temp;
//"下沉"调整最大堆
downAdjust(array,0,i);
}
}
public static void main(String[] args){
int[] arr=new int[] {1,3,2,6,5,7,8,9,10,0};
heapSort(arr);
System.out.println(Arrays.toString(arr));
}
}
堆排序
空间复杂度: O ( 1 ) O(1) O(1)
时间复杂度:
第一步:把无序数组构造成二叉堆 O ( n ) O(n) O(n)
第二步:需要进行 n − 1 n-1 n−1次循环。每次循环调用一次downAdjust方法,所以第2步的计算规模是 ( n − 1 ) × log n (n-1)\times\log{n} (n−1)×logn,时间复杂度为 O ( n log n ) O(n\log{n}) O(nlogn)
两个步骤是并列关系,所以整体的时间复杂度是 O ( n log n ) O(n\log{n}) O(nlogn)
4. 计数排序
4.1 原始版与优化step1
利用数组下标来确定元素的正确位置
(1)随机整数,有取值范围,建立长度为取值范围的数组
(2)遍历序列,每一个元素对应下标的数组元素+1
(3)遍历数组,输出数组元素的下标值(元素值为输出次数)
java
public static int[] countSort(int[] array){
//1.得到数列的最大值
int max=array[0];
for(int i=1;i<array.length;i++){
if(array[i]>max){
max=array[i];
}
}
//2.根据数列最大值确定统计数组的长度
/* 算法改进step1:用最大值-最小值+1作为数组的长度,
* 数组的最小值作为偏移量,用于统计整数在统计数组中的下标
*/
int[] countArray=new int[max+1];
//3.遍历数列,填充统计数组
for(int i=0;i<array.length;i++){
countArray[array[i]]++;
}
//4.遍历统计数组,输出结果
int index=0;
int[] sortedArray=new int[array.length];
for(int i=0;i<countArray.length;i++){
for(int j=0;j<countArray[i];j++){
sortedArray[index++]=i;
}
}
return sortedArray;
}
public static void main(String[] args){
int[] array=new int[]{4,4,6,5,3,2,8,1,7,5,6,0,10};
int[] sortedArray=countSort(array);
System.out.println(Arrays.toString(sortedArray));
}
4.2 优化step2
要求遵循原表固有顺序(变成稳定排序)
方法:从统计数组的第2个元素开始,每一个元素都加上前面所有元素之和
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XZjeDK0k-1595582632835)(漫画算法3.JPG)]
相加的目的:让统计数组存储的元素值,等于相应整数的最终排序位置的序号
接下来,创建输出数组sortedArray,长度和输入数列一致,然后从后向前遍历输入数列。(下标→位置,统计数组相应-1)
java
public static int[] countSort(int[] array){
//1. 得到数列的最大值和最小值,并算出差值
int max=array[0];
int min=array[0];
for(int i=1;i<array.length;i++){
if(array[i]>max){
max=array[i];
}
if(array[i]<min){
min=array[i];
}
}
int d=max-min;
//2. 创建统计数组并统计对应元素的个数
int[] countArray=new int[d+1];
for(int i=0;i<array.length;i++){
countArray[array[i]-min]++;
}
//3. 统计数组做变形,后面的元素等于前面的元素之和
for(int i=1;i<countArray.length;i++){
countArray[i]+=countArray[i-1];
}
//4. 倒序遍历原始数列,从统计数组找到正确位置,输出到结果数组
int[] sortedArray=new int[array.length];
for(int i=array.length-1;i>=0;i--){
sortedArray[countArray[array[i]-min]-1]=array[i];
countArray[array[i]-min]--;
}
return sortedArray;
}
public static void main(String[] args) {
int[] array=new int[]{95,94,91,98,99,90,99,93,91,92};
int[] sortedArray=countSort(array);
System.out.println(Arrays.toString(sortedArray));
}
如果原始数列的规模是n,最大和最小整数的差值是m,时间复杂度是 O ( n + m ) O(n+m) O(n+m),空间复杂度(如果不考虑结果数组,只考虑统计数组)是 O ( m ) O(m) O(m)
5. 桶排序
- 每一个桶(bucket)代表一个区间范围,里面可以承载一个或多个元素。
- 步骤
2.1 step1:分桶
2.2 step2:把数列元素放进桶
2.3 step3:对每个桶内部的元素分别进行排序
2.4 step4:遍历桶,输出所有元素
java
public static double[] bucketSort(double[] array){
//1. 得到数列的最大值和最小值,并算出差值
double max=array[0];
double min=array[0];
for(int i=1;i<array.length;i++){
if(array[i]>max){
max=array[i];
}
if(array[i]<min){
min=array[i];
}
}
double d=max-min;
//2. 初始化桶
int bucketNum=array.length;
ArrayList<LinkedList<Double>> bucketList=new ArrayList<LinkedList<Double>>(bucketNum);
for(int i=0;i<bucketNum;i++){
bucketList.add(new LinkedList<Double>());
}
//3. 遍历原始数组,将每个元素放入桶中
for(int i=0;i<array.length;i++){
int num=(int)((array[i]-min)*(bucketNum-1)/d);
bucketList.get(num).add(array[i]);
}
//4. 对每个桶内部进行排序
for(int i=0;i<bucketList.size();i++){
//JDK底层采用了归并排序或归并的优化版本
Collections.sort(bucketList.get(i));
}
//5. 输出全部元素
double[] sortedArray=new double[array.length];
int index=0;
for(LinkedList<Double> list:bucketList){
for(double element:list){
sortedArray[index]=element;
index++;
}
}
return sortedArray;
}
public static void main(String[] args) {
double[] array=new double[]{4.12,6.421,0.0023,3.0,2.123,8.122,4.12,10.09};
double[] sortedArray=bucketSort(array);
System.out.println(Arrays.toString(sortedArray));
}
桶排序的性能并非绝对稳定。如果元素的分布极不均衡,在极端情况下,第一个桶中有n-1个元素,最后一个桶中有1个元素。此时的时间复杂度将退化为 O ( n log n ) O(n\log{n}) O(nlogn),而且还白白创建了许多空桶。
Chapter5:面试中的算法
1. 如何判断链表有环
单向链表
1.1 方法一:暴力遍历
方法:依次遍历节点,每遍历一个新节点,就从头检查新节点之前的所有节点,用新节点和之前的所有节点作比较。如果发现新节点和之前的某个节点相同,则说明该节点被遍历过2次,链表有环。
时间复杂度: O ( n 2 ) O(n^2) O(n2)
空间复杂度: O ( 1 ) O(1) O(1)
1.2 方法二:哈希表
方法:创建一个以节点ID为Key的HashSet集合,用来存储曾经遍历过的节点。
时间复杂度: O ( n ) O(n) O(n)
空间复杂度: O ( n ) O(n) O(n)
1.3 方法三:两个指针
方法:首先创建2个指针p1和p2(在Java里就是两个对象引用),让它们同时指向这个链表的头节点。然后开始一个大循环,在循环体中,让指针p1每次向后移动1个节点,让指针p2每次向后移动2个节点,然后比较两个指针指向的节点是否相同。如果相同,则可以判断出链表有环,如果不同,则继续下一次循环。
原理:追及问题 (因为是环形的,所以肯定能追上)
时间复杂度: O ( n ) O(n) O(n)
空间复杂度: O ( 1 ) O(1) O(1)
java
/*
* 判断是否有环
* @param head 链表头节点
* */
public static boolean isCycle(Node head){
Node p1=head;
Node p2=head;
while(p2!=null&&p2.next!=null){
p1=p1.next;
p2=p2.next.next;
if(p1==p2){
return true;
}
}
return false;
}
/*
* 链表节点
* */
public static class Node{
int data;
Node next;
Node(int data){
this.data=data;
}
}
public static void main(String[] args) {
Node node1=new Node(5);
Node node2=new Node(3);
Node node3=new Node(7);
Node node4=new Node(2);
Node node5=new Node(6);
node1.next=node2;
node2.next=node3;
node3.next=node4;
node4.next=node5;
node5.next=node2;
System.out.println(isCycle(node1));
}
1.4 扩展问题:环的长度
方法:当两个指针首次相遇,证明链表有环的时候,让两个指针从相遇点继续循环前进,并统计前进的循环次数,直到两个指针第2次相遇。此时,统计出来的前进次数就是环长。
原理:
- 指针p1每次走1步,指针p2每次走2步,两者的速度差是1步。当2个指针再次相遇时,p2比p1多走了整整1圈。
- 因此,环长=每一次速度差×前进次数=前进次数
1.5 扩展问题:出入环节点
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sWGTmMQW-1595582632839)(漫画算法5.PNG)]
2. 最小栈
题目:实现一个栈,该栈带有出栈(pop)、入栈(push)、取最小元素(getMin)3个方法。要保证这3个方法的时间复杂度都是 O ( 1 ) O(1) O(1)
2.1 有问题的方法:把栈中的最小元素下标暂存起来
问题:这个元素出栈了咋办
2.2 方法:存储栈中曾经的最小值
方法:创建备胎栈B,对每一个入栈的元素,入栈最新的最小值/栈A出栈,按情况让栈B出栈
最坏情况空间复杂度: O ( n ) O(n) O(n)
java
public static class MinStack{
private Stack<Integer> mainStack=new Stack<Integer>();
private Stack<Integer> minStack=new Stack<Integer>();
/**
* 入栈操作
* @param element 入栈的元素
* */
public void push(int element){
mainStack.push(element);
//如果辅助栈为空,或者新元素小于或等于辅助栈栈顶,则将新元素压入辅助栈
if(minStack.empty()||element<=minStack.peek()){
minStack.push(element);
}
}
/**
* 出栈操作
* */
public Integer pop(){
//如果出栈元素和辅助栈栈顶元素值相等,辅助栈出栈
if(mainStack.peek().equals(minStack.peek())){
minStack.pop();
}
return mainStack.pop();
}
/**
* 获取栈的最小元素
* */
public int getMin() throws Exception{
if(mainStack.empty()){
throw new Exception("stack is empty");
}
return minStack.peek();
}
}
public static void main(String[] args) throws Exception{
MinStack stack=new MinStack();
stack.push(4);
stack.push(9);
stack.push(7);
stack.push(3);
stack.push(8);
stack.push(5);
System.out.println(stack.getMin());
stack.pop();
stack.pop();
stack.pop();
System.out.println(stack.getMin());
}
3. 最大公约数
题目:求两个整数的最大公约数
3.1 方法一:暴力枚举法
java
public static int getGreatestCommonDivisor(int a,int b){
int big=a>b?a:b;
int small=a<b?a:b;
if(big%small==0){
return small;
}
for(int i=small/2;i>1;i--){
if(small%i==0&&big%i==0){
return i;
}
}
return 1;
}
public static void main(String[] args){
System.out.println(getGreatestCommonDivisor(25,5));
System.out.println(getGreatestCommonDivisor(100,80));
System.out.println(getGreatestCommonDivisor(27,14));
}
时间复杂度: O ( m i n ( a , b ) ) O(min(a,b)) O(min(a,b))
3.2 方法二:辗转相除法/欧几里得算法
秦九韶算法/霍纳算法
定理:两个正整数a和b(a>b),它们的最大公约数等于a除以b的余数c和b之间的最大公约数。
java
public static int getGrestestCommonDivisorV2(int a,int b){
int big=a>b?a:b;
int small=a<b?a:b;
if(big%small==0){
return small;
}
return getGrestestCommonDivisorV2(big%small,small);
}
public static void main(String[] args){
System.out.println(getGrestestCommonDivisorV2(25,5));
System.out.println(getGrestestCommonDivisorV2(100,80));
System.out.println(getGrestestCommonDivisorV2(27,14));
}
问题:当两个整数较大时,取模运算的性能会比较差。
时间复杂度可以近似为: O ( log ( m a x ( a , b ) ) ) O(\log{(max(a,b))}) O(log(max(a,b)))
3.3 方法三:辗转相减法/更相减损法
定理:两个正整数a和b(a>b),它们的最大公约数等于a-b的差值c和b之间的最大公约数。
java
public static int getGrestestCommonDivisorV3(int a,int b){
if(a==b){
return a;
}
int big=a>b?a:b;
int small=a<b?a:b;
return getGrestestCommonDivisorV3(big-small,small);
}
public static void main(String[] args){
System.out.println(getGrestestCommonDivisorV3(25,5));
System.out.println(getGrestestCommonDivisorV3(100,80));
System.out.println(getGrestestCommonDivisorV3(27,14));
}
问题:更相减损术是不稳定的算法,运算次数可能极多
最坏时间复杂度为 O ( m a x ( a , b ) ) O(max(a,b)) O(max(a,b))
3.4 方法四:移位运算
- 当a和b均为偶数时,gcd(a,b)=2×gcd(a/2,b/2)=2×gcd(a>>1,b>>1)。
- 当a为奇数,b为偶数时,gcd(a,b)=gcd(a,b/2)=gcd(a,b>>1)。
- 当a和b均为奇数时,先利用更相减损术运算一次,gcd(a,b)=gcd(b,a-b),此时a-b必将为偶数,然后又可以继续进行移位运算
java
public static int gcd(int a,int b){
if(a==b){
return a;
}
if((a&1)==0&&(b&1)==0){ //都是偶数
return gcd(a>>1,b>>1)<<1;
}else if((a&1)==0&&(b&1)!=0){
return gcd(a>>1,b);
}else if((a&1)!=0&&(b&1)==0){
return gcd(a,b>>1);
}else{
int big=a>b?a:b;
int small=a<b?a:b;
return gcd(big-small,small);
}
}
public static void main(String[] args){
System.out.println(gcd(25,5));
System.out.println(gcd(100,80));
System.out.println(gcd(27,14));
}
时间复杂度: O ( log ( m a x ( a , b ) ) ) O(\log{(max(a,b))}) O(log(max(a,b)))
4. 2的整数次幂
题目:判断一个正整数是否是2的整数次幂
4.1 方法一:暴力枚举法
java
public static boolean isPowerOf2(int num){
int temp=1;
while(temp<=num){
if(temp==num){
return true;
}
temp*=2; //优化:把乘以2的操作改成向左移位
//temp=temp<<1;
}
return false;
}
public static void main(String[] args){
System.out.println(isPowerOf2(32));
System.out.println(isPowerOf2(19));
}
时间复杂度: O ( log n ) O(\log{n}) O(logn)
4.2 方法二
如果一个整数是2的整数次幂,那么当它转化成二进制时,只有最高位是1,其他位都是0。
这样的数一旦减1,它的二进制数字就全部变成了1。
n&(n-1)=0
java
return (num&num-1)==0;
5. 无序数组排序后的最大相邻差
题目:有一个无序整型数组,如何求出该数组排序后的任意两个相邻元素的最大差值?
5.1 方法一:排序后求解
使用时间复杂度为 O ( n log n ) O(n\log{n}) O(nlogn)的排序算法(如快排)给原数组排序,然后遍历求差。
时间复杂度: O ( n log n ) O(n\log{n}) O(nlogn)
在不改变原数组的情况下,空间复杂度: O ( n ) O(n) O(n)
5.2 方法二:利用计数排序的思想
- 利用计数排序的思想,先求出原数组的最大值max与最小值min的区间长度k(k=max-min+1),以及偏移量d=min。
- 创建一个长度为k的新数组Array。
- 遍历原数组,每遍历一个元素,就把新数组Array对应下标的值+1。遍历结束后,Array的一部分元素值变成了1或更高的数值,一部分元素值仍然是0。
- 遍历新数组Array,统计出Array中最大连续出现0值的次数+1,即为相邻元素最大差值。
问题:数组元素可能差值悬殊
5.3 方法三:利用桶排序的思想
- 利用桶排序的思想,根据原数组的长度n,创建出n个桶,每一个桶代表一个区间范围。其中第一个桶从原数组的最小值min开始,区间跨度是(max-min)/(n-1)。
- 遍历原数组,把原数组每一个元素插入到对应的桶中,记录每一个桶的最大和最小值。
- 遍历所有的桶,统计出每一个桶的最大值,和这个桶右侧非空桶的最小值的差,数值最大的差即为原数组排序后的相邻最大差值。
时间复杂度: O ( n ) O(n) O(n)
java
public static int getMaxSortedDistance(int[] array){
//1. 得到数列的最大值和最小值
int max=array[0];
int min=array[0];
for(int i=1;i<array.length;i++){
if(array[i]>max){
max=array[i];
}
if(array[i]<min){
min=array[i];
}
}
int d=max-min;
//如果max和min相等,说明数组所有元素都相等,返回0
if(d==0){
return 0;
}
//2. 初始化桶
int bucketNum=array.length;
Bucket[] buckets=new Bucket[bucketNum];
for(int i=0;i<bucketNum;i++){
buckets[i]=new Bucket();
}
//3. 遍历原始数组,确定每个桶的最大最小值
for(int i=0;i<array.length;i++){
//确定数组元素所归属的桶下标
int index=((array[i]-min)*(bucketNum-1)/d);
if(buckets[index].min==null||buckets[index].min>array[i]){
buckets[index].min=array[i];
}
if(buckets[index].max==null||buckets[index].max<array[i]){
buckets[index].max=array[i];
}
}
//4. 遍历桶,找到最大差值
int leftMax=buckets[0].max;
int maxDistance=0;
for(int i=1;i<buckets.length;i++){
if(buckets[i].min==null){
continue;
}
if(buckets[i].min-leftMax>maxDistance){
maxDistance=buckets[i].min-leftMax;
}
leftMax=buckets[i].max;
}
return maxDistance;
}
/**
* 桶
* */
private static class Bucket{
Integer min;
Integer max;
}
public static void main(String[] args){
int[] array=new int[]{2,6,3,4,5,10,9};
System.out.println(getMaxSortedDistance(array));
}
6. 用栈实现队列
两个栈,来来回回倒
java
static class StackQueue{
private Stack<Integer> stackA=new Stack<Integer>();
private Stack<Integer> stackB=new Stack<Integer>();
/**
* 入队操作
* @param element 入队的元素
* */
public void enQueue(int element){
stackA.push(element);
}
/**
* 出队操作
* */
public Integer deQueue(){
if(stackB.isEmpty()){
if(stackA.isEmpty()){
return null;
}
transfer();
}
return stackB.pop();
}
/**
* 栈A元素转移到栈B
* */
private void transfer(){
while(!stackA.isEmpty()){
stackB.push(stackA.pop());
}
}
}
public static void main(String[] args){
StackQueue stackQueue=new StackQueue();
stackQueue.enQueue(1);
stackQueue.enQueue(2);
stackQueue.enQueue(3);
System.out.println(stackQueue.deQueue());
System.out.println(stackQueue.deQueue());
stackQueue.enQueue(4);
System.out.println(stackQueue.deQueue());
System.out.println(stackQueue.deQueue());
}
入队操作的时间复杂度: O ( 1 ) O(1) O(1)
出队操作,如果涉及元素迁移,时间复杂度为 O ( n ) O(n) O(n);如果不用迁移,时间复杂度为 O ( 1 ) O(1) O(1)
均摊时间复杂度
需要元素迁移的出队操作只有少数情况,并且不可能连续出现,其后的大多数出队操作都不需要元素迁移。
所以把时间均摊到每一次出队操作上面,其时间复杂度是 O ( 1 ) O(1) O(1)。
7. 寻找全排列的下一个数
题目:给出一个正整数,找出这个正整数所有数字全排列的下一个数。
方法:
- 从后向前查看逆序区域,找到逆序区域的前一位,也就是数字置换的边界。
- 让逆序区域的前一位和逆序区域中大于它的最小的数字交换位置。
- 把原来的逆序区域转为顺序状态。
java
//为方便数字的交换,入参和返回值的类型都采用了整型数组
public static int[] findNearestNumber(int[] numbers){
//1. 从后向前查看逆序区域,找到逆序区域的前一位,也就是数字置换的边界
int index=findTransferPoint(numbers);
//如果数字置换边界是0,说明整个数组已经逆序,无法得到更大的相同数字组成的整数,返回null
if(index==0){
return null;
}
//2. 把逆序区域的前一位和逆序区域中刚刚大于它的数字交换位置
//复制并入参,避免直接修改入参
int[] numbersCopy=Arrays.copyOf(numbers, numbers.length);
exchangeHead(numbersCopy,index);
//3. 把原来的逆序区域转为顺序
reverse(numbersCopy,index);
return numbersCopy;
}
private static int findTransferPoint(int[] numbers){
for(int i=numbers.length-1;i>0;i--){
if(numbers[i]>numbers[i-1]){
return i;
}
}
return 0;
}
private static int[] exchangeHead(int[] numbers,int index){
int head=numbers[index-1];
for(int i=numbers.length-1;i>0;i--){
if(head<numbers[i]){
numbers[index-1]=numbers[i];
numbers[i]=head;
break;
}
}
return numbers;
}
private static int[] reverse(int[] num,int index){
for(int i=index,j=num.length-1;i<j;i++,j--){
int temp=num[i];
num[i]=num[j];
num[j]=temp;
}
return num;
}
public static void main(String[] args){
int[] numbers={1,2,3,4,5};
//打印12345 之后的10个全排列整数
for(int i=0;i<10;i++){
numbers=findNearestNumber(numbers);
outputNumbers(numbers);
}
}
//输出数组
private static void outputNumbers(int[] numbers){
for(int i:numbers){
System.out.print(i);
}
System.out.println();
}
解法名:字典序算法
时间复杂度: O ( n ) O(n) O(n)
8. 删去k个数字后的最小值
问题:给出一个整数,从该整数中去掉k个数字,要求剩下的数字形成的新整数尽可能小。(给出的整数的大小可以超过long类型的数字范围)
每一次删掉比右边数字大的数字(第一个顺序的最后一个元素)
依次求得局部最优解 ,最终得到全局最优解 的思想,叫做贪心算法
8.1 性能不好的代码
java
/**
* 删除整数的k个数字,获得删除后的最小值
* @param num 原整数
* @param k 删除数量
* */
public static String removeKDigits(String num,int k){
String numNew=num;
for(int i=0;i<k;i++){
boolean hasCut=false;
//从左向右遍历,找到比自己右侧数字大的数字并删除
for(int j=0;j<numNew.length()-1;j++){
if(numNew.charAt(j)>numNew.charAt(j+1)){
numNew=numNew.substring(0,j)+numNew.substring(j+1,numNew.length());
hasCut=true;
break;
}
}
//如果没有找到要删除的数字,则删除最后一个数字
if(!hasCut){
numNew=numNew.substring(0,numNew.length()-1);
}
//清除整数左侧的数字0
numNew=removeZero(numNew);
}
//如果整数的所有数字都被删除了,直接返回0
if(numNew.length()==0){
return "0";
}
return numNew;
}
private static String removeZero(String num){
for(int i=0;i<num.length()-1;i++){
if(num.charAt(0)!='0'){
break;
}
num=num.substring(1,num.length());
}
return num;
}
public static void main(String[] args){
System.out.println(removeKDigits("1593212",3));
System.out.println(removeKDigits("30200",1));
System.out.println(removeKDigits("10",2));
System.out.println(removeKDigits("541270936",3));
}
时间复杂度: O ( k n ) O(kn) O(kn)
缺点:
- 每次内层循环都需要从头开始遍历所有数字(应该停留在上一次删除的位置继续)
- subString方法的时间复杂度是 O ( n ) O(n) O(n),性能不高
8.2 优化后的代码
java
/**
* 删除整数的k个数字,获得删除后的最小值
* @param num 原整数
* @param k 删除数量
* */
public static String removeKDigits(String num,int k){
//新整数的最终长度=原整数长度-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-=1;
k-=1;
}
//遍历到的当前数字入栈
stack[top++]=c;
}
//找到栈中第1个非零数字的位置,以此构建新的整数字符串
int offset=0;
while(offset<newLength&&stack[offset]=='0'){
offset++;
}
return offset==newLength?"0":new String(stack,offset,newLength-offset);
}
public static void main(String[] args){
System.out.println(removeKDigits("1593212",3));
System.out.println(removeKDigits("30200",1));
System.out.println(removeKDigits("10",2));
System.out.println(removeKDigits("541270936",3));
}
时间复杂度: O ( n ) O(n) O(n)
空间复杂度: O ( n ) O(n) O(n)
9. 如何实现大整数相加
数组存储,数位相加(类似于列竖式)
java
/**
* 大整数求和
* @param bigNumberA 大整数A
* @param bigNumberB 大整数B
* */
public static String bigNumberSum(String bigNumberA,String bigNumberB){
//1. 把两个大整数用数组逆序存储,数组长度等于较大整数位数+1
int maxLength=bigNumberA.length()>bigNumberB.length()?bigNumberA.length():bigNumberB.length();
int[] arrayA=new int[maxLength+1];
for(int i=0;i<bigNumberA.length();i++){
arrayA[i]=bigNumberA.charAt(bigNumberA.length()-1-i)-'0';
}
int[] arrayB=new int[maxLength+1];
for(int i=0;i<bigNumberB.length();i++){
arrayB[i]=bigNumberB.charAt(bigNumberB.length()-1-i)-'0';
}
//2. 构建result数组,数组长度等于较大整数位数+1
int[] result=new int[maxLength+1];
//3. 遍历数组,按位相加
for(int i=0;i<result.length;i++){
int temp=result[i];
temp+=arrayA[i];
temp+=arrayB[i];
//判断是否进位
if(temp>=10){
temp=temp-10;
result[i+1]=1;
}
result[i]=temp;
}
//4. 把result数组再次逆序并转成String
StringBuilder sb=new StringBuilder();
//是否找到大整数的最高有效位
boolean findFirst=false;
for(int i=result.length-1;i>=0;i--){
if(!findFirst){
if(result[i]==0){
continue;
}
findFirst=true;
}
sb.append(result[i]);
}
return sb.toString();
}
public static void main(String[] args){
System.out.println(bigNumberSum("426709752318","95481253129"));
}
时间复杂度: O ( n ) O(n) O(n)
优化:不需要把原整数拆分得这么细,只需要拆分到可以被直接计算的地步就可以了。
int最多可以有10位整数,为防溢出,可以把大整数的每9位作为一个元素,进行加法运算。
10. 如何求解金矿问题
题目:有位国王拥有5座金矿,每座金矿的黄金储量不同,需要参与挖掘的工人人数也不同。例如有的金矿储量是500kg黄金,需要5个工人来挖掘;有的金矿储量是200kg黄金,需要3个工人来挖掘......
如果参与挖矿的工人的总数是10。每座金矿要么全挖,要么不挖,不能派出一半人挖取一半的金矿。要求用程序求出,要想得到尽可能多的黄金,应该选择挖取哪几座金矿?
标签:动态规划 背包问题
最优子结构
边界
状态转移方程式
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZbTIVYdJ-1595582632843)(漫画算法6.png)]
10.1 优化前的代码
java
/**
* 获得金矿最优收益
* @param w 工人数量
* @param n 可选金矿数量
* @param p 金矿开采所需的工人数量
* @param g 金矿储量
* */
public static int getBestGoldMining(int w,int n,int[] p,int[] g){
if(w==0||n==0){
return 0;
}
if(w<p[n-1]){
return getBestGoldMining(w,n-1,p,g);
}
return Math.max(getBestGoldMining(w,n-1,p,g), getBestGoldMining(w-p[n-1],n-1,p,g)+g[n-1]);
}
public static void main(String[] args){
int w=10;
int[] p={5,5,3,4,3};
int[] g={400,500,200,300,350};
System.out.println("最优收益:"+getBestGoldMining(w,g.length,p,g));
}
时间复杂度: O ( 2 n ) O(2^n) O(2n)
问题:递归做了很多重复计算
10.2 优化step1
自底向上求解
以金矿为行,工人人数为列,逐一求解表格
java
/**
* 获得金矿最优收益
* @param w 工人数量
* @param p 金矿开采所需的工人数量
* @param g 金矿储量
* */
public static int getBestGoldMiningV2(int w,int[] p,int[] g){
//创建表格
int[][] resultTable=new int[g.length+1][w+1];
//填充表格
for(int i=1;i<=g.length;i++){
for(int j=1;j<=w;j++){
if(j<p[i-1]){
resultTable[i][j]=resultTable[i-1][j];
}else{
resultTable[i][j]=Math.max(resultTable[i-1][j], resultTable[i-1][j-p[i-1]]+g[i-1]);
}
}
}
//返回最后一个格子的值
return resultTable[g.length][w];
}
时间复杂度: O ( n w ) O(nw) O(nw)
10.3 优化step2
在程序中并不需要保存整个表格,无论金矿有多少座,我们只保存1行的数据即可。在计算下一行时,要从右向左统计,把旧的数据一个一个替换掉。
java
/**
* 获得金矿最优收益
* @param w 工人数量
* @param p 金矿开采所需的工人数量
* @param g 金矿储量
* */
public static int getBestGoldMiningV3(int w,int[] p,int[] g){
//创建当前结果
int[] results=new int[w+1];
//填充一维数组
for(int i=1;i<=g.length;i++){
for(int j=w;j>=1;j--){
if(j>=p[i-1]){
results[j]=Math.max(results[j], results[j-p[i-1]]+g[i-1]);
}
}
}
//返回最后1个格子的值
return results[w];
}
空间复杂度: O ( n ) O(n) O(n)
11. 寻找缺失的整数
问题:在1个无序数组里有99个不重复的正整数,范围是1 ~ 100,唯独缺少1个1~100中的整数。如何找出这个缺失的整数?
11.1 解法1
创建一个以1~100为Key的哈希表,遍历数组删除元素对应的Key,最后剩下的Key就是缺失的整数。
时间复杂度: O ( n ) O(n) O(n)
空间复杂度: O ( n ) O(n) O(n)
11.2 解法2
先把数组元素排序,然后遍历,如果发现两个相邻元素并不连续,说明缺少的就是这两个元素之间的整数
时间复杂度: O ( n log n ) O(n\log{n}) O(nlogn)
空间复杂度: O ( 1 ) O(1) O(1)
11.3 解法3
先算出1~100的累加和,然后再依次减去数组里的所有元素,最后的差值就是所缺少的整数
时间复杂度: O ( n ) O(n) O(n)
空间复杂度: O ( 1 ) O(1) O(1)
11.4 问题扩展step1
题目:一个无序数组里有若干个正整数,范围是1~100,其中99个整数都出现了偶数次,只有1个整数出现了奇数次,如何找到这个出现奇数次的整数?
异或运算 同0异1
把数组里所有元素依次进行异或运算,最后得到的就是那个整数
时间复杂度: O ( n ) O(n) O(n)
空间复杂度: O ( 1 ) O(1) O(1)
11.5 问题扩展step2
问题:......如果数组里有2个整数出现了奇数次,其他整数出现偶数次,如何找出这两个整数呢?
分治法
如果把数组分成两部分,保证每一部分都包含1个出现奇数次的整数,这样就与上一题的情况一样了
首先把数组元素依次进行异或运算,最后结果中肯定至少有1个二进制位是1,就用这个位置把数组分成两份,就能把这两个要找的整数分开
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uOYZlpjp-1595582632846)(漫画算法7.png)]
时间复杂度: O ( n ) O(n) O(n)
空间复杂度: O ( 1 ) O(1) O(1)
java
public static int[] findLostNum(int[] array){
//用于存储2个出现奇数次的整数
int result[]=new int[2];
//第1次进行整体异或运算
int xorResult=0;
for(int i=0;i<array.length;i++){
xorResult^=array[i];
}
//如果进行异或运算的结果为0,则说明输入的数组不符合题目要求
if(xorResult==0){
return null;
}
//确定2个整数的不同位,以此来做分组
int separator=1;
while(0==(xorResult&separator)){
separator<<=1;
}
//第2次分组进行异或运算
for(int i=0;i<array.length;i++){
if(0==(array[i]&separator)){
result[0]^=array[i];
}else{
result[1]^=array[i];
}
}
return result;
}
public static void main(String[] args){
int[] array={4,1,2,2,5,1,4,3};
int[] result=findLostNum(array);
System.out.println(result[0]+", "+result[1]);
}
Chapter6:算法的业务应用
1. Bitmap算法/位图算法
用户标签→关系型数据库
Bitmap(一块长度为10bit的内存空间)算法:用于对大量整数做去重和查询操作
用标签对应多个用户,将其转为Bitmap做交集/并集
算法优势:高性能的位运算
反向匹配:全量的Bitmap(异或)
java
static class MyBitmap{
//每一个word是一个long类型元素,对应一个64位二进制数据
private long[] words;
//Bitmap的位数大小
private int size;
public MyBitmap(int size){
this.size=size;
this.words=new long[(getWordIndex(size-1)+1)];
}
/**
* 判断Bitmap某一位的状态
* @param bitIndex 位图的第bitIndex位
* */
public boolean getBit(int bitIndex){
if(bitIndex<0||bitIndex>size-1){
throw new IndexOutOfBoundsException("超过Bitmap有效范围");
}
int wordIndex=getWordIndex(bitIndex);
return (words[wordIndex]&(1L<<bitIndex))!=0;
}
/**
* 把Bitmap某一位设置为true
* @param bitIndex 位图的第bitIndex位
* */
public void setBit(int bitIndex){
if(bitIndex<0||bitIndex>size-1){
throw new IndexOutOfBoundsException("超过Bitmap有效范围");
}
int wordIndex=getWordIndex(bitIndex);
words[wordIndex]|=(1L<<bitIndex);
}
/**
* 定位Bitmap某一位所对应的word
* @param bitIndex 位图的第bitIndex位
* */
private int getWordIndex(int bitIndex){
//右移6位,相当于除以64
return bitIndex>>6;
}
}
public static void main(String[] args){
MyBitmap bitMap=new MyBitmap(128);
bitMap.setBit(126);
bitMap.setBit(75);
System.out.println(bitMap.getBit(126));
System.out.println(bitMap.getBit(78));
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uP2R2Xxj-1595582632849)(漫画算法8.png)]
2. LRU算法
用哈希表做缓存,哈希表过大
Least Recently Used
内存管理算法
假设:长期不被使用的数据,在未来被用到的几率也不大。因此,当数据所占内存达到一定阈值时,我们要移除掉最近最少被使用的数据。
数据结构------哈希链表
依靠哈希链表的有序性,可以把Key-Value按照最后的使用时间进行排序。
大致来说就是把最近访问的用户添加/移动到最后面
Java里封装的轮子:LinkedHashMap
java
static class LRUCache{
private Node head;
private Node end;
//缓存存储上限
private int limit;
private HashMap<String,Node> hashMap;
public LRUCache(int limit){
this.limit=limit;
hashMap=new HashMap<String,Node>();
}
public String get(String key){
Node node=hashMap.get(key);
if(node==null){
return null;
}
refreshNode(node);
return node.value;
}
public void put(String key,String value){
Node node=hashMap.get(key);
if(node==null){
//如果Key不存在,则插入Key-Value
if(hashMap.size()>=limit){
String oldKey=removeNode(head);
hashMap.remove(oldKey);
}
node=new Node(key,value);
addNode(node);
hashMap.put(key, node);
}else{
//如果Key存在,则刷新Key-Value
node.value=value;
refreshNode(node);
}
}
public void remove(String key){
Node node=hashMap.get(key);
removeNode(node);
hashMap.remove(key);
}
/**
* 刷新被访问的节点位置
* @param node 被访问的节点
* */
private void refreshNode(Node node){
//如果访问的是尾节点,则无需移动节点
if(node==end){
return;
}
//移除节点
removeNode(node);
//重新插入节点
addNode(node);
}
/**
* 删除节点
* @param node 要删除的节点
* */
private String removeNode(Node node){
if(node==head&&node==end){
//移除唯一的节点
head=null;
end=null;
}else if(node==end){
//移除尾节点
end=end.pre;
end.next=null;
}else if(node==head){
//移除头节点
head=head.next;
head.pre=null;
}else{
//移除中间节点
node.pre.next=node.next;
node.next.pre=node.pre;
}
return node.key;
}
/**
* 尾部插入节点
* @param node 要插入的节点
* */
private void addNode(Node node){
if(end!=null){
end.next=node;
node.pre=end;
node.next=null;
}
end=node;
if(head==null){
head=node;
}
}
class Node{
Node(String key,String value){
this.key=key;
this.value=value;
}
public Node pre;
public Node next;
public String key;
public String value;
}
}
public static void main(String[] args){
LRUCache lruCache=new LRUCache(5);
lruCache.put("001", "用户1信息");
lruCache.put("002", "用户2信息");
lruCache.put("003", "用户3信息");
lruCache.put("004", "用户4信息");
lruCache.put("005", "用户5信息");
lruCache.get("002");
lruCache.put("004", "用户4信息更新");
lruCache.put("006", "用户6信息");
System.out.println(lruCache.get("001"));
System.out.println(lruCache.get("006"));
}
注意:这段代码不是线程安全的代码
对于用户系统的需求,也可以使用缓存数据库Redis来实现,Redis底层也实现了类似LRU的回收算法。
3. A星寻路算法
A*search algorithm
从起点绕过障碍物走到终点
OpenList:可到达的格子
CloseList:已到达的格子
F=G+H
G:从起点走到当前格子的成本,也就是已经花费了多少步。
H:在不考虑障碍的情况下,从当前格子走到目标格子的距离,也就是离目标还有多远。
F:G和H的综合评估,也就是从起点到达当前格子,再从当前格子到达目标格子的总步数。
将起点放入OpenList→
(找出OpenList中F值最小的值作为当前格子,将当前格子移入CloseList→将当前格子上下左右可到达的格子,看它们是否在OpenList或CloseList中;如果不在,则将它们加入OpenList,计算出相应的G、H、F值,并把当前格子作为它们的"父节点"。)循环
到达终点后顺着父节点依次回溯,找到一条最佳路径
启发式搜索
java
//迷宫地图
public static final int[][] MAZE={
{0,0,0,0,0,0,0},
{0,0,0,1,0,0,0},
{0,0,0,1,0,0,0},
{0,0,0,1,0,0,0},
{0,0,0,0,0,0,0}
};
/**
* A*寻路主逻辑
* @param start 迷宫起点
* @param end 迷宫终点
* */
public static Grid aStarSearch(Grid start,Grid end){
ArrayList<Grid> openList=new ArrayList<Grid>();
ArrayList<Grid> closeList=new ArrayList<Grid>();
//把起点加入openList
openList.add(start);
//主循环,每一轮检查1个当前方格节点
while(openList.size()>0){
//在openList中查找F值最小的节点,将其作为当前方格节点
Grid currentGrid=findMinGird(openList);
//将当前方格节点从openList中移除
openList.remove(currentGrid);
//当前方格节点进入closeList
closeList.add(currentGrid);
//找到所有邻近节点
List<Grid> neighbors=findNeighbors(currentGrid,openList,closeList);
for(Grid grid:neighbors){
if(!openList.contains(grid)){
//邻近节点不在openList中,标记"父节点"、G、H、F,并放入openList
grid.initGird(currentGrid,end);
openList.add(grid);
}
}
//如果终点在openList中,直接返回终点格子
for(Grid grid:openList){
if((grid.x==end.x)&&(grid.y==end.y)){
return grid;
}
}
}
//openList用尽,仍然找不到终点,说明终点不可到达,返回空
return null;
}
private static Grid findMinGird(ArrayList<Grid> openList){
Grid tempGrid=openList.get(0);
for(Grid grid:openList){
if(grid.f<tempGrid.f){
tempGrid=grid;
}
}
return tempGrid;
}
private static ArrayList<Grid> findNeighbors(Grid grid,List<Grid> openList,List<Grid> closeList){
ArrayList<Grid> gridList=new ArrayList<Grid>();
if(isValidGrid(grid.x,grid.y-1,openList,closeList)){
gridList.add(new Grid(grid.x,grid.y-1));
}
if(isValidGrid(grid.x,grid.y+1,openList,closeList)){
gridList.add(new Grid(grid.x,grid.y+1));
}
if(isValidGrid(grid.x-1,grid.y,openList,closeList)){
gridList.add(new Grid(grid.x-1,grid.y));
}
if(isValidGrid(grid.x+1,grid.y,openList,closeList)){
gridList.add(new Grid(grid.x+1,grid.y));
}
return gridList;
}
private static boolean isValidGrid(int x,int y,List<Grid> openList,List<Grid> closeList){
//是否超过边界
if(x<0||x>=MAZE.length||y<0||y>=MAZE[0].length){
return false;
}
//是否有障碍物
if(MAZE[x][y]==1){
return false;
}
//是否已经在openList中
if(containGrid(openList,x,y)){
return false;
}
//是否已经在closeList中
if(containGrid(closeList,x,y)){
return false;
}
return true;
}
private static boolean containGrid(List<Grid> grids,int x,int y){
for(Grid n:grids){
if((n.x==x)&&(n.y==y)){
return true;
}
}
return false;
}
static class Grid{
public int x;
public int y;
public int f;
public int g;
public int h;
public Grid parent;
public Grid(int x,int y){
this.x=x;
this.y=y;
}
public void initGird(Grid parent,Grid end){
this.parent=parent;
if(parent!=null){
this.g=parent.g+1;
}else{
this.g=1;
}
this.h=Math.abs(this.x-end.x)+Math.abs(this.y-end.y);
this.f=this.g+this.h;
}
}
public static void main(String[] args){
//设置起点和终点
Grid startGrid=new Grid(2,1);
Grid endGrid=new Grid(2,5);
//搜索迷宫终点
Grid resultGrid=aStarSearch(startGrid,endGrid);
//回溯迷宫路径
ArrayList<Grid> path=new ArrayList<Grid>();
while(resultGrid!=null){
path.add(new Grid(resultGrid.x,resultGrid.y));
resultGrid=resultGrid.parent;
}
//输出迷宫和路径,路径用*表示
for(int i=0;i<MAZE.length;i++){
for(int j=0;j<MAZE[0].length;j++){
if(containGrid(path,i,j)){
System.out.print("*,");
}else{
System.out.print(MAZE[i][j]+",");
}
}
System.out.println();
}
}
4. 红包算法
- 红包金额尽可能分布均衡
- 为了避免高并发引起的一些问题:先计算好每个红包拆除的金额,并把它们放到一个队列里,领取红包的用户要在队列中找到属于自己的那一份。
- 公平
方法1:二倍均值法
把每次随机金额的上限定为剩余人均金额的两倍
假设剩余红包金额为m元,剩余人数为n:
每次抢到的金额=随机区间[0.01, m/n×2-0.01]元
保证了每次随机金额的均值是相等的
java
/**
* 拆分红包
* @param totalAmount 总金额(以分为单位)
* @param totalPeopleNum 总人数
* */
public static List<Integer> divideRedPackage(Integer totalAmount,Integer totalPeopleNum){
List<Integer> amountList=new ArrayList<Integer>();
Integer restAmount=totalAmount;
Integer restPeopleNum=totalPeopleNum;
Random random=new Random();
for(int i=0;i<totalPeopleNum-1;i++){
//随机范围:[1,剩余人均金额的2倍-1]分
int amount=random.nextInt(restAmount/restPeopleNum*2-1)+1;
restAmount-=amount;
restPeopleNum--;
amountList.add(amount);
}
amountList.add(restAmount);
return amountList;
}
public static void main(String[] args){
List<Integer> amountList=divideRedPackage(1000,10);
for(Integer amount:amountList){
System.out.println("抢到金额:"+new BigDecimal(amount).divide(new BigDecimal(100)));
}
}
方法2:线段切割法
rid(2,1);
Grid endGrid=new Grid(2,5);
//搜索迷宫终点
Grid resultGrid=aStarSearch(startGrid,endGrid);
//回溯迷宫路径
ArrayList path=new ArrayList();
while(resultGrid!=null){
path.add(new Grid(resultGrid.x,resultGrid.y));
resultGrid=resultGrid.parent;
}
//输出迷宫和路径,路径用表示
for(int i=0;i<MAZE.length;i++){
for(int j=0;j<MAZE[0].length;j++){
if(containGrid(path,i,j)){
System.out.print(" ,");
}else{
System.out.print(MAZE[i][j]+",");
}
}
System.out.println();
}
}
## 4. 红包算法
1. 红包金额尽可能分布均衡
2. 为了避免高并发引起的一些问题:先计算好每个红包拆除的金额,并把它们放到一个队列里,领取红包的用户要在队列中找到属于自己的那一份。
3. 公平
### 方法1:二倍均值法
把每次随机金额的上限定为剩余人均金额的两倍
假设剩余红包金额为m元,剩余人数为n:
每次抢到的金额=随机区间[0.01, m/n×2-0.01]元
保证了每次随机金额的均值是相等的
```java
/**
* 拆分红包
* @param totalAmount 总金额(以分为单位)
* @param totalPeopleNum 总人数
* */
public static List<Integer> divideRedPackage(Integer totalAmount,Integer totalPeopleNum){
List<Integer> amountList=new ArrayList<Integer>();
Integer restAmount=totalAmount;
Integer restPeopleNum=totalPeopleNum;
Random random=new Random();
for(int i=0;i<totalPeopleNum-1;i++){
//随机范围:[1,剩余人均金额的2倍-1]分
int amount=random.nextInt(restAmount/restPeopleNum*2-1)+1;
restAmount-=amount;
restPeopleNum--;
amountList.add(amount);
}
amountList.add(restAmount);
return amountList;
}
public static void main(String[] args){
List<Integer> amountList=divideRedPackage(1000,10);
for(Integer amount:amountList){
System.out.println("抢到金额:"+new BigDecimal(amount).divide(new BigDecimal(100)));
}
}
方法2:线段切割法
将红包总金额想象成长线,每个人抢到的金额是拆分出的子线段。每段长度由"切割点"决定。