牛客算法学习1

六月ゝ 毕业季﹏ 提交于 2019-11-30 06:12:38

title: 牛客算法学习part1
date: 2019-06-16 20:59:43
categories:

  • 算法
    tags:

1. 概念

1.1. 时间复杂度

  1. 概念

    • 时间复杂度
      为一个算法流程中, 常数操作数量的指标, 这个指标叫做O, big O.只要高阶项, 不要低阶项, 也不要高阶项系数, 剩下部分记为f(N), 时间复杂度为O(f(N))

    • 常数操作
      完成操作的时间与数据量无关

  2. 例子

    1. 寻找数组(长度N)中最大值
      变量max = 系统最小值, 遍历数组, 时间复杂度为O(N)

    2. 有序数组二分查找
      时间复杂度为O(logN) 默认以2为底

    3. 两个有序数组寻找相同的部分, 长度为N, M

      1. 循环遍历两个数组
        O(N * M)

      2. 遍历左边数组, 在右边数组二分查找
        O(N * logM)

      3. 外排, 假定无重复
        取两个数组的起始索引为P1, P2, 因为是寻找公共的部分且两个数组有序, 所以只有当a[P1] == a[p2]时指针才同时动, 如果当谁的值更小, 就单独移动谁的指针, 一直移动到两端值相等为止.
        O(N + M)
        最优解要根据实际的数据量进行确定, 当N的长度远小于M的时候, 通过第二种方法时间复杂度更小, 具体情况具体分析.

1.2. 空间复杂度

  1. 概念

    • 空间复杂度
      一般指额外空间复杂度, 不算上输入输出需要的空间.
  2. 例子

    1. 数组中分成两个部分, 左右两个部分交换, 总长度为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)
时间复杂度的计算过程
T(n)=aT(nb)+O(nd)T(n)=aT(\frac{n}{b}) +O(n^{d})
其中:
logba>dO(nlogba) \log _b a > d \Rightarrow O(n^{log _b a})
logba<dO(nd) \log _b a < d \Rightarrow O(n^d)
logba=dO(ndlogn) \log _b a = d \Rightarrow O(n^d * \log n)
**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),且要求不能用非基于比较的排序

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!