漫画算法做题笔记

诸神缄默不语-个人CSDN博文目录

哦这是我三年前写的,我现在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:算法与数据结构常识

  1. 线性结构:数组、链表  栈、队列、哈希表
    树:二叉树  二叉堆
  2. 时间复杂度
    • 基本操作执行次数 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为算法的渐进时间复杂度 ,简称为时间复杂度
  3. 空间复杂度   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:线性数据结构

  1. 数组array  顺序存储  顺序表
    根据下标读取元素的方式:随机读取
    读取和更新的时间复杂度: O ( 1 ) O(1) O(1)
    插入/删除: O ( n ) O(n) O(n)
    读操作多,写操作少
  2. 链表  随机存储
    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自动化垃圾回收机制)
  1. 物理结构和逻辑结构

  2. 栈FILO

    • 最早进入的元素存放的位置:栈底bottom
    • 最后进入的元素存放的位置:栈顶top
    • 入栈push
    • 出栈pop
    • O ( 1 ) O(1) O(1)
    • 用于:对历史的回溯------递归、面包屑导航
  3. 队列FIFO

    • 队列的出口端:队头front
    • 队列的入口端:队尾rear
    • 入队enque
    • 出队deque(循环队列)
    • O ( 1 ) O(1) O(1)
    • 用于:对历史的回放------多线程中争夺公平锁的等待队列、爬虫实现网络抓取
  4. 双端队列deque

  5. 优先队列  谁的优先级最高,谁先出队

  6. 散列表/哈希表  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:

扩容 重新哈希

Chapter3:树

  1. 树tree是n(n≥0)个节点的有限集。当n=0时,称为空树。在任意一个非空树中,有如下特点:
    • 有且仅有一个特定的称为根的节点。
    • 当n>1时,其余节点可分为m(m>0)个互不相交的有限集,每一个集合的本身又是一个树,并称为根的子树。
java 复制代码
根节点root
叶子节点leaf
子树
层级
父节点parent
孩子节点child
兄弟节点sibling
最大层级数:高度或深度
  1. 二叉树:每个节点最多有两个孩子节点
java 复制代码
左孩子left child
右孩子right child
  • 满二叉树:所有非叶子节点都存在左右孩子,并且所有叶子节点都在同一层级上
  • 完全二叉树:对一个有n个节点的二叉树,按层级顺序编号,则所有节点的编号为从1到n。如果这个树所有节点和同样深度的满二叉树的编号为1到n的节点位置相同,则这个二叉树为完全二叉树
    完全二叉树是满二叉树缺最后几个叶子节点
  1. 二叉树的存储方式
    3.1 链式存储结构
    3.2 数组
    parent  左孩子2parent+1  右孩子2parent+2
    leftChild  父节点(leftChild-1)/2
    应用:二叉堆

  2. 二叉查找树 (排序)
    (1)如果左子树不为空,则左子树上所有节点的值均小于根节点的值
    (2)如果右子树不为空,则右子树上所有节点的值均大于根节点的值
    (3)左、右子树也都是二叉查找树
    节点分布相对均衡的二叉查找树:搜索时间复杂度 O ( n ) O(n) O(n)
    自平衡

  3. 二叉查找树的深度优先遍历  栈

    • 前序遍历:根节点、左子树、右子树
    • 中序遍历:左子树、根节点、右子树
    • 后序遍历:左子树、右子树、根节点
  4. 二叉查找树的广度优先遍历  队列

  5. 二叉堆

    • 本质:完全二叉树
    • 分类
      (1)最大堆:任何一个父节点的值都大于等于左、右子节点值
      (2)最小堆:任何一个父节点的值都小于等于左、右子节点值
    • 根节点:堆顶
    • 数组存储
  6. 二叉堆的自我调整

    • 插入 O ( log ⁡ n ) O(\log{n}) O(logn):最后一个位置→上浮
    • 删除 O ( log ⁡ n ) O(\log{n}) O(logn):(删的是堆顶)最后一个节点临时补到堆顶→下沉
    • 构建二叉堆 O ( n ) O(n) O(n):把无序的完全二叉树调整为二叉堆,本质就是让所有非叶子节点依次"下沉"(从最后一个非叶子节点开始)
  7. 优先队列

    • 最大优先队列:当前最大的元素优先出队  最大堆
    • 最小优先队列:当前最小的元素优先出队  最小堆

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. 冒泡排序

  1. 两两交换,最大到最小
  2. 稳定排序
  3. 优化
    • 有序标记:isSorted
    • 数列有序区(记录最后一次元素交换的位置)
  4. 鸡尾酒排序:双向
    (大部分元素已经有序的情况)

2. 快速排序

(交换排序)  平均空间复杂度是 O ( log ⁡ n ) O(\log{n}) O(logn)

  1. 基准元素的选择
  2. 元素的交换
    • 双边循环法:左右两个指针(同时比成,则交换)一边与基准元素比一边相靠,最后与基准元素交换
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. 桶排序

  1. 每一个桶(bucket)代表一个区间范围,里面可以承载一个或多个元素。
  2. 步骤
    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 方法四:移位运算

  1. 当a和b均为偶数时,gcd(a,b)=2×gcd(a/2,b/2)=2×gcd(a>>1,b>>1)。
  2. 当a为奇数,b为偶数时,gcd(a,b)=gcd(a,b/2)=gcd(a,b>>1)。
  3. 当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 方法二:利用计数排序的思想

  1. 利用计数排序的思想,先求出原数组的最大值max与最小值min的区间长度k(k=max-min+1),以及偏移量d=min。
  2. 创建一个长度为k的新数组Array。
  3. 遍历原数组,每遍历一个元素,就把新数组Array对应下标的值+1。遍历结束后,Array的一部分元素值变成了1或更高的数值,一部分元素值仍然是0。
  4. 遍历新数组Array,统计出Array中最大连续出现0值的次数+1,即为相邻元素最大差值。

问题:数组元素可能差值悬殊

5.3 方法三:利用桶排序的思想

  1. 利用桶排序的思想,根据原数组的长度n,创建出n个桶,每一个桶代表一个区间范围。其中第一个桶从原数组的最小值min开始,区间跨度是(max-min)/(n-1)。
  2. 遍历原数组,把原数组每一个元素插入到对应的桶中,记录每一个桶的最大和最小值。
  3. 遍历所有的桶,统计出每一个桶的最大值,和这个桶右侧非空桶的最小值的差,数值最大的差即为原数组排序后的相邻最大差值。

时间复杂度: 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. 寻找全排列的下一个数

题目:给出一个正整数,找出这个正整数所有数字全排列的下一个数。

方法:

  1. 从后向前查看逆序区域,找到逆序区域的前一位,也就是数字置换的边界。
  2. 让逆序区域的前一位和逆序区域中大于它的最小的数字交换位置。
  3. 把原来的逆序区域转为顺序状态。
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)

缺点:

  1. 每次内层循环都需要从头开始遍历所有数字(应该停留在上一次删除的位置继续)
  2. 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. 红包金额尽可能分布均衡
  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:线段切割法

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:线段切割法

将红包总金额想象成长线,每个人抢到的金额是拆分出的子线段。每段长度由"切割点"决定。

相关推荐
游是水里的游31 分钟前
【算法day19】回溯:分割与子集问题
算法
不想当程序猿_31 分钟前
【蓝桥杯每日一题】分糖果——DFS
c++·算法·蓝桥杯·深度优先
南城花随雪。1 小时前
单片机:实现FFT快速傅里叶变换算法(附带源码)
单片机·嵌入式硬件·算法
dundunmm1 小时前
机器学习之scikit-learn(简称 sklearn)
python·算法·机器学习·scikit-learn·sklearn·分类算法
古希腊掌管学习的神1 小时前
[机器学习]sklearn入门指南(1)
人工智能·python·算法·机器学习·sklearn
波音彬要多做1 小时前
41 stack类与queue类
开发语言·数据结构·c++·学习·算法
m0_748256782 小时前
WebGIS实战开源项目:智慧机场三维可视化(学习笔记)
笔记·学习·开源
红色的山茶花2 小时前
YOLOv9-0.1部分代码阅读笔记-loss.py
笔记
程序员老冯头3 小时前
第十五章 C++ 数组
开发语言·c++·算法
胡西风_foxww4 小时前
【es6复习笔记】Promise对象详解(12)
javascript·笔记·es6·promise·异步·回调·地狱