诸神缄默不语-个人CSDN博文目录
哦这是我三年前写的,我现在Java语法都快忘光了……
反正之前的博文也发一下好了。这个因为我当年是用有道云笔记而不是直接用CSDN编辑器写的,所以后面有些内容写乱了,因为我现在猛的一看有点看不懂,所以……等我过段时间看懂了再回来把排版重新改一下,酱。
文章目录
- Chapter1:算法与数据结构常识
- Chapter2:线性数据结构
- Chapter3:树
- Chapter4:排序算法
- 1. 冒泡排序
- 2. 快速排序
- 3. 堆排序
- 4. 计数排序
- 4.1 原始版与优化step1
- 4.2 优化step2
- 5. 桶排序
- Chapter5:面试中的算法
- 1. 如何判断链表有环
- 1.1 方法一:暴力遍历
- 1.2 方法二:哈希表
- 1.3 方法三:两个指针
- 1.4 扩展问题:环的长度
- 1.5 扩展问题:出入环节点
- 2. 最小栈
- 2.1 有问题的方法:把栈中的最小元素下标暂存起来
- 2.2 方法:存储栈中曾经的最小值
- 3. 最大公约数
- 3.1 方法一:暴力枚举法
- 3.2 方法二:辗转相除法/欧几里得算法
- 3.3 方法三:辗转相减法/更相减损法
- 3.4 方法四:移位运算
- 4. 2的整数次幂
- 4.1 方法一:暴力枚举法
- 4.2 方法二
- 5. 无序数组排序后的最大相邻差
- 5.1 方法一:排序后求解
- 5.2 方法二:利用计数排序的思想
- 5.3 方法三:利用桶排序的思想
- 6. 用栈实现队列
- 7. 寻找全排列的下一个数
- 8. 删去k个数字后的最小值
- 8.1 性能不好的代码
- 8.2 优化后的代码
- 9. 如何实现大整数相加
- 10. 如何求解金矿问题
- 10.1 优化前的代码
- 10.2 优化step1
- 10.3 优化step2
- 11. 寻找缺失的整数
- 11.1 解法1
- 11.2 解法2
- 11.3 解法3
- 11.4 问题扩展step1
- 11.5 问题扩展step2
- Chapter6:算法的业务应用
- 1. Bitmap算法/位图算法
- 2. LRU算法
- 3. A星寻路算法
- 4. 红包算法
- 方法1:二倍均值法
- 方法2:线段切割法
- 方法2:线段切割法
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)个互不相交的有限集,每一个集合的本身又是一个树,并称为根的子树。
根节点root
叶子节点leaf
子树
层级
父节点parent
孩子节点child
兄弟节点sibling
最大层级数:高度或深度
- 二叉树:每个节点最多有两个孩子节点
左孩子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)
- 基准元素的选择
- 元素的交换
- 双边循环法:左右两个指针(同时比成,则交换)一边与基准元素比一边相靠,最后与基准元素交换
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位置
//仅有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 不用递归的方法
//仅有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 循环删除堆顶元素,替换到二叉堆的末尾,调整堆产生新的堆顶
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)遍历数组,输出数组元素的下标值(元素值为输出次数)
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)
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:遍历桶,输出所有元素
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)
/** 判断是否有环* @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)
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 方法一:暴力枚举法
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之间的最大公约数。
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之间的最大公约数。
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必将为偶数,然后又可以继续进行移位运算
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 方法一:暴力枚举法
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
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)
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相等,说明数组所有元素都相等,返回0if(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. 用栈实现队列
两个栈,来来回回倒
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. 寻找全排列的下一个数
题目:给出一个正整数,找出这个正整数所有数字全排列的下一个数。
方法:
- 从后向前查看逆序区域,找到逆序区域的前一位,也就是数字置换的边界。
- 让逆序区域的前一位和逆序区域中大于它的最小的数字交换位置。
- 把原来的逆序区域转为顺序状态。
//为方便数字的交换,入参和返回值的类型都采用了整型数组
public static int[] findNearestNumber(int[] numbers){//1. 从后向前查看逆序区域,找到逆序区域的前一位,也就是数字置换的边界int index=findTransferPoint(numbers);//如果数字置换边界是0,说明整个数组已经逆序,无法得到更大的相同数字组成的整数,返回nullif(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 性能不好的代码
/*** 删除整数的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);}//清除整数左侧的数字0numNew=removeZero(numNew);}//如果整数的所有数字都被删除了,直接返回0if(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 优化后的代码
/*** 删除整数的k个数字,获得删除后的最小值* @param num 原整数* @param k 删除数量* */
public static String removeKDigits(String num,int k){//新整数的最终长度=原整数长度-kint 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. 如何实现大整数相加
数组存储,数位相加(类似于列竖式)
/*** 大整数求和* @param bigNumberA 大整数A* @param bigNumberB 大整数B* */
public static String bigNumberSum(String bigNumberA,String bigNumberB){//1. 把两个大整数用数组逆序存储,数组长度等于较大整数位数+1int 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数组,数组长度等于较大整数位数+1int[] 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数组再次逆序并转成StringStringBuilder 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 优化前的代码
/*** 获得金矿最优收益* @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
自底向上求解
以金矿为行,工人人数为列,逐一求解表格
/*** 获得金矿最优收益* @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行的数据即可。在计算下一行时,要从右向左统计,把旧的数据一个一个替换掉。
/*** 获得金矿最优收益* @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)
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(异或)
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位,相当于除以64return 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
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-Valueif(hashMap.size()>=limit){String oldKey=removeNode(head);hashMap.remove(oldKey);}node=new Node(key,value);addNode(node);hashMap.put(key, node);}else{//如果Key存在,则刷新Key-Valuenode.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值,并把当前格子作为它们的“父节点”。)循环
到达终点后顺着父节点依次回溯,找到一条最佳路径
启发式搜索
//迷宫地图
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>();//把起点加入openListopenList.add(start);//主循环,每一轮检查1个当前方格节点while(openList.size()>0){//在openList中查找F值最小的节点,将其作为当前方格节点Grid currentGrid=findMinGird(openList);//将当前方格节点从openList中移除openList.remove(currentGrid);//当前方格节点进入closeListcloseList.add(currentGrid);//找到所有邻近节点List<Grid> neighbors=findNeighbors(currentGrid,openList,closeList);for(Grid grid:neighbors){if(!openList.contains(grid)){//邻近节点不在openList中,标记“父节点”、G、H、F,并放入openListgrid.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]元
保证了每次随机金额的均值是相等的
/*** 拆分红包* @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:线段切割法
将红包总金额想象成长线,每个人抢到的金额是拆分出的子线段。每段长度由“切割点”决定。