排序算法之归并,快速,堆和桶

1,047 阅读5分钟

归并排序(Merge sort)

归并排序是创建在归并操作上的一种有效的排序算法。该算法是采用分治法的一个非常典型的应用,且各层分治递归可以同时进行

算法思路

归并算法,指的是将两个已经排序的序列合并成一个序列的操作

递归法(Top-down)

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

迭代法(Bottom-up) 原理如下(假设序列共有 n 个元素):

  1. 将序列每相邻两个数字进行归并操作,形成 floor(n/2) 个序列,排序后每个序列包含两/一个元素
  2. 若此时序列数不是 1 个则将上述序列再次归并,形成 floor(n/4) 个序列,每个序列包含四/三个元素
  3. 重复步骤2,直到所有元素排序完毕,即序列数为 1

图片源自Visualgo

实现

Java 递归版本

public class MergeSort {

    public static void main(String[] args) {
        int[] unsortedArray = new int[]{5, 3, 6, 2, 1, 9, 4, 8, 7};
        MergeSort.mergeSort(unsortedArray);
        System.out.println("After sorted: ");
        for (int number : unsortedArray) {
            System.out.print(" " + number);
        }
    }

    public static void mergeSort(int[] arrayList) {
        if (arrayList == null || arrayList.length == 0) {
            return;
        }

        sort(arrayList, 0, arrayList.length - 1);
    }

    private static void sort(int[] arrayList, int leftStart, int rightEnd) {
        if (leftStart >= rightEnd) {
            return;
        }

        // 找出中间索引即左边数组的末尾位置
        int leftEnd = (leftStart + rightEnd) >> 1;

        // 右边数组的起始位置在左边数组末尾右侧
        int rightStart = leftEnd + 1;

        // 对左边数组进行递归
        sort(arrayList, leftStart, leftEnd);

        // 对右边数组进行递归
        sort(arrayList, rightStart, rightEnd);

        // 合并
        merge(arrayList, leftStart, leftEnd, rightStart, rightEnd);
    }

    private static void merge(int[] arrayList, int leftStart, int leftEnd, int rightStart, int rightEnd) {
        int[] tempArray = new int[arrayList.length];
        int tempIndex = leftStart;
        int resultIndex = leftStart;

        while (leftStart <= leftEnd && rightStart <= rightEnd) {
            // 从两个数组中取出最小的放入临时数组
            tempArray[tempIndex++] = arrayList[leftStart] <= arrayList[rightStart] ? arrayList[leftStart++] : arrayList[rightStart++];
        }

        // 剩余部分依次放入临时数组(实际上两个while只会执行其中一个)
        while (leftStart <= leftEnd) {
            tempArray[tempIndex++] = arrayList[leftStart++];
        }

        while (rightStart <= rightEnd) {
            tempArray[tempIndex++] = arrayList[rightStart++];
        }

        // 将临时数组中的内容拷贝回原数组中
        //(原left-right范围的内容被复制回原数组)
        while (resultIndex <= rightEnd) {
            arrayList[resultIndex] = tempArray[resultIndex++];
        }
    }
}

Java 迭代版本

public static void mergeSort(int[] arr) {
        int len = arr.length;
        int[] result = new int[len];
        int block, start;

        // 原版代码的迭代次数少了一次,没有考虑到奇数列数组的情况
        for(block = 1; block < len; block *= 2) {
            for(start = 0; start <len; start += 2 * block) {
                int low = start;
                int mid = (start + block) < len ? (start + block) : len;
                int high = (start + 2 * block) < len ? (start + 2 * block) : len;
                //两个块的起始下标及结束下标
                int start1 = low, end1 = mid;
                int start2 = mid, end2 = high;
                //开始对两个block进行归并排序
                while (start1 < end1 && start2 < end2) {
                    result[low++] = arr[start1] < arr[start2] ? arr[start1++] : arr[start2++];
                }
                while(start1 < end1) {
                    result[low++] = arr[start1++];
                }
                while(start2 < end2) {
                    result[low++] = arr[start2++];
                }
            }
            int[] temp = arr;
            arr = result;
            result = temp;
        }
    }

时间复杂度

最好的情况:一趟归并需要 n 次,总共需要 logN 次,因此为 O(N*logN)

最坏的情况: 接近于平均情况,为 O(N*logN)

说明:对长度为 n 的文件,需进行 logN 趟二路归并,每趟归并的时间为O(n),故其时间复杂度无论是在最好情况下还是在最坏情况下均是 O(N*logN)

稳定性

归并排序最大的特色就是它是一种稳定的排序算法。归并过程中是不会改变元素的相对位置的。

缺点是,它需要O(n)的额外空间。但是很适合于多链表排序。

快速排序(Quick Sort)

它是由冒泡排序改进而来的。相比冒泡排序,每次交换是跳跃式的。每次排序的时候设置一个基准点,将小于等于基准点的数全部放在基准点左边,将大于等于基准点的数全部放在基准点右边,重复此操作即可得到排序后的序列。

图片源自Visualgo

实现

Java

public class QuickSort {

    public static void sort(int[] array) {
        if (array == null || array.length == 0) {
            return;
        }

        int left = 0;
        int right = array.length - 1;
        quickSort(array, left, right);
    }

    private static void quickSort(int[] array, int left, int right) {
        if (left > right) {
            return;
        }

        int base = array[left];
        int i = left;
        int j = right;
        while (i != j) {
            while (array[j] > base && i < j) {
                --j;
            }

            while (array[i] < base && i < j) {
                ++i;
            }

            if (i < j) {
                int temp = array[i];
                array[i] = array[j];
                array[j] = temp;
            }
        }

        quickSort(array, left, i - 1);
        quickSort(array, i + 1, right);
    }

    public static void main(String[] args) {
        int[] unsortedArray = new int[]{6, 5, 3, 1, 8, 7, 2, 4};
        QuickSort.sort(unsortedArray);
        System.out.println("After sorted: ");
        for (int number : unsortedArray) {
            System.out.print(" " + number);
        }

    }
}

时间复杂度

最好的情况:因为每次都将序列分为两个部分(一般二分都复杂度都和 logN 相关),故为 O(N*logN)

最坏的情况:序列基本有序,选取的枢轴元素为最大值或最小值时,退化为冒泡排序,几乎要比较 N x N / 2 次,故为 O(N*N)

如何避免最坏情况:为改进快速排序算法,随机选取界点或最左、最右、中间三个元素中的值处于中间的作为界点,通常可以避免原始序列有序的最坏情况

稳定性

由于每次都需要和中轴元素交换,因此原来的顺序就可能被打乱。如序列为 5 3 3 4 3 8 9 10 11会将3的顺序打乱。所以说,快速排序是不稳定的

快排在数据少的时候是不占优势的,所以一般是在数据少的时候用插入排序,数据多的时候用快排。

堆排序

请参考常见排序算法 - 堆排序

时间复杂度

最坏情况下,接近于最差情况下:O(N*logN),因此它是一种效果不错的排序算法

稳定性

堆排序需要不断地调整堆,因此堆排序是一种不稳定的排序!

桶排序

请参考常见排序算法 - 桶排序