title: 牛客算法学习part1
date: 2019-06-16 20:59:43
categories:
- 算法
tags:
1. 概念
1.1. 时间复杂度
-
概念
-
时间复杂度
为一个算法流程中, 常数操作数量的指标, 这个指标叫做O, big O.只要高阶项, 不要低阶项, 也不要高阶项系数, 剩下部分记为f(N), 时间复杂度为O(f(N)) -
常数操作
完成操作的时间与数据量无关
-
-
例子
-
寻找数组(长度N)中最大值
变量max = 系统最小值, 遍历数组, 时间复杂度为O(N) -
有序数组二分查找
时间复杂度为O(logN) 默认以2为底 -
两个有序数组寻找相同的部分, 长度为N, M
-
循环遍历两个数组
O(N * M) -
遍历左边数组, 在右边数组二分查找
O(N * logM) -
外排, 假定无重复
取两个数组的起始索引为P1, P2, 因为是寻找公共的部分且两个数组有序, 所以只有当a[P1] == a[p2]时指针才同时动, 如果当谁的值更小, 就单独移动谁的指针, 一直移动到两端值相等为止.
O(N + M)
最优解要根据实际的数据量进行确定, 当N的长度远小于M的时候, 通过第二种方法时间复杂度更小, 具体情况具体分析.
-
-
1.2. 空间复杂度
-
概念
- 空间复杂度
一般指额外空间复杂度, 不算上输入输出需要的空间.
- 空间复杂度
-
例子
- 数组中分成两个部分, 左右两个部分交换, 总长度为N
如12345 67, 交换为67 12345
- 通过一个辅助空间先存入67, 存入12345 空间复杂度为O(N) - 直接在原数组中进行操作 通过一个辅助变量进行数组逆序, 得到7654321, 再对67单独逆序, 54321单独逆序, 得到结果6712345, 也可以先单独逆序, 再进行整体逆序 这里的空间复杂度为O(1)
1.3. 最优解
一般情况下 先满足时间复杂度最优, 再满足空间复杂度最优.
1.4. 排序的稳定性
无序数组中值相同的部分 排成有序之后相对次序保持不变
- 可以做到稳定
- 冒泡
- 插入
- 归并
- 不可以做到稳定
- 选择
- 快速 (可以做到, 论文级别, 很难 01 stable sort)
2. 数组排序
2.1. 时间复杂度O(N^2),空间复杂度O(1)
2.1.1. 冒泡排序
```java
public static void bubbleSort(int[] arr){
if (arr == null || arr.length < 2) {
return;
}
for (int end = arr.length - 1; end > 0; end--){
for (int i = 0; i < end; i ++){
if (arr[i] > arr[i + 1]) {
DigitalArrayUtil.swap(arr, i , i + 1);
}
}
}
}
```
2.1.2. 选择排序
```java
public static void selectionSort(int[] arr){
int minIndex;
for (int i = 0; i < arr.length - 1; i++) {
minIndex = i;
for (int j = i + 1; j < arr.length; j++) {
minIndex = (arr[minIndex] < arr[j]) ? minIndex : j;
}
DigitalArrayUtil.swap(arr, minIndex, i);
}
}
```
2.1.3. 插入排序
时间复杂度最好O(N), 最差O(N^2)
public static void insertionSort(int[] arr){
for (int i = 1; i < arr.length; i++) {
for (int j = i - 1; j >= 0 && arr[j] > arr[j + 1] ; j--) {
DigitalArrayUtil.swap(arr, j, j + 1);
}
}
}
2.2. 时间复杂度O(NlogN)
2.2.1. 归并排序
例如7 9 8 4 6 2, 先划分为798 462两个部分, 左边再划分成 79 8 , 左边再到7 9, 最后左边到7, 此时只剩一个数了, 不用再划分, 再依次与右边的进行有序合并的, 一直递归向上, 最后排序完成.
其实就是一个划分, 合并的过程.
空间复杂度O(N)
归并排序内部缓存法实现空间复杂度O(1)
时间复杂度的计算过程
其中:
**a 为递归中子递归个数, n/b 为子递归的数据规模。**这里a = 2, b = 2, d = 1
其实就是
private static void mergeSort(int[] arr) {
if (arr == null || arr.length < 2){
return;
}
sortProcess(arr, 0, arr.length - 1);
}
/**
* 归并
* @param arr
* @param L
* @param R
*/
public static void sortProcess(int[] arr, int L, int R){
if (L == R){
return;
}
int mid = (R - L) / 2 + L;
sortProcess(arr, L, mid);
sortProcess(arr, mid + 1, R);
merge(arr, L, mid, R);
}
/**
* 外排合并
* @param arr
* @param L
* @param mid
* @param R
*/
public static void merge(int[] arr, int L, int mid, int R){
int[] help = new int[R - L + 1];
int p1 = L;
int p2 = mid + 1;
int i = 0;
while (p1 <= mid && p2 <= R){
help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
}
//只会有一个越界, 只会处理一个while循环
while(p1 <= mid){
help[i++] = arr[p1++];
}
while(p2 <= R){
help[i++] = arr[p2++];
}
for (int j = 0; j < help.length; j++) {
arr[L + j] = help[j];
}
}
2.2.2. 快速排序
快速排序的思想基本上还是将问题划分, 然后递归进行.
总体上划分为三个区域, 大于区, 小于区, 等于区.
再接着在大于区和小于区上进行同样的划分, 最后排序成功
最好的时间复杂度为O(NlogN), 最差为O(N²), 空间复杂度O(logN)
private static void quickSort(int[] arr, int L, int R){
if (L < R){
//随机快速排序
DigitalArrayUtil.swap(arr, L + (int)(Math.random() * (R - L + 1)), R);
//划分区域
int[] partition = partition(arr, L, R);
quickSort(arr, L, partition[0] - 1);
quickSort(arr, partition[1] + 1, R);
}
}
private static int[] partition(int[] arr, int L, int R){
int less = L - 1;
//为了少用一个变量, 这里大于区域直接指向最后一位, 就避免了最后被改变, 但要在最后进行交换
int more = R;
//一直循环到大于区域为止
while(L < more){
if (arr[L] < arr[R]) {
//如果属于小于区域
DigitalArrayUtil.swap(arr, L++, ++less);
} else if (arr[L] > arr[R]){
//如果属于大于区域, 因为此时所在的位置是等于区, 所以指针不动, 接着比较, 直到进入正确的大于区域
DigitalArrayUtil.swap(arr, L, --more);
} else{
//属于等于区域, 不做改变
L++;
}
}
//为之前的准备 做交换
DigitalArrayUtil.swap(arr, more, R);
// 值得注意的是这里等于区域到more为止, 因为之前最后一位作为保留, 后又进行了交换, 所以大于区域的第一位实际是等于区的数
return new int[]{less + 1, more};
}
2.2.3. 堆排序
时间复杂度O(N*logN),额外空间复杂度O(1)
堆可以看成一个二叉树,所以可以考虑使用二叉树的表示方法来表示堆。但是因为堆中元素按照一定的优先顺序排列,因此可以使用更简单的方法——数组——来表示,这样可以节省子节点指针空间,并且可以快速访问每个节点。
如果只是建立堆的过程,时间复杂度为O(N)
堆排序其实就是一个建立大根堆之后进行循环处理的过程, 在简单理解了堆的结构之后理解堆排序其实不难.
package chapter2;
import utils.DigitalArrayUtil;
import java.util.Arrays;
/**
* @author jhmarryme.cn
* @date 2019/7/4 10:42
*/
public class Code_05_HeapSort {
/**
* 堆排序
* 1. 建立大根堆
* 2. 堆循环处理
* 2.1 首尾交换
* 2.2 调整位置
* @param arr
*/
public static void heapSort(int[] arr){
if (arr == null || arr.length < 2) {
return;
}
//建立大根堆
for (int i = 0; i < arr.length; i++) {
heapInsert(arr, i);
}
int size = arr.length;
while (--size > 0){
DigitalArrayUtil.swap(arr, 0, size);
heapfiy(arr, 0, size);
}
}
/**
* 交换首尾后的 堆排序处理
* @param arr
* @param index
* @param heapSize
*/
private static void heapfiy(int[] arr, int index, int heapSize) {
int left = index * 2 + 1;
while (left < heapSize){
// 当右节点存在且大于左节点时 取右节点
int largest = left + 1 < heapSize && arr[left + 1] > arr[left] ? left + 1 : left;
largest = arr[index] > arr[largest] ? index :largest;
if (largest == index) {
// 代表子节点没有比自己大的了
break;
}
DigitalArrayUtil.swap(arr, index, largest);
index = largest;
left = index * 2 + 1;
}
}
/**
* 建立大根堆
* @param arr
* @param index
*/
private static void heapInsert(int[] arr, int index) {
while(arr[index] > arr[(index - 1) / 2]){
DigitalArrayUtil.swap(arr, index, (index - 1) / 2);
index = (index - 1) / 2;
}
}
/**
* 测试堆排序
* @param args
*/
public static void main(String[] args) {
int testTime = 50000;
int size = 100;
int value = 100;
boolean success = true;
long l = System.currentTimeMillis();
for (int i = 0; i < testTime; i++) {
int[] arr1 = DigitalArrayUtil.generateRandomArray(size, value);
int[] arr2 = Arrays.copyOf(arr1, arr1.length);
heapSort(arr1);
DigitalArrayUtil.comparator(arr2);
if (!DigitalArrayUtil.isEqual(arr1, arr2)) {
success = false;
break;
}
}
long l2 = System.currentTimeMillis();
System.out.println("success = " + success);
System.out.println("运行时间 = " + (l2 - l));
}
}
2.2.4. 桶排序
不是基于比较的算法
例如有几亿个数, 范围是0~200, 那么只需要准备201个桶, 遍历一遍数组把对应的数字放入桶内, 最后再将桶内的数字依次倒出来组成新的数组.
但是虽然很快, 但实际用的不多. 实际应用中一般需要排序的很少是基本类型, 都是自定义的类型. 而桶排序需要分析数据的状况, 不具备通用性.
容器类型决定稳定性, 使用队列结构作为容器是稳定的, 栈结构就不稳定
- 计数排序
package chapter2;
import utils.DigitalArrayUtil;
import java.util.Arrays;
import java.util.stream.Stream;
/**
* @author jhmarryme.cn
* @date 2019/7/28 10:35
*/
public class Code_06_BucketSort {
/**
* 计数排序
* @param arr
*/
private static void bucketSort(int[] arr) {
if (arr == null || arr.length < 2){
return;
}
int max = Integer.MIN_VALUE;
for (int i : arr) {
max = Math.max(i, max);
}
int[] bucket = new int[max + 1];
for (int i : arr) {
bucket[i]++;
}
int index = 0;
for (int i = 0; i < bucket.length; i++) {
while(bucket[i]-- > 0){
arr[index++] = i;
}
}
}
/**
* 测试桶排序
* @param args
*/
public static void main(String[] args) {
int testTime = 50000;
int size = 100;
int value = 100;
boolean success = true;
long l = System.currentTimeMillis();
for (int i = 0; i < testTime; i++) {
int[] arr1 = new int[(int) ((size + 1) * Math.random())];
for (int j = 0; j < arr1.length; j++) {
arr1[j] = ((int)(Math.random() * 201));
}
int[] arr2 = Arrays.copyOf(arr1, arr1.length);
bucketSort(arr1);
DigitalArrayUtil.comparator(arr2);
if (!DigitalArrayUtil.isEqual(arr1, arr2)) {
success = false;
break;
}
}
long l2 = System.currentTimeMillis();
System.out.println("success = " + success);
System.out.println("运行时间 = " + (l2 - l));
}
}
- 基数排序
3. 延伸题型
3.1. 区别
3.1.1. merge与quick的区别
系统中基础类型使用quick, 自定义class类型用merge类型
稳定性, 基础类型排序不考虑稳定性所以用quick, merge是稳定的.
3.2. 归并延伸
3.2.1. 求小和
利用归并排序的特点, 在进行排序过程中计算小和的总和.
3.2.2. 降序对
也是利用归并排序, 求降序对.
3.3. 桶排序
3.3.1. 排序之后的相邻最大差值
给定一个数组,求如果排序之后,相邻两数的最大差值,要求时 间复杂度O(N),且要求不能用非基于比较的排序
来源:https://blog.csdn.net/m0_37572877/article/details/101148307