算法<初级> - 第一章 时间复杂度与排序(完结)

算法<初级> - 第一章

时间复杂度:

  • 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));
	}
posted @ 2019-10-11 22:39  黄龙士  阅读(434)  评论(0编辑  收藏  举报