排序算法学习 前言 题目 选择排序 冒泡排序 插入排序 希尔排序 快速排序 归并排序 堆排序

算法,其实算术题的解法,如同我们读书时做的数学题。一道关于排序的算术题,有几种解法就有几种思路。一般程序员可能一辈子都用不上排序算法,但是我们可以学习其中的解题思路,融会贯通后,可以对我们实际开发有指导作用。

题目

 数量为10的无序数组,将其按照升序排列。例如

排序算法学习
前言
题目
选择排序
冒泡排序
插入排序
希尔排序
快速排序
归并排序
堆排序

选择排序

如果这样的数组放在面前,要你手工排序,一般人都是先浏览一遍,找出最小的19拿走,再浏览剩下的,再拿走最小的22,直到全部取走,到手的就是有序集合。其实这种做法就是选择排序法。

当然,程序实现上为了节省内存,会用交换代替取的。如

public class SelectSort {
    public static int[] sort(int[] sourceArray) {
        int[] array = Arrays.copyOf(sourceArray, sourceArray.length);
        int length = array.length;
        for (int i = 0; i < length-1; i++) {
            int min = i;
            for (int j = i + 1; j < length; j++) {
                if (array[min] > array[j]) {
                    min = j;
                }
            }
            swap(array,i,min);
        }
        return array;
    }

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

冒泡排序

冒泡排序形象点描述是用泡泡装着最右的元素,不断向左前进比较,泡泡里面的元素较大就交换,较小就继续前进,直到到达最左。

public class BubbleSort {
    public static int[] sort(int[] sourceArray) {
        int[] array = Arrays.copyOf(sourceArray, sourceArray.length);
        int length = array.length;
        for (int i = 0; i < length; i++) {
            for (int j = length - 1; j > i; j--) {
                if (array[j] < array[j - 1]) {
                    swap(array, j, j - 1);
                }
            }
        }
        return array;
    }
    static void swap(int[] array, int i, int j) {
        int temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }
}

插入排序

插入排序的主要思路是将数组分成有序和无序两虚拟集合。

初始状态时,38是有序集合,19到66都是无序集合。

排序算法学习
前言
题目
选择排序
冒泡排序
插入排序
希尔排序
快速排序
归并排序
堆排序

然后将无序集合的第一个元素19存起来,从右往左对比有序集合中的元素,如

排序算法学习
前言
题目
选择排序
冒泡排序
插入排序
希尔排序
快速排序
归并排序
堆排序

和有序集合中的38比较,38较大,移动38到下一个位置,当19走到有序集合的尽头或者遇到比19小的元素时,插入19,如

排序算法学习
前言
题目
选择排序
冒泡排序
插入排序
希尔排序
快速排序
归并排序
堆排序

现在19和38是有序集合的元素,其余的都是无序集合,循环上述操作,直到无序集合的最后一个元素。代码实现如下

public class InsertSort {
    public static int[] sort(int[] sourceArray) {
        int[] array = Arrays.copyOf(sourceArray, sourceArray.length);
        int length = array.length;
        for (int i = 1; i < length; i++) {
            int tmp = array[i];
            int index = i;
            for (int j = i - 1; j >= 0; j--) {
                if (tmp < array[j]) {
                    array[j + 1] = array[j];
                    index = j;
                } else {
                    break;
                }
            }
            array[index] = tmp;
        }
        return array;
    }
}

希尔排序

上面的插入排序是通过不断移动较大的元素后移以达到排序的目的,例如4要移动最前面,要移动5次前面的元素。如果要优化插入排序的话,可以从减少移动次数下手,所以希尔排序应运而生。

 希尔排序引入步长的概念,将数组分成数个虚拟数组,例如步长为10/2=5,那么分组后如下,同色的为一组

排序算法学习
前言
题目
选择排序
冒泡排序
插入排序
希尔排序
快速排序
归并排序
堆排序

然后每一组使用插入排序法排序后,如

排序算法学习
前言
题目
选择排序
冒泡排序
插入排序
希尔排序
快速排序
归并排序
堆排序

然后再缩小步长,例如步长为5/2向下取整2,分成

排序算法学习
前言
题目
选择排序
冒泡排序
插入排序
希尔排序
快速排序
归并排序
堆排序

再对这两组进行插入排序,如

排序算法学习
前言
题目
选择排序
冒泡排序
插入排序
希尔排序
快速排序
归并排序
堆排序

缩小步长,步长为2/1=1,这个时候就相当于普通的插入排序。

代码实现上,和插入排序相似,只不过再最外层加多一个缩小步长的循环,直到步长小于1,如

public class ShellSort {
    public static int[] sort(int[] sourceArray) {
        int[] array = Arrays.copyOf(sourceArray, sourceArray.length);
        int gap = array.length/2;
        while (gap >= 1) {
            for (int i = gap; i< array.length; i++) {
                int tmp = array[i];
                int index = i;
                for (int j = i-gap; j >= 0; j = j - gap) {
                    if (tmp < array[j]) {
                        array[j + gap] = array[j];
                        index = j;
                    } else {
                        break;
                    }
                }
                array[index] = tmp;
            }
            gap = gap / 2;
        }
        return array;
    }
}

快速排序

顾名思义,在一般情况下,快速排序是速度最快的排序。

它的解题思路是选一个基准值出来,例如数组第一个位置38,通过移动位置分成三个虚拟数组,左边是小于等于38的,中间是38,右边是大于等于38的。

首先先从右边遍历和对比大小,直到找到小于38的值停下,然后从左边遍历,直到找到大于38的停下,如

排序算法学习
前言
题目
选择排序
冒泡排序
插入排序
希尔排序
快速排序
归并排序
堆排序

这个时候交换左右位置,如下

排序算法学习
前言
题目
选择排序
冒泡排序
插入排序
希尔排序
快速排序
归并排序
堆排序

右边继续遍历,找到小于38的21值停下来,左边开始遍历,到21的位置时,发现左边等于右边,也停下来

排序算法学习
前言
题目
选择排序
冒泡排序
插入排序
希尔排序
快速排序
归并排序
堆排序

这个时候,除了左右到达的位置,左边走过的路都是小于等于38的,右边走过的路都是都是大于等于38,那么只要将基准值和21交换就完成三个虚拟数组的划分了

排序算法学习
前言
题目
选择排序
冒泡排序
插入排序
希尔排序
快速排序
归并排序
堆排序

之后,将左右数组重复上面步骤,最终会得到一个有序数组。

public class QuickSort {

    public static int[] sort(int[] sourceArray) {
        int[] array = Arrays.copyOf(sourceArray, sourceArray.length);
        quickSort(array, 0, sourceArray.length - 1);
        return array;
    }

    static void quickSort(int[] array, int left, int right) {
        if (left >= right) {
            return;
        }
        int low = left;
        int high = right;
        int tmp = array[left];

        while (left < right) {
            while (left < right && array[right] >= tmp) {
                right--;
            }
            while (left < right && array[left] <= tmp) {
                left++;
            }
            if (left != right) {
                swap(array, left, right);
            }
        }
        swap(array, low, left);
        quickSort(array, low, left - 1);
        quickSort(array, left + 1, high);
    }

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

归并排序

归并排序常见于数据库排序,将扫描的数据生成多个磁盘文件,再归并排序。

它的主要思路比较简单粗暴,就是是将数组通过递归地方式拆分到单个元素,再通过不断两两合并成为有序数组。

我觉得它的精髓在于递归方式的运用,发散性地递归。

public class MergeSort {
    public static int[] sort(int[] sourceArray) {
        int[] array = Arrays.copyOf(sourceArray, sourceArray.length);
        int length = array.length;
        if (length == 1) {
            return array;
        }
        int middle = (int) Math.floor(length / 2);

        int[] leftArray = Arrays.copyOfRange(sourceArray, 0, middle);
        int[] rightArray = Arrays.copyOfRange(sourceArray, middle, length);

        return merge(sort(leftArray), sort(rightArray));
    }

    static int[] merge(int[] leftArray, int[] rightArray) {
        int[] array = new int[leftArray.length + rightArray.length];
        int i = 0, j = 0, k = 0;
        while (i < leftArray.length && j < rightArray.length) {
            if (leftArray[i] < rightArray[j]) {
                array[k++] = leftArray[i++];
            } else {
                array[k++] = rightArray[j++];
            }
        }
        while (i < leftArray.length) {
            array[k++] = leftArray[i++];
        }
        while (j < rightArray.length) {
            array[k++] = rightArray[j++];
        }
        return array;
    }
}

堆排序

每一个数组都可以看成一颗完全二叉树,特点是每一个非叶子节点i,左子节点在数组的2i+1位置上,右节点在2i+2位置。简单来说,将数组从上往下从左往右铺满,可以看到非叶子节点都是在数组的前面,如下

排序算法学习
前言
题目
选择排序
冒泡排序
插入排序
希尔排序
快速排序
归并排序
堆排序

而堆也是完全二叉树的一种,分为最大堆和最小堆,最大堆的特点是每一个父节点都大于等于子节点,所以在根节点的元素在数组的第一个位置,同时也是最大值。

所以堆排序的解题思路就是,从最后一个非叶子节点遍历,比较子节点大小,交换最大值位置,就这样将数组调整成最大堆,拿走根节点放在数组的末尾,第二次将剩余的元素继续调整成堆,拿走根节点放在数组的次末尾,重复上述操作,直到剩余元素只有一个。

public class HeapSort {

    public static int[] sort(int[] sourceArray) {
        int[] array = Arrays.copyOf(sourceArray, sourceArray.length);

        for (int i = array.length - 1; i > 0; i--) {
            buildMaxHeap(array, i);
            swap(array, 0, i);
        }
        return array;
    }

    static void buildMaxHeap(int[] array, int lastLeaf) {
        //计算最后一个非叶子节点的位置
        int lastNonLeaf;
        if (lastLeaf % 2 == 1) {
            lastNonLeaf = (lastLeaf - 1) / 2;
        } else {
            lastNonLeaf = lastLeaf / 2 - 1;
        }
        for (int i = lastNonLeaf; i >= 0; i--) {
            int left = 2 * i + 1;
            int right = 2 * i + 2;
            if (array[left] > array[i]) {
                swap(array, left, i);
            }
            if (right <= lastLeaf && array[right] > array[i]) {
                swap(array, right, i);
            }
        }
    }

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

测试代码  https://github.com/mycaizilin/sort