【算法】牛客网算法初级班(复杂度估计和排序算法(上))

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/ARPOSPF/article/details/81587454

复杂度估计和排序算法(上)


时间复杂度

(1)常数时间的操作:一个操作如果和数据量没有关系,每次都是固定时间内完成的操作,叫做常数操作。

(2)时间频度:一个算法执行所耗费的时间,从理论上是不能算出来的,必须上机运行测试才能知道。一个算法中的语句执行次数称为语句频度或时间频度。记为T(n)。

(3)时间复杂度:时间频度中,n称为问题的规模,当n不断变化时,时间频度T(n)也会不断变化。引入时间复杂度概念。 一般情况下,算法中基本操作重复执行的次数是问题规模n的某个函数,用T(n)表示,若有某个辅助函数f(n),使得当n趋近于无穷大时,T(n)/f(n)的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数。记作T(n)=O(f(n)),称O(f(n)) 为算法的渐进时间复杂度,简称时间复杂度。 

在各种不同算法中,若算法中语句执行次数为一个常数,则时间复杂度为O(1),另外,在时间频度不相同时,时间复杂度有可能相同,如T(n)=n2+3n+4与T(n)=4n2+2n+1它们的频度不同,但时间复杂度相同,都为O(n2)。 按数量级递增排列,常见的时间复杂度有:常数阶O(1),对数阶O(log2n),线性阶O(n), 线性对数阶O(nlog2n),平方阶O(n2),立方阶O(n3),..., k次方阶O(nk),指数阶O(2n)。随着问题规模n的不断增大,上述时间复杂度不断增大,算法的执行效率越低。

常见时间复杂度度排序:

O(logN)<O(N)<O(N*logN)<O(N^2)<O(n^3)<O(n^k)<O(2^N)<O(k^N)<O(N!)

(4)空间复杂度:空间复杂度是指算法在计算机内执行时所需存储空间的度量。记作: S(n)=O(f(n)) ,一般所讨论的是除正常占用内存开销外的辅助存储单元规模。

(5)渐进时间复杂度评价算法时间性能:主要用算法时间复杂度的数量级(即算法的渐近时间复杂度)评价一个算法的时间性能。 

当一个算法的空间复杂度为一个常量,即不随被处理数据量n的大小而改变时,可表示为O(1);当一个算法的空间复杂度与以2为底的n的对数成正比时,可表示为0(10g2n);当一个算法的空I司复杂度与n成线性比例关系时,可表示为0(n).若形参为数组,则只需要为它分配一个存储由实参传送来的一个地址指针的空间,即一个机器字长空间;若形参为引用方式,则也只需要为其分配存储一个地址的空间,用它来存储对应实参变量的地址,以便由系统自动引用实参变量。 

评价一个算法流程的好坏,先看时间复杂度的指标,然后再分析不同数据样本下的实际运行时间,也就是常数项时间。

常用的算法的时间复杂度和空间复杂度

排序法

最差时间分析 平均时间复杂度 稳定度 空间复杂度
冒泡排序 O(n2) O(n2) 稳定 O(1)
快速排序 O(n2) O(n*log2n) 不稳定 O(log2n)~O(n)
选择排序 O(n2) O(n2) 稳定 O(1)
二叉树排序 O(n2) O(n*log2n) 不一顶 O(n)

插入排序

O(n2) O(n2) 稳定 O(1)
堆排序 O(n*log2n) O(n*log2n) 不稳定 O(1)
希尔排序 O O 不稳定 O(1)

2.排序(冒泡、选择、插入)

查找一个数:二分法,时间复杂度为O(log_2N),N个数一共可以分log_2N次,遍历法,时间复杂度为O(N)。

/**
 * 冒泡排序
 */
public class BubbleSort {
    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]) {
                    swap(arr, i, i + 1);
                }
            }
        }
    }

    public static void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
}
/**
 * 选择排序
 */
public class SelectionSort {
    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);
        }
    }

    private static void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
}
/**
 * 插入排序
 */
public class InsertionSort {
    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);
            }
        }
    }
    //交换两个数(i,j不同位置的数)
    private 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];
    }
}

3.递归:当一个函数用它自己来定义时就称为是递归(recursive)

递归的四个基本法则:

  • 基准情形(base case):必须总要有某些基准的情形,它们不用递归就能求解
  • 不断推进(making progress):对于那些要递归求解的情形,递归调用必须总能够朝着一个基准情形推进
  • 设计法则:假设所有的递归调用都能运行
  • 合成效益法则:在求解一个问题的同一个实例时,切勿在不同的递归调用中做重复性的工作

master公式的使用:

T(N)=a*T(N/b)+O(N^d)(第一部分表示子过程时间复杂度,第二部分表示除子过程外的时间复杂度)

  1. log(b,a)>d \rightarrow O(N^{log(b,a)})
  2. log(b,a)=d\rightarrow O(N^d*logN)
  3. log(b,a)<d\rightarrow O(N^d)
/**
 * 归并排序
 */
public class MergeSort {
    public static void mergeSort(int[] arr) {
        if (arr == null || arr.length < 2) {
            return;
        }
        mergeSort(arr, 0, arr.length - 1);
    }

    //arr[L...R]范围上调整成有序的->T(N)
    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);
    }

    //L...M   M+1...R->L...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++];
        }
        //p1和p2一定有一个越界,另一个不越界
        while (p1 <= M) {
            help[i++] = arr[p1++];
        }
        while (p2 <= R) {
            help[i++] = arr[p2++];
        }
        //help有序的,拷贝回原数组
        for (i = 0; i < help.length; i++) {
            arr[L + i] = help[i];
        }
    }
}

对数器的概念与使用

  1. 有一个你想要测的方法a,
  2. 实现一个绝对正确但是复杂度不好的方法b,
  3. 实现一个随机样本产生器,
  4. 实现比对的方法,
  5. 把方法a和方法b比对很多次来验证方法a是否正确,
  6. 如果有一个样本使得比对出错,打印样本分析是哪个方法出错,
  7. 当样本数量很多时比对测试依然正确,可以确定方法a已经正确。

小和问题和逆序对问题

小和问题:在一个数组中,每一个数左边比当前数小的累加起来,叫做这个数组的小和。求一个数组的小和

  1. 假设左组为L[],右组为R[],左右两个组的组内都已经有序,现在要利用外排序合并成一个大组,并假设当前外排序是L[i]和R[j]在进行比较。
  2. 如果L[i]<R[j],那么产生小和。假设从R[j]往右一直到R[]结束,元素的个数为m,那么产生的小和为L[i]*m。
  3. 如果L[i]>=R[j],不产生小和。
  4. 整个归并排序的过程该怎么进行就怎么进行,排序的过程没有任何变化,只是利用步骤1~步骤3,也就是在组间合并的过程中累加所有产生的小和,总共的累加和就是结果。
  5. 时间复杂度为O(NlogN),额外空间复杂度为O(N)​​​​​​​
import java.util.Scanner;

/**
 * 小和问题
 * 在一个数组中,每一个数左边比当前数小的累加起来,叫做这个数的小和,求一个数组的小和
 */
public class SmallSum {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        while (sc.hasNext()) {
            String[] str = sc.nextLine().split(",");
            int[] num = new int[str.length];
            for (int i = 0; i < str.length; i++) {
                num[i] = Integer.parseInt(str[i]);
            }
            int result = getSmallSum(num);
            System.out.println(result);
        }
        sc.close();
    }

    private static int getSmallSum(int[] num) {
        if (num == null || num.length < 2)
            return 0;
        return func(num, 0, num.length - 1);
    }

    private static int func(int[] num, int L, int R) {
        //结束条件
        if (L == R) {
            return 0;
        }
        //计算中点位置
        //防止溢出
        int mid = L + ((R - L) >> 1);
        //左边产生的小和+右边陈胜的小和+合并产生的小和就是整个数组的小和
        return func(num, L, mid) + func(num, mid + 1, R) + merge(num, L, mid, R);
    }

    private static int merge(int[] num, int l, int mid, int r) {
        //辅助数组
        int[] temp = new int[r - l + 1];
        //辅助数组下标
        int i = 0;
        //左半边数组的指针
        int p1 = l;
        //右半边数组的指针
        int p2 = mid + 1;
        //小和
        int res = 0;
        while (p1 <= mid && p2 <= r) {
            //如果左边指向的值小于右边指向的值,那么p1位置的值一定小于p2以后的所有值
            //因为是有序的,这时候产生小和
            if (num[p1] <= num[p2]) {
                //计算小和
                res = res + num[p1] * (r - p2 + 1);
                //排序过程
                temp[i++] = num[p1++];
            } else {
                //排序过程
                temp[i++] = num[p2++];
            }
        }
        //p1没有越界,说明p2越界了,将左边剩余元素拷贝到辅助数组
        while (p1 <= mid) {
            temp[i++] = num[p1++];
        }
        //p2没有越界,说明p1越界了
        while (p2 <= r) {
            temp[i++] = num[p2++];
        }
        //将临时数组中的内容拷贝回原数组中
        //(原left-righe范围的内容被复制回原数组)
        for (int j = 0; j < temp.length; j++) {
            num[l + j] = temp[j];
        }
        return res;
    }
}

逆序对问题:在一个数组中,左边的数如果比右边的数大,则这两个数构成一个逆序对,请打印所有逆序对。

import java.util.Scanner;

/**
 * 逆序对问题
 * 在一个数组中,左边的数如果比右边的数大,则这两个数构成一个逆序对,请打印所有逆序对。
 */
public class Reverse {
    public static int count = 0;
    public static void main(String[] args){
        Scanner sc = new Scanner(System.in);
        while (sc.hasNext()){
            String[] str = sc.nextLine().split(",");
            int[] num = new int[str.length];
            for (int i = 0; i < str.length; i++) {
                num[i] = Integer.parseInt(str[i]);
            }
            reverse(num);
            System.out.println(count);
        }
        sc.close();
    }

    private static void reverse(int[] num) {
        if (num==null||num.length<2){
            return;
        }
        mergeSort(num,0,num.length-1);
    }

    private static void mergeSort(int[] num, int l, int r) {
        if (l==r){
            return;
        }
        int mid = l+((r-l)>>1);
        //打印左边合并产生的逆序对
        mergeSort(num,l,mid);
        //打印右边合并产生的逆序对
        mergeSort(num,mid+1,r);
        //打印整体合并产生的逆序对
        merge(num,l,mid,r);
    }

    private static void merge(int[] num, int l, int mid, int r) {
        int[] temp = new int[r-l+1];
        int i = 0;
        int p1 = l;
        int p2 = mid+1;
        while (p1<=mid&&p2<=r){
            //如果p1元素大于p2元素,那么左半边元素一定都大于p1元素
            if (num[p1]>num[p2]){
                //p2以后的逆序对元素全部打印出来
                for (int j = p2; j <=r ; j++) {
                    System.out.println(num[p1]+""+num[j]);
                }
                //计算数量
                count+=(r-p2+1);
                temp[i++]=num[p1++];
            }else {
                temp[i++]=num[p2++];
            }
        }
        while (p1<=mid){
            temp[i++]=num[p1++];
        }
        while (p2<=r){
            temp[i++]=num[p2++];
        }
        for (int j = 0; j < temp.length; j++) {
            num[l+j] = temp[j];
        }
    }
}

猜你喜欢

转载自blog.csdn.net/ARPOSPF/article/details/81587454