排序算法的原理、实现、优缺点

这里的排序算法指内部排序算法,即对内存中的数据进行排序。

1 概述

排序算法大体可分为两种:

  1. 比较排序
    时间复杂度 O(nlogn) ~ O(n^2),主要有:冒泡排序,选择排序,插入排序,归并排序,堆排序,快速排序等。
  2. 非比较排序
    当数据本身包含了定位特征时,才能不通过比较来确定元素的位置。时间复杂度可以达到O(n),主要有:计数排序,基数排序,桶排序等。

选择排序算法,需要考虑数据类型和特点,关注时间复杂度、空间复杂度,还有稳定性。

1.1 稳定性

假定在待排序的记录序列中,存在多个具有相同关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,ri=rj,且 ri 在 rj 之前,而在排序后的序列中,ri 仍在 rj 之前,则称这种排序算法是稳定的;否则称为不稳定的。

对于不稳定的排序算法,只要举出一个实例,即可说明它的不稳定性;而对于稳定的排序算法,必须对算法进行分析从而得到稳定的特性。

需要注意的是,排序算法是否为稳定的是由具体算法决定的,不稳定的算法在某种条件下可以变为稳定的算法,而稳定的算法在某种条件下也可以变为不稳定的算法。

稳定性的意义

  • 如果只是简单的进行数字的排序,那么稳定性将毫无意义
  • 如果排序的内容仅仅是一个复杂对象的某一个数字属性,那么稳定性依旧将毫无意义
  • 如果要排序的内容是一个复杂对象的多个数字属性,但是其原本的初始顺序毫无意义,那么稳定性依旧将毫无意义。
  • 除非要排序的内容是一个复杂对象的多个数字属性,且其原本的初始顺序存在意义,那么我们需要在二次排序的基础上保持原有排序的意义,才需要使用到稳定性的算法。

例如,要排序的内容是一组原本按照价格高低排序的对象,如今需要按照销量高低排序,使用稳定性算法,可以使得想同销量的对象依旧保持着价格高低的排序展现,只有销量不同的才会重新排序。

1.2 检索表

排序算法的原理、实现、优缺点_第1张图片

2 冒泡排序

2.1 原理

  1. 比较相邻的元素,如果前一个比后一个大,就把它们两个调换位置。
  2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
  3. 针对所有的元素重复以上的步骤,除了最后一个。
  4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

可以看出,上述2-3步骤要进行 N-1 次,即使原数组是已排序好的情况,复杂度始终是 O(N^2)。
可以考虑在每轮遍历时设置标志位flag,如果发生了交换flag设置为true;如果没有交换就设置为false。这样当一轮比较结束后如果flag仍为false,即:这一轮没有发生交换,说明数据的顺序已经排好,没有必要继续进行下去。
从而冒泡排序的最好情况复杂度是 O(N)。

2.2 实现

public static void bubbleSort(int[] arr) 
{
    boolean sorted = false;
    for (int i=0; i1; i++) {
        sorted = true;
        for (int j=0; j1-i; j++) {
            if (arr[j] > arr[j+1]) {
                swap(arr, j, j+1);
                sorted = false;
            }
        }
        if (sorted)
            break;
    }
}

2.3 特点

它对于少数元素之外的数列排序是很没有效率的。

3 选择排序

3.1 原理

在长度为N的无序数组中,第一次遍历N-1个数,找到最大的数值与最后一个元素交换;
第二次遍历N-2个数,找到最小的数值与第二个元素交换;
。。。
第N-1次遍历,找到最小的数值与第N-1个元素交换,排序完成。

3.2 实现

public static void selectSort(int[] arr) 
{
    int minIndex = 0;
    for (int p=0; p1; p++) {
        minIndex = p;
        for (int j=p+1; j<=arr.length-1; j++) {
            if (arr[j] < arr[minIndex])
                minIndex = j;
        }
        if (minIndex != p)
            swap(arr, p, minIndex);
    }
}

3.3 特点

选择排序是不稳定的排序算法,不稳定发生在最小元素与arr[p]交换的时刻。

4 插入排序

4.1 原理

插入排序的原理非常类似于抓扑克牌,对于未排序数据(右手抓到的牌),在已排序序列(左手已经排好序的手牌)中从后向前扫描,找到相应位置并插入。
插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。

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

4.2 实现

public static void insertSort(int[] arr)
{
    int x = 0;
    int j = 0;
    for (int p=1; p<=arr.length-1; p++) {
        x = arr[p];
        for (j=p-1; j>=0 && arr[j]>x; j--) {
            arr[j+1] = arr[j];
        }
        arr[j+1] = x;
    }
}

4.3 特点

如果需要排序的数据量很小,比如量级小于千,那么插入排序还是一个不错的选择。 插入排序在工业级库中也有着广泛的应用,在STL的sort算法和stdlib的qsort算法中,都将插入排序作为快速排序的补充,用于少量元素的排序(通常为8个或以下)。

5 归并排序

5.1 原理

归并排序的实现分为递归实现与非递归(迭代)实现。递归实现的归并排序是算法设计中分治策略的典型应用,我们将一个大问题分割成小问题分别解决,然后用所有小问题的答案来解决整个大问题。非递归(迭代)实现的归并排序首先进行是两两归并,然后四四归并,然后是八八归并,一直下去直到归并了整个数组。

归并排序算法主要依赖归并(Merge)操作。归并操作指的是将两个已经排序的序列合并成一个序列的操作,归并操作步骤如下:

  1. 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
  2. 设定两个指针,最初位置分别为两个已经排序序列的起始位置
  3. 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
  4. 重复步骤3直到某一指针到达序列尾
  5. 将另一序列剩下的所有元素直接复制到合并序列尾

5.2 实现

public static void mergeSortRecur(int[] arr, int left, int right) 
{
    if (left == right) {
        return;
    }
    int mid = (left + right) / 2;
    mergeSortRecur(arr, left, mid);
    mergeSortRecur(arr, mid+1, right);
    merge(arr, left, mid, right);
}

public static void mergeSortIter(int[] arr, int len) 
{
    int left, mid, right;
    for (int i=1; i<len; i*=2) {
        left = 0;
        while (left+i < len) {
            mid = left + i - 1;
            right = mid + i <len ? mid + i : len - 1;
            merge(arr, left, mid, right);
            left = right + 1;
        }
    }
}

private static void merge(int[] arr, int left, int mid, int right)
{
    int[] tmp = new int[right - left + 1];
    int i = left;
    int j = mid + 1;
    int index = 0;
    while (i <= mid && j<= right) {
        if (arr[i] < arr[j]) {
            tmp[index++] = arr[i++];
        } else {
            tmp[index++] = arr[j++];
        }
    }
    while (i <= mid) {
        tmp[index++] = arr[i++];
    }
    while (j <= right) {
        tmp[index++] = arr[j++];
    }
    for (int k=0; kleft++] = tmp[k];
    }
}

5.3 特点

归并排序除了可以对数组进行排序,还可以高效的求出数组小和(即单调和)以及数组中的逆序对。

6 堆排序

6.1 原理

堆排序是指利用堆这种数据结构所设计的一种选择排序算法。
通常堆是通过一维数组来实现的。在数组起始为 0 的情形中,如果 i 为当前节点的索引,则有:

父节点在位置 floor((i-1)/2);
左子节点在位置 (2*i+1);
右子节点在位置 (2*i+2);

  1. 由输入的无序数组构造一个最大堆,作为初始的无序区
  2. 把堆顶元素(最大值)和堆尾元素互换
  3. 把堆(无序区)的尺寸缩小1,并调用heapify(A, 0)从新的堆顶元素开始进行堆调整
  4. 重复步骤2,直到堆的尺寸为1

6.2 实现

public static void heapSort(int[] arr)
{
    buildHeap(arr);
    for (int tail=arr.length-1; tail>=1; tail--) {
        swap(arr, 0, tail);
        heapify(arr, 0, tail);   // 此时 tail 恰好是剩余堆的 size
    }
}

private static void buildHeap(int[] arr)
{
    for (int i=arr.length/2; i>=0; i--) {
        heapify(arr, i, arr.length);
    }
}

private static void heapify(int[] arr, int parent, int size)
{
    int left = parent*2 + 1;
    int right = parent*2 + 2;
    int maxIndex = parent;
    if (leftarr[maxIndex]) {
        maxIndex = left;
    }
    if (rightarr[maxIndex]) {
        maxIndex = right;
    }
    if (maxIndex != parent) {
        swap(arr, parent, maxIndex);
        heapify(arr, maxIndex, size);
    }
}

private static void swap(int[] arr, int i, int j)
{
    int temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}

6.3 特点

堆排序是不稳定的排序算法,不稳定发生在堆顶元素与arr[tail]交换的时刻。

7 快速排序

7.1 原理

在平均状况下,排序n个元素要O(nlogn)次比较。在最坏状况下则需要O(n^2)次比较,但这种状况并不常见。
事实上,快速排序通常明显比其他O(nlogn)算法更快,因为它的内部循环可以在大部分的架构上很有效率地被实现出来。

快速排序使用分治策略来把一个序列分为两个子序列。
步骤为:

  1. 从序列中挑出一个元素,作为”基准”(pivot).
  2. 把所有比基准值小的元素放在基准前面,所有比基准值大的元素放在基准的后面(相同的数可以到任一边),这个称为分区(partition)操作。
  3. 对每个分区递归地进行步骤1~2,递归的结束条件是序列的大小是0或1,这时整体已经被排好序了。

7.2 实现

public static void quickSort(int[] arr, int left, int right)
{
    if (left >= right) {
        return;
    }
    int pivotIndex = partition(arr, left, right);
    quickSort(arr, left, pivotIndex-1);
    quickSort(arr, pivotIndex+1, right);
}

private static int partition(int[] arr, int left, int right)
{
    int pivotVal = arr[right];  // 选择最右侧元素为基准
    int tail = left - 1;
    for (int i=left; iif (arr[i] <= pivotVal) {
            swap(arr, i, ++tail);
        }
    }
    swap(arr, tail+1, right);
    return tail+1;
}

private static void swap(int[] arr, int i, int j)
{
    int temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}

7.3 特点

快速排序是不稳定的排序算法,不稳定发生在基准元素与arr[tail+1]交换的时刻。

JDK 提供的 Arrays.sort 函数,考虑到排序算法的稳定性,对于基础类型,底层使用快速排序,对于非基础类型,底层使用归并排序。
对于基础类型,相同值是无差别的,排序前后相同值的相对位置并不重要,所以选择更为高效的快速排序,尽管它是不稳定的排序算法;而对于非基础类型,排序前后相等实例的相对位置不宜改变,所以选择稳定的归并排序。

你可能感兴趣的:(数据结构与算法分析)