6.数据结构和算法[排序算法]

133 阅读9分钟

算法的时间复杂度

度量一个程序(算法)执行时间的两种方法

1.事后统计的方法:这种方法可行, 但是有两个问题:一是要想对设计的算法的运行性能进行评测,需要实际运行该程序;二是所得时间的统计量依赖于计算机的硬件、软件等环境因素, 这种方式,要在同一台计算机的相同状态下运行,才能比较那个算法速度更快
2.事前估算的方法:通过分析某个算法的时间复杂度来判断哪个算法更优

  • 时间频度:一个算法花费的时间与算法中语句的执行次数成正比例,哪个算法中语句执行次数多,它花费时间就多。一个算法中的语句执行次数称为语句频度或时间频度。记为T(n)
  • 举例说明:比如计算1-100所有数字之和, 我们设计两种算法:
  • 忽略常数项 结论:
    2n+20 和 2n 随着n 变大,执行曲线无限接近, 20可以忽略
    3n+10 和 3n 随着n 变大,执行曲线无限接近, 10可以忽略
  • 忽略低次项 结论:
    2n^2+3n+10 和 2n^2 随着n 变大, 执行曲线无限接近, 可以忽略 3n+10
    n^2+5n+20 和 n^2 随着n 变大,执行曲线无限接近, 可以忽略 5n+20
  • 忽略系数 结论:
    随着n值变大,5n^2+7n 和 3n^2 + 2n ,执行曲线重合, 说明 这种情况下, 5和3可以忽略。
    而n^3+5n 和 6n^3+4n ,执行曲线分离,说明多少次方式关键

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

T(n) 不同,但时间复杂度可能相同。 如:T(n)=n²+7n+6 与 T(n)=3n²+2n+2 它们的T(n) 不同,但时间复杂度相同,都为O(n²)。

计算方法

用常数1代替运行时间中的所有加法常数  T(n)=n²+7n+6 => T(n)=n²+7n+1
修改后的运行次数函数中,只保留最高阶项  T(n)=n²+7n+1 => T(n) = n²
去除最高阶项的系数 T(n) = n² => T(n) = n² => O(n²)

常见的时间复杂度

  • 常数阶O(1)
    无论代码执行了多少行,只要是没有循环等复杂结构,那这个代码的时间复杂度就都是O(1),代码在执行的时候,它消耗的时候并不随着某个变量的增长而增长,那么无论这类代码有多长,即使有几万几十万行,都可以用O(1)来表示它的时间复杂度。

  • 对数阶O(log2n)
    在while循环里面,每次都将 i 乘以 2,乘完之后,i 距离 n 就越来越近了。假设循环x次之后,i 就大于 2 了,此时这个循环就退出了,也就是说 2 的 x 次方等于 n,那么 x = log2n也就是说当循环 log2n 次以后,这个代码就结束了。因此这个代码的时间复杂度为:O(log2n) 。 O(log2n) 的这个2 时间上是根据代码变化的,i = i * 3 ,则是 O(log3n)

  • 线性阶O(n)
    这段代码,for循环里面的代码会执行n遍,因此它消耗的时间是随着n的变化而变化的,因此这类代码都可以用O(n)来表示它的时间复杂度

  • 线性对数阶O(nlog2n)
    线性对数阶O(nlogN) 其实非常容易理解,将时间复杂度为O(logn)的代码循环N遍的话,那么它的时间复杂度就是 n * O(logN),也就是了O(nlogN)

  • 平方阶O(n^2)
    平方阶O(n²) 就更容易理解了,如果把 O(n) 的代码再嵌套循环一遍,它的时间复杂度就是 O(n²),这段代码其实就是嵌套了2层n循环,它的时间复杂度就是 O(nn),即  O(n²) 如果将其中一层循环的n改成m,那它的时间复杂度就变成了 O(mn)

  • 立方阶O(n^3) k次方阶O(n^k)
    参考上面的O(n²) 去理解就好了,O(n³)相当于三层n循环,其它的类似

  • 指数阶O(2^n)

常见的算法时间复杂度由小到大依次为:Ο(1)<Ο(log2n)<Ο(n)<Ο(nlog2n)<Ο(n2)<Ο(n3)< Ο(nk) <Ο(2n) ,随着问题规模n的不断增大,上述时间复杂度不断增大,算法的执行效率越低

排序算法

冒泡排序(Bubble Sort)

1.比较相邻的元素,前一个比后一个大(或者前一个比后一个小)调换位置
2.每一对相邻的元素进行重复的工作,从开始对一直到结尾对,这步完成后,结尾为做大或最小的数
3.针对除了最后一个元素重复进行上面的步骤
4.重复1-3步骤直到完成排序

// 将前面额冒泡排序算法,封装成一个方法
public static void bubbleSort(int[] arr) {
    // 冒泡排序 的时间复杂度 O(n^2), 自己写出
    int temp = 0; // 临时变量
    boolean flag = false; // 标识变量,表示是否进行过交换
    for (int i = 0; i < arr.length - 1; i++) {
        for (int j = 0; j < arr.length - 1 - i; j++) {
            // 如果前面的数比后面的数大,则交换
            if (arr[j] > arr[j + 1]) {
                flag = true;
                temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
        //System.out.println("第" + (i + 1) + "趟排序后的数组");
        if (!flag) { // 在一趟排序中,一次交换都没有发生过
            break;
        } else {
            flag = false; // 重置flag!!!, 进行下次判断
        }
    }
}

选择排序(Select Sort)

1.初始状态:序列为无序状态
2.第1次排序:从n个元素中找出最小(大)元素与第1个记录交换
3.第2次排序:从n-1个元素中找出最小(大)元素与第2个记录交换
4.第i次排序:从n-i+1个元素中找出最小(大)元素与第i个记录交换
5.以此类推直到排序完成

//选择排序
public static void selectSort(int[] arr) {
  for (int i = 0; i < arr.length - 1; i++) {
      int minIndex = i;
      int min = arr[i];
      for (int j = i + 1; j < arr.length; j++) {
          if (min > arr[j]) { // 说明假定的最小值,并不是最小
              min = arr[j]; // 重置min
              minIndex = j; // 重置minIndex
          }
      }
      // 将最小值,放在arr[0], 即交换
      if (minIndex != i) {
          arr[minIndex] = arr[i];
          arr[i] = min;
      }
  }
}

插入排序(Insert Sort)

1.从第一个元素开始,该元素可认为已排序
2.取出下一个元素,在排序好的元素序列中从后往前扫描
3.如果元素(已排序)大于新元素,将该元素移到下一位置
4.重复3.直到找到已排序的元素小于或等于新元素的位置
5.将新元素插入该位置后
6.重复2-5直到排序完成

//插入排序
public static void insertSort(int[] arr) {
int insertVal = 0;
int insertIndex = 0;
//使用for循环来把代码简化
for(int i = 1; i < arr.length; i++) {
    //定义待插入的数
    insertVal = arr[i];
    insertIndex = i - 1; // 即arr[1]的前面这个数的下标

    // 给insertVal 找到插入的位置
    // 说明
    // 1. insertIndex >= 0 保证在给insertVal 找插入位置,不越界
    // 2. insertVal < arr[insertIndex] 待插入的数,还没有找到插入位置
    // 3. 就需要将 arr[insertIndex] 后移
    while (insertIndex >= 0 && insertVal < arr[insertIndex]) {
        arr[insertIndex + 1] = arr[insertIndex];// arr[insertIndex]
        insertIndex--;
    }
    // 当退出while循环时,说明插入的位置找到, insertIndex + 1
    // 举例:理解不了,我们一会 debug
    //这里我们判断是否需要赋值
    if(insertIndex + 1 != i) {
        arr[insertIndex + 1] = insertVal;
    }
    //System.out.println("第"+i+"轮插入");
    //System.out.println(Arrays.toString(arr));
}

希尔排序(Shell Sort)

希尔排序是把记录按下表的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。

// 使用逐步推导的方式来编写希尔排序
// 希尔排序时, 对有序序列在插入时采用交换法, 
// 思路(算法) ===> 代码
public static void shellSort(int[] arr) {
  int temp = 0;
  int count = 0;
  // 根据前面的逐步分析,使用循环处理
  for (int gap = arr.length / 2; gap > 0; gap /= 2) {
      for (int i = gap; i < arr.length; i++) {
          // 遍历各组中所有的元素(共gap组,每组有个元素), 步长gap
          for (int j = i - gap; j >= 0; j -= gap) {
              // 如果当前元素大于加上步长后的那个元素,说明交换
              if (arr[j] > arr[j + gap]) {
                  temp = arr[j];
                  arr[j] = arr[j + gap];
                  arr[j + gap] = temp;
              }
          }
      }
      //System.out.println("希尔排序第" + (++count) + "轮 =" + Arrays.toString(arr));
}

归并排序(Merge Sort)

把长度为n的输入序列分成两个长度为n/2的子序列;
对这两个子序列分别采用归并排序;
将两个排序好的子序列合并成一个最终的排序序列。

/**
 * 归并排序
 *
 * @param array
 * @return
 */
public static int[] MergeSort(int[] array) {
    if (array.length < 2) return array;
    int mid = array.length / 2;
    int[] left = Arrays.copyOfRange(array, 0, mid);
    int[] right = Arrays.copyOfRange(array, mid, array.length);
    return merge(MergeSort(left), MergeSort(right));
}
/**
 * 归并排序——将两段排序好的数组结合成一个排序数组
 *
 * @param left
 * @param right
 * @return
 */
public static int[] merge(int[] left, int[] right) {
    int[] result = new int[left.length + right.length];
    for (int index = 0, i = 0, j = 0; index < result.length; index++) {
        if (i >= left.length)
            result[index] = right[j++];
        else if (j >= right.length)
            result[index] = left[i++];
        else if (left[i] > right[j])
            result[index] = right[j++];
        else
            result[index] = left[i++];
    }
    return result;
}

快速排序(Quick Sort)

快速排序(Quicksort)是对冒泡排序的一种改进。基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列

 /**
 * 快速排序方法
 * @param array
 * @param start
 * @param end
 * @return
 */
public static int[] QuickSort(int[] array, int start, int end) {
    if (array.length < 1 || start < 0 || end >= array.length || start > end) return null;
    int smallIndex = partition(array, start, end);
    if (smallIndex > start)
        QuickSort(array, start, smallIndex - 1);
    if (smallIndex < end)
        QuickSort(array, smallIndex + 1, end);
    return array;
}
/**
 * 快速排序算法——partition
 * @param array
 * @param start
 * @param end
 * @return
 */
public static int partition(int[] array, int start, int end) {
    int pivot = (int) (start + Math.random() * (end - start + 1));
    int smallIndex = start - 1;
    swap(array, pivot, end);
    for (int i = start; i <= end; i++)
        if (array[i] <= array[end]) {
            smallIndex++;
            if (i > smallIndex)
                swap(array, i, smallIndex);
        }
    return smallIndex;
}

/**
 * 交换数组内两个元素
 * @param array
 * @param i
 * @param j
 */
public static void swap(int[] array, int i, int j) {
    int temp = array[i];
    array[i] = array[j];
    array[j] = temp;
}

基数排序(Radix Sort)

基数排序也是非比较的排序算法,对每一位进行排序,从最低位开始排序,复杂度为O(kn),为数组长度,k为数组中的数的最大的位数;

基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以是稳定的。

取得数组中的最大数,并取得位数;
arr为原始数组,从最低位开始取每个位组成radix数组;
对radix进行计数排序(利用计数排序适用于小范围数的特点)

 /**
 * 基数排序
 * @param array
 * @return
 */
public static int[] RadixSort(int[] array) {
    if (array == null || array.length < 2)
        return array;
    // 1.先算出最大数的位数;
    int max = array[0];
    for (int i = 1; i < array.length; i++) {
        max = Math.max(max, array[i]);
    }
    int maxDigit = 0;
    while (max != 0) {
        max /= 10;
        maxDigit++;
    }
    int mod = 10, div = 1;
    ArrayList<ArrayList<Integer>> bucketList = new ArrayList<ArrayList<Integer>>();
    for (int i = 0; i < 10; i++)
        bucketList.add(new ArrayList<Integer>());
    for (int i = 0; i < maxDigit; i++, mod *= 10, div *= 10) {
        for (int j = 0; j < array.length; j++) {
            int num = (array[j] % mod) / div;
            bucketList.get(num).add(array[j]);
        }
        int index = 0;
        for (int j = 0; j < bucketList.size(); j++) {
            for (int k = 0; k < bucketList.get(j).size(); k++)
                array[index++] = bucketList.get(j).get(k);
            bucketList.get(j).clear();
        }
    }
    return array;
}