• 算法<初级>


    算法<初级> - 第一章

    时间复杂度:

    • Big O
      时间/空间复杂度计算一样,都是跟输入数据源的大小有关

    • n->∞

    • O(logn)
      每次只使用数据源的一半,logn同理

    • 最优解
      先满足时间复杂度的情况最优情况下使用最小空间复杂度

    例题:子序列交换

    • 题目描述:
      输入一个序列,如1234567,在5和6位置处分成两个子序列,12345与67,将两个序列交换输出,如该输出为6712345。
    • 思路讲解:
    1. 首先将两个子序列逆序合并
    2. 逆序:左右两指针交换元素值直至中间
      逆序空间复杂度:O(1)
    3. 再将生成的新序列逆序,得到输出结果
    4. 时间复杂度:O(n)
    • 演示:
      • 子逆序合并:
        12345 67
        54321 76 —> 5432176
      • 整体逆序:
        6432175 —> 6732145 —> 6712345
      • 完成√

    排序-冒泡排序

    • 思想

      • 序列相邻两元素进行比较
    • 演示(升序)

      • 第一轮:
        确定最大值/最后一位:
        147523689 —> 147523689 —>
        147523689 —> 145723689 —>
        145273689 —> 145237689 —>
        145236789 —> 145236789 —>
        145236789
      • 第二轮:
        确定第二大值/倒数第二位:
        145236789 —> 145236789 —>
        145236789 —> 142536789 —>
        142356789 —> 142356789 —>
        145236789 —>
        145236789
      • 后面同理;
    • 时间复杂度O(n+n-1+n-2...1)=O(n2)

      • 无论好坏
    • 算法实现(Java)

    public static void bubbleSort(int[] arr) {  //冒泡排序
    	if (arr == null || arr.length < 2) {
    		return;
    	}
    	for (int e = arr.length - 1; e > 0; e--) {
    		for (int i = 0; i < e; i++) {
    			if (arr[i] > arr[i + 1]) {
    				swap(arr, i, i + 1);
    			}
    		}
    	}
    }
    
    public static void swap(int[] arr, int i, int j) {  //交换
    	arr[i] = arr[i] ^ arr[j];
    	arr[j] = arr[i] ^ arr[j];
    	arr[i] = arr[i] ^ arr[j];
    }
    

    对数器

    验证自我实现算法正确性的方式:与正确算法跑大量同样的测试用例比较结果

    • 随机数发射器
      用于生成大量随机的测试用例
    //随机数发射器
    public static int[] generateRandomArray(int maxSize, int maxValue) {
        //Math.random() -> double [0,1) 
        //(maxSize + 1) * Math.random() -> [0,size+1] double 
        //(int) ((maxSize + 1) * Math.random()) -> [0,size] 整数 
        //生成测试用例数组
        int[] arr = new int[(int) ((maxSize + 1) * Math.random())];
        for (int i = 0; i < arr.length; i++) {
        //随机生成数组中的数(任意方式)
    	    arr[i] = (int) ((maxValue + 1) * Math.random()) - (int) (maxValue * Math.random());
    	}
    	return arr;
    }
    
    • 用于快速验证贪心策略的正确性
    • 举例:用自我实现的冒泡排序与Java库排序函数进行比较:实现对数器
    public static void main(String[] args) {
    	int testTime = 500000;
    	int maxSize = 100;
    	int maxValue = 100;
    	boolean succeed = true;
    	for (int i = 0; i < testTime; i++) {
    		int[] arr1 = generateRandomArray(maxSize, maxValue);
    		int[] arr2 = copyArray(arr1);
    		bubbleSort(arr1);
    		comparator(arr2);
    		if (!isEqual(arr1, arr2)) {
    			succeed = false;
    			break;
    		}
    	}
    	System.out.println(succeed ? "Nice!" : "Fucking fucked!");
    
    	int[] arr = generateRandomArray(maxSize, maxValue);
    	printArray(arr);
    	bubbleSort(arr);
    	printArray(arr);
    }
    
    public static void comparator(int[] arr) {
    	Arrays.sort(arr);
    }
     
    public static int[] copyArray(int[] arr) {
    	if (arr == null) {
    		return null;
    	}
    	int[] res = new int[arr.length];
    	for (int i = 0; i < arr.length; i++) {
    		res[i] = arr[i];
    	}
    	return res;
    }
    
    // for test
    public static boolean isEqual(int[] arr1, int[] arr2) {
    	if ((arr1 == null && arr2 != null) || (arr1 != null && arr2 == null)) {
    		return false;
    	}
    	if (arr1 == null && arr2 == null) {
    		return true;
    	}
    	if (arr1.length != arr2.length) {
    		return false;
    	}
    	for (int i = 0; i < arr1.length; i++) {
    		if (arr1[i] != arr2[i]) {
    			return false;
    		}
    	}
    	return true;
    }
    
    // for test
    public static void printArray(int[] arr) {
    	if (arr == null) {
    		return;
    	}
    	for (int i = 0; i < arr.length; i++) {
    		System.out.print(arr[i] + " ");
    	}
    	System.out.println();
    }
    

    排序-选择排序

    • 思想
      • 序列各值与标记位置上的值进行比较
    • 时间复杂度:O(n2)
      • 无论好坏
    • 算法实现(Java)
    public static void selectionSort(int[] arr) {
    	if (arr == null || arr.length < 2) {
    		return;
    	}
    	for (int i = 0; i < arr.length - 1; i++) {
    		int minIndex = i;
    		for (int j = i + 1; j < arr.length; j++) {
    			minIndex = arr[j] < arr[minIndex] ? j : minIndex;
    		}
    		swap(arr, i, minIndex);
    	}
    }
    

    排序-插入排序

    • 思想
      • 从第二个(1索引)开始,跟自己之前所有比较,根据结果交换,直至停止,再继续循环
    • 时间复杂度:O(n2)
      • 最好情况:O(n)
    • 算法实现(Java)
    public static void insertionSort(int[] arr) {
    	if (arr == null || arr.length < 2) {
    		return;
    	}
    	for (int i = 1; i < arr.length; i++) {
    		for (int j = i - 1; j >= 0 && arr[j] > arr[j + 1]; j--) {
    			swap(arr, j, j + 1);
    		}
    	}
    }
    

    排序-归并排序

    • 思想
      • 递归
        计算机内栈实现-现场保存变量压栈
        先进后出-保证子递归值返回给父递归
        递归终止条件:base case
      • mergeSort():递归
        • 如果序列len1,或者左右,则返回 (base case)
        • 序列分成左右两个子序列,对两个子序列对其调用归并排序使之有序(递归)
          取中间数方法:(L+R)/2 (L+R)>>1 L+(R-L)/2 —>(防溢出)
      • merge():合并 (又称外排
        • 两个指针子序列头开始,左小放左,右小放右(针对有序序列的排序合并)
        • 当某边为空(越界)时,另一边剩下直接放
    • 演示
      • 74583291 —> 7458 3291 —>
        74 58 32 91 —> 7 4 5 8 3 2 9 1—>
        47 58 23 19 —> 4578 1239
      • 完成√
    • 时间复杂度:O(N* logN)
      T(n)=2T(n/2)+O(N) T(n)递归复杂度 O(N)除递归外复杂度
      • 空间复杂度:O(N)
        • 针对空间复杂度的优化 - 归并排序的内部缓存法 — 空间复杂度O(1)
        • 原地归并排序 — 空间复杂度O(1) 时间复杂度却是O(N2)
      • Master公式:计算递归类算法的时间复杂度公式
        • [补充阅读]
        • T(n)=aT(n/b)+O(Nd)
        • 若logba > d:
          时间复杂度:O(Nlogba)
        • 若logba < d:
          时间复杂度:O(Nd)
        • 若logba = d:
          时间复杂度:O(Nd* logN)
    • 非递归版本思想
      + 每相邻序列之间进行外排
    • 算法实现(Java)
    public static void mergeSort(int[] arr) {
    	if (arr == null || arr.length < 2) {
    		return;
    	}
    	mergeSort(arr, 0, arr.length - 1);
    }
    
    public static void mergeSort(int[] arr, int l, int r) {
    	if (l == r) {
    		return;
    	}
    	int mid = l + ((r - l) >> 1);
    	mergeSort(arr, l, mid);
    	mergeSort(arr, mid + 1, r);
    	merge(arr, l, mid, r);
    }
    
    public static void merge(int[] arr, int l, int m, int r) {
    	int[] help = new int[r - l + 1];
    	int i = 0;
    	int p1 = l;
    	int p2 = m + 1;
    	while (p1 <= m && p2 <= r) {
    		help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
    	}
    	while (p1 <= m) {
    		help[i++] = arr[p1++];
    	}
    	while (p2 <= r) {
    		help[i++] = arr[p2++];
    	}
    	for (i = 0; i < help.length; i++) {
    		arr[l + i] = help[i];
    	}
    }
    

    例题:小和问题

    • 题目表述

    对于某序列,将各个元素 左边比之小的数相加 ,求和各个元素的如此操作的值,称为小和,返回小和的值。

    • 暴力求解
      遍历-遍历
      时间复杂度:O(n2)
    • 归并排序求解
      • 归并代码
      • 小和产生在外排“左小放左,右小放右”的过程中(左右组间产生小和)
      • 当外排比较时,此时两边都是有序序列
        • 若左小,则右边产生小和,小和值为
          (左值 * 右边未放下序列的个数)
        • 若右小,则照旧进行
    res+=arr[p1] < arr[p2] ? (r-p2+1) * arr[p1] : 0 ;
    
    • 演示
      413506 —> 413 506 —>
      41 3 50 6 —> 4 1 3 5 0 6 —>
      41外排(不产生小和) 3外排 50外排(不产生小和) 6外排 —>
      14 3外排 05 6外排 —>

      • 14 3产生小和:1*3=3
      • 05 6产生小和:01+51=5

      134 056外排产生小和:1 * 2+3 * 2+5 * 2=18
      所以最终小和=3+5+18=26

    排序-快速排序

    经典快排(partition)

    • 取最后一个数做基准
    • 设一个 小于区 ,边界索引为-1 (大于区同理)
    • 开始遍历序列
      • 若元素小于(等于)基准,则将该元素与 边界索引+1 的位置交换,并将边界索引设置为 交换后该元素位置索引 —>等于是为了将最后位置的基准放到中间区
      • 若元素大于基准,则继续遍历下一个元素
    • 演示
      • 0351674 —> 0351674(边界-1)—>
      • 0351674(边界0)—> 0351674 (边界1) —>
      • 0351674 (边界1) —> 0351674 (边界2) —>
      • 0315674(边界2)—> 0315674 (边界2) —>
      • 0315674(边界3) —> 0314675
    • 算法实现(Java)
    public static int[] partition(int[] arr, int l, int r) {
    	int less = l - 1;
    	int more = r;
    	while (l < more) {
    		if (arr[l] < arr[r]) {
    			swap(arr, ++less, l++);
    		} else if (arr[l] > arr[r]) {
    			swap(arr, --more, l);
    		} else {
    			l++;
    		}
    	}
    	swap(arr, more, r);
    	return new int[] { less + 1, more };
    }
    

    例题:荷兰国旗问题

    • 题目表述
      给定一个整数数组,给定一个值K,这个值在原数组中一定存在,要求把数组中小于K的元素放到数组的左边,大于K的元素放到数组的右边,等于K的元素放到数组的中间,最终返回一个整数数组,其中只有两个值,分别是等于K的数组部分的左右两个下标值。
    • 实际上就是一个经典快排的改编(大于区+小于区)
      • 一开始基准归于大于区,大于区判定完后判定小于区,等于不进行操作继续遍历,遍历完后大于区边界与基准交换位置。

    随机快排

    • 思想
      • 随机选择序列中一个数,首先将其跟序列最后一个数进行交换,然后对该序列进行一次partition经典快排,返回 小于区边界与大于区边界索引
      • 之后对 (序列左边界 - 小于区边界-1)(大于区边界+1 - 序列右边界) 进行随机快排调用递归
      • 终止条件basecase:L>=R时,返回该序列
    • 时间复杂度
      • 概率长期期望O(NlogN):实际工程非常好,因为排序常数项很少
      • 最优O(NlogN)
      • 最差O(N2):每次随机基准选择都是序列最值
      • 空间复杂度 O(logN):记录每次的排序完后基准位置
    • 算法实现(Java)
    public static void quickSort(int[] arr) {
    	if (arr == null || arr.length < 2) {
    		return;
    	}
    	quickSort(arr, 0, arr.length - 1);
    }
    
    public static void quickSort(int[] arr, int l, int r) {
    	if (l < r) {
    		swap(arr, l + (int) (Math.random() * (r - l + 1)), r);
    		int[] p = partition(arr, l, r);
    		quickSort(arr, l, p[0] - 1);
    		quickSort(arr, p[1] + 1, r);
    	}
    }
    

    排序稳定性

    • 概念
      无序序列排成有序序列后,位置相对次序保持不变 —> 排序算法具有稳定性

    • 可以实现稳定性版本的排序算法
      冒泡排序 - 插入排序 - 归并排序

    • 不可以实现稳定性版本的排序算法
      选择排序 - 快速排序(01stable sort可以实现稳定性)

    面试坑题:按奇偶划分左右-次序不变

    • 题目表述
      一个序列,将序列中奇数放在左边,偶数放在右边,且保证奇偶数子序列次序不变,返回新序列,要求额外空间复杂度O(1)

    • 实际上是一个0-1博弈,对一个元素的判定非0即1

    • 大小 / 奇偶 都是0-1博弈的一种判定机制而已

    • 结合题目,实际上就是问实现 稳定性版本快排问题 —— 着重点就是次序不变

    • 要求 01stable sort论文级别算法 - 面试坑题

    排序-堆排序

    • 堆结构

      • 堆是一棵完全二叉树(数组可以对应完全二叉树/堆结构)
        • N结点,高度=logN
      • i结点:左孩子结点2i+1 / 右孩子结点2i+2 / 父亲结点(i-1)/2 (地板除)
    • 大根堆:堆每棵子树的头节点都是最大值(小根堆同理)

      • Java中优先队列默认是由小根堆构成,可以用比较器自定义优先级
    • 堆构造大根堆算法:heapinsert()过程

      • 经常用于解其他堆类题目
      1. 遍历每个数
      2. 与其父节点((i-1)/2)进行比较,如果大于父节点,则两个进行交换;交换后再与新父节点进行比较,直至停止(while)
      3. 遍历完后堆就变成了大根堆,小根堆构造同理
      4. 大根堆构造完后,形成上大下小的结构,但是无序
    for (int i = 0; i < arr.length; i++) {
    	heapInsert(arr, i);
    }
    
    public static void heapInsert(int[] arr, int index) {
    	while (arr[index] > arr[(index - 1) / 2]) {
    		swap(arr, index, (index - 1) / 2);
    		index = (index - 1) / 2;
    	}
    }
    
    
    • 大根堆有序化算法:heapify()过程
      1. 先将大根堆头节点跟最后一个结点交换(最大值固定,size-1)
      2. 遍历除最后结点外所有的节点
      3. 如果小于子节点最大值,则与最大值位置进行交换;交换后再与新子节点进行比较,直至停止(while)
      4. 遍历完后堆就变成了从小到大的有序堆 - 堆排序
    int size = arr.length;
    swap(arr, 0, --size);
    while (size > 0) {
    	heapify(arr, 0, size);
    	swap(arr, 0, --size);
    }
    
    public static void heapify(int[] arr, int index, int size) {
    	int left = index * 2 + 1;
    	while (left < size) { //左边不越界
    		int largest = left + 1 < size && arr[left + 1] > arr[left] ? left + 1 : left; //找到两个孩子中最大的;如果只有一个左孩子,就返回左孩子
    		largest = arr[largest] > arr[index] ? largest : index; //孩子最大值与自己比较
    		if (largest == index) {	//自己节点最大
    			break;
    		}
    		swap(arr, largest, index);
    		index = largest;
    		left = index * 2 + 1;
    	}
    }
    
    • 堆排序两个过程
      • 调整堆向上走:heapinsert
      • 调整堆向下走:heapify
      • 堆 - 大根堆 - 有序堆
    • 时间复杂度:
      • 建立大根堆时间复杂度:log1+log2+..logN=O(N)
      • 调整大根堆时间复杂度:O(NlogN)
      • 工程效果不好,原因常数项操作太多

    Java_Arrays库 - Arrays.sort()系统排序

    • Arrays.sort(arr)
      • 当arr.size<60左右时,后台使用的是insertion_sort
        原因插入排序常数项很少,在数据项少时使用效果很好
      • 当arr.size>60左右时,后台使用的是merge_sort和quick_sort
        • 当数据类型是int / char / double时,后台用quick_sort
        • 当数据类型是自定义数据类型时,后台默认用merge_sort(可以使用自定义比较器)
        • 两者区别使用的原因:排序稳定性
          • 基础类型不要求稳定性,只要求快(常数项比merge_sort少)
          • 自定义类型要求排序稳定性,现实世界有原始数据次序往下传的需求

    比较器

    • 对两个或多个数据项进行比较,以确定它们是否相等,或确定它们之间的大小关系及排列顺序称为比较。 能够实现这种比较功能的电路或装置称为比较器。

    • 定义比较器 / 重新定义比较函数

      • 其中一种继承Comparator接口的比较器实现方法
    public static class IdDescendingComparator implements Comparator<Student> {
    
        @Override    //传入student类
    	public int compare(Student o1, Student o2) {   //比较student类中的id属性
    		return o2.id - o1.id;  //return>0,则第一个参数排序在前;否则第二个参数排序在前
    	}
    }
    
    Arrays.sort(students, new IdAscendingComparator());
    		printStudents(students);
    

    排序-桶排序

    • 与之前的算法不同,排序过程与比较无关,基于数据状况(数据唯一值越少越好)
    • 思想(桶排序是一种思想)
      • 数据序列N个,唯一数据种类n个,准备n个容器
      • 遍历数据,放入对应数据类容器
      • 遍历完后再将容器倒出,排序完成
    • 时间复杂度:O(N),空间复杂度:O(N)
      • 可以用桶排序去减少时间开销,当空间内存富裕的时候
    • 常见的两种桶排序方法
      • 计数排序
      • 基数排序
    • 算法实现(Java)
    public static void bucketSort(int[] arr) {       //计数排序实现的桶排序
    	if (arr == null || arr.length < 2) {
    		return;
    	}
    	int max = Integer.MIN_VALUE;
    	for (int i = 0; i < arr.length; i++) {
    		max = Math.max(max, arr[i]);
    	}
    	int[] bucket = new int[max + 1];
    	for (int i = 0; i < arr.length; i++) {
    		bucket[arr[i]]++;
    	}
    	int i = 0;
    	for (int j = 0; j < bucket.length; j++) {
    		while (bucket[j]-- > 0) {
    			arr[i++] = j;
    		}
    	}
    }
    

    计数排序

    • 生成0-max索引的数组
    • 遍历序列,遇到某值则数组某值索引++
    • 遍历数组,有多少数则返回多少个对应索引值

    例题:最大相邻数差

    • 题目表述

      • 输入一个无序数组,元素long类型,正负0都可以;输出有序后相邻两元素之间的最大差值,要求时间复杂度O(N)
    • 思想

      • 不使用正常排序,想到桶排序,因为时间复杂度O(N);也不适用计数排序,因为元素状况
      • 数组个数N个,准备N+1个桶,将该数组中最小-最大值范围等分成N+1份,对应N+1个桶
      • 将数组元素根据划分范围放入桶中,最小桶和最大桶一定不为空,中间桶一定有一个空桶
      • 核心:
        • 离空桶左右最近的两个桶,左桶的最大值跟右桶的最小值,一定相邻,并且差值一定>=一个桶的范围;所以,我们不用关心一个桶内部相邻情况,只需要知道空桶左右最值相差情况。
      • 所以在数据放入桶的时候,只需要记录桶的最大值/最小值,其他值都不需要存储,遍历完后将找到所有空桶,比较各空桶相邻的两数差即可
    • 演示

      • 3150(-9)8(0.5) 范围(-9 - 8)等分成(7+1)份 有8个桶
      • -9 空 空 空 0,0.5,1 3 5 8
      • 只保留各个桶的最值情况,找到空桶(连续空桶可以跳过),比较空桶相邻两数差
    • 算法过程

      1. boolean min max数组各n+1个元素(建桶)
      2. 序列元素进桶,记录桶号
      3. boolean设置true,比较该桶最大小值
      4. 遍历每一个非空桶,用(上一桶的最大值-本桶的最小值)与全局比较,得到结果
    public static int maxGap(int[] nums) {
    		if (nums == null || nums.length < 2) {
    			return 0;
    		}
    		int len = nums.length;
    		int min = Integer.MAX_VALUE;
    		int max = Integer.MIN_VALUE;
    		for (int i = 0; i < len; i++) {
    			min = Math.min(min, nums[i]);
    			max = Math.max(max, nums[i]);
    		}
    		if (min == max) {
    			return 0;
    		}
    		boolean[] hasNum = new boolean[len + 1];  //建桶
    		int[] maxs = new int[len + 1];
    		int[] mins = new int[len + 1];
    		int bid = 0;
    		for (int i = 0; i < len; i++) {
    			bid = bucket(nums[i], len, min, max);    //放桶
    			mins[bid] = hasNum[bid] ? Math.min(mins[bid], nums[i]) : nums[i];   //更新
    			maxs[bid] = hasNum[bid] ? Math.max(maxs[bid], nums[i]) : nums[i];
    			hasNum[bid] = true;
    		}
    		int res = 0;
    		int lastMax = maxs[0];
    		int i = 1;
    		for (; i <= len; i++) {     //得到结果
    			if (hasNum[i]) {
    				res = Math.max(res, mins[i] - lastMax);
    				lastMax = maxs[i];
    			}
    		}
    		return res;
    	}
    
    	public static int bucket(long num, long len, long min, long max) {
    		return (int) ((num - min) * len / (max - min));
    	}
    
  • 相关阅读:
    博弈基础小结
    P4677 山区建小学|区间dp
    两道DP,四年修一次路
    每天一套题打卡|河南省第七届ACM/ICPC
    nyoj 1278G: Prototypes analyze 与 二叉排序树(BST)模板
    表达式求值
    每天一套题打卡|河南省第八届ACM/ICPC
    每天一套题打卡|河南省第九届ACM/ICPC
    每天一套题打卡|河南省第十届ACM/ICPC
    [UNIX]UNIX常用命令总结
  • 原文地址:https://www.cnblogs.com/ymjun/p/11657471.html
Copyright © 2020-2023  润新知