Algorithm -- 排序算法

单链表

  • 十大经典排序算法
  • 冒泡排序
  • 选择排序
  • 插入排序
  • 归并排序
  • 快速排序
  • 堆排序
  • 计数排序
  • 桶排序
图片来源于网络

1. 十大经典排序算法

十大经典排序算法对比如下:

排序算法对比
  • 稳定性的定义
    排序后 2 个相等键值的顺序和排序之前它们的顺序相同。
    例如:在原序列中,A1=A2,且A1A2之前,在排序后的序列中,A1仍在A2之前,则称这种排序算法是稳定的;否则称为不稳定的。

  • 稳定性的意义
    若只是简单的进行数字排序,那么稳定性无意义。
    排序的内容是一个复杂对象的多个数字属性,且原本的初始顺序存在意义,稳定性的算法可以在二次排序的基础上保持原有排序的意义。
    例如:一组原本按价格高低排序的对象,现需要按销量高低排序,使用稳定性算法,可以使相同销量的对象依旧保持着价格高低的排序展现,只有销量不同的才会重新排序。

2. 冒泡排序

    /**
     * 冒泡排序 平均时间复杂度O(n²),最坏时间复杂度O(n²),最好时间复杂度O(n),空间O(1),稳定
     * 1. 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
     * 2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
     * 3. 针对所有的元素重复以上的步骤,除了最后一个。
     * 4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
     */
    private void bubbleSort(int[] array) {
        for (int i = 0; i < array.length; i++) {
            for (int j = 0; j < array.length - i - 1; j++) {
                int temp;
                if (array[j] > array[j + 1]) {
                    temp = array[j];
                    array[j] = array[j + 1];
                    array[j + 1] = temp;
                }
            }
        }
    }

可加个判断优化上面的算法:

    private void bubbleSort(int[] array) {
        for (int i = 0; i < array.length - 1; i++) {
            boolean sorted = true;
            for (int j = 0; j < array.length - i - 1; j++) {
                int temp;
                if (array[j] > array[j + 1]) {
                    temp = array[j];
                    array[j] = array[j + 1];
                    array[j + 1] = temp;
                    sorted = false;
                }
            }
            if (sorted) break;
        }
    }

保存循环的最后一次交换下标,进一步优化:

    private void bubbleSort(int[] array) {
        int lastExchangeIndex = 0;
        int sortBorder = array.length - 1;
        for (int i = 0; i < array.length - 1; i++) {
            boolean sorted = true;
            for (int j = 0; j < sortBorder; j++) {
                int temp;
                if (array[j] > array[j + 1]) {
                    temp = array[j];
                    array[j] = array[j + 1];
                    array[j + 1] = temp;
                    sorted = false;
                    lastExchangeIndex = j;
                }
            }
            sortBorder = lastExchangeIndex;
            if (sorted) break;
        }
    }

或如下方式实现:

void bubbleSort(int[] arr) {
    for(int end = arr.length - 1; end > 0; end--) {
        for(int j = 0; j < end; j++) {
            if(arr[j] > arr[j+1]) swap(arr, j, j+1);
        }
    }
}

3. 选择排序

    /**
     * 简单选择排序 平均时间复杂度O(n²),最坏时间复杂度O(n²),最好时间复杂度O(n²),空间O(1),不稳定
     * 1. 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。
     * 2. 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
     * 3. 重复第二步,直到所有元素均排序完毕。
     */
    private void selectSort(int[] array) {
        int len = array.length;
        int minIndex, temp;
        for (int i = 0; i < len - 1; i++) {
            minIndex = i;
            for (int j = i + 1; j < len; j++) {
                if (array[j] < array[minIndex]) { // 寻找最小的数
                    minIndex = j; // 将最小数的索引保存
                }
            }
            temp = array[i];
            array[i] = array[minIndex];
            array[minIndex] = temp;
        }
    }

或如下方式实现:

// 每次排序时把最小的放在前面
void selectsort(int[] arr) {
    for(int i = 0; i < arr.length; i++) {
        // 最小值在哪个位置上
        int minIndex = i;
        for(int j = i + 1; j < arr.length; j++) {
            minIndex = arr[j] < arr[minIndex] ? j : minIndex;
        }
        swap(arr, i, minIndex);
    }
}

4. 插入排序

    /**
     * 简单插入排序:平均时间复杂度O(n²),最坏时间复杂度O(n²),最好时间复杂度O(n),空间O(1),稳定
     * 1. 将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。
     * 2. 从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。
     * (如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。)
     */
    private void insertSort(int[] array) {
        int len = array.length;
        for (int i = 1; i < len; i++) {
            int temp = array[i]; // 记录要插入的数据

            // 从已经排序的序列最右边的开始比较,找到比其小的数
            int j = i;
            while (j > 0 && array[j - 1] > temp) {
                array[j] = array[j - 1];
                j--;
            }
            // 存在比其小的数,插入
            if (j != i) {
                array[j] = temp;
            }
        }
    }

或如下方式实现:

void insertSort(int[] arr) {
    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);
        }
    }
}

5. 归并排序

    /**
     * 归并排序:采用分治法  平均时间复杂度O(nlogn) ,最坏时间复杂度O(nlogn) ,最好时间复杂度O(nlogn) ,空间O(n),稳定
     * 1.申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
     * 2.设定两个指针,最初位置分别为两个已经排序序列的起始位置;
     * 3.比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
     * 4.重复步骤 3 直到某一指针达到序列尾;
     * 5.将另一序列剩下的所有元素直接复制到合并序列尾。
     */
    private int[] mergeSort(int[] array) {
        int middle = (int) Math.floor(array.length / 2);
        int[] left = Arrays.copyOfRange(array, 0, middle);
        int[] right = Arrays.copyOfRange(array, middle, array.length);

        return merge(mergeSort(left), mergeSort(right));
    }

    private int[] merge(int[] left, int[] right) {
        int[] res = new int[left.length + right.length];
        int i = 0;
        while (left.length > 0 && right.length > 0) {
            if (left[0] <= right[0]) {
                res[i++] = left[0];
                left = Arrays.copyOfRange(left, 1, left.length);
            } else {
                res[i++] = right[0];
                right = Arrays.copyOfRange(right, 1, right.length);
            }
        }

        while (left.length > 0) {
            res[i++] = left[0];
            left = Arrays.copyOfRange(left, 1, left.length);
        }

        while (right.length > 0) {
            res[i++] = right[0];
            right = Arrays.copyOfRange(right, 1, right.length);
        }

        return res;
    }

或用如下方式实现:

// 通过递归实现
public void mergeSort(int[] arr) {
    if(arr == null || arr.length < 2) return;
    process(arr, 0, arr.length - 1);
}

// arr[L..R] 范围上,变成有序的
private void process(int[] arr, int L, int R) {
    if(L == R) return;
    int mid = L + ((R - L) >> 1);
    process(arr, L, mid);
    process(arr, mid + 1, R);
    merge(arr, L, mid, R);
}

private void merge(int[] arr, int L, int M, int R) {
    // L..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++];
    }
    while(p1 <= M) {
        help[i++] = arr[p1++];
    }
    while(p2 <= R) {
        help[i++] = arr[p2++];
    }
    for(i = 0; i < help.length; i++) {
        arr[L + i] = help[i];
    }
}

非递归方法实现:

public void mergerSort(int[] arr) {
    if (arr == null || arr.length < 2) return;
    int N = arr.length;
    int mergeSize = 1;// 当前有序的左组长度,一组是2倍的mergeSize
    while(mergeSize < N) {
        int L = 0;
        while(L < N) {
            int M = L + mergeSize - 1; // L..M
            if(M >= N) break;
            int R = Math.min(M + mergeSize, N - 1); // L..M M+1..R
            merge(arr, L, M, R);
            L = R + 1;
        }
        if(mergeSize > N / 2) break; // 防止溢出
        mergeSize <<= 1; // mergeSize * 2
    }
}

6. 快速排序

    /**
     * 快速排序:平均时间复杂度O(nlogn) ,最坏时间复杂度O(n²) ,最好时间复杂度O(nlogn) ,空间O(logn),不稳定
     * 1.从数列中挑出一个元素,称为 "基准"(pivot);
     * 2.重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。
     * 在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
     * 3.递归地把小于基准值元素的子数列和大于基准值元素的子数列排序;
     */
    private int[] quickSort(int[] array, int left, int right) {
        if (left < right) {
            int partitionIndex = partition(array, left, right);
            quickSort(array, left, partitionIndex - 1);
            quickSort(array, partitionIndex + 1, right);
        }
        return array;
    }

    // 分治(单边循环法)确定基准元素
    private int partition(int[] arr, int left, int right) {
        // 设定基准值
        int pivot = left;
        int index = pivot + 1;
        for (int i = index; i <= right; i++) {
            if (arr[i] < arr[pivot]) {
                swap(arr, i, index);
                index++;
            }
        }
        swap(arr, pivot, index - 1);
        return index - 1;
    }

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

或用如下方式实现:

    // 以 arr[R] 为划分值
    // [ <=arr[R]   arr[R]  >arr[R] ]
    public static int partition(int[] arr, int L, int R) {
        if (L > R) return -1;
        if (L == R) return L;
        int lessEqual = L - 1;
        int index = L;
        while (index < R) {
            if (arr[index] <= arr[R]) {
                swap(arr, index, ++lessEqual);
            }
            index++;
        }
        swap(arr, ++lessEqual, R);
        return lessEqual;
    }

    /**
     * 快排
     * 时间复杂度:最差 O(N^2)
     */
    public static void quickSort1(int[] arr) {
        if (arr == null || arr.length < 2) return;
        process1(arr, 0, arr.length - 1);
    }

    public static void process1(int[] arr, int L, int R) {
        if (L > R) return;
        int M = partition(arr, L, R);
        process1(arr, L, M - 1);
        process1(arr, M + 1, R);
    }

或利用荷兰国旗问题:

    // 在 arr[L..R] 玩荷兰国旗问题的划分,以 arr[R] 做划分值
    //  <arr[R]   ==arr[R]  >arr[R]
    // 返回等于区域的左右边界位置
    // 如 [1,2, 3,3, 5,4] 返回3的区域左右边界下标:[2,3]
    public static int[] netherlandsFlag(int[] arr, int L, int R) {
        if (L > R) return new int[]{-1, -1};
        if (L == R) return new int[]{L, R}; // 说明区域只有一个数
        int less = L - 1; // 小于区域右边界
        int more = R; // 大于区域左边界
        int index = L;
        while (index < more) {
            if (arr[index] == arr[R]) {
                index++;
            } else if (arr[index] < arr[R]) {
                // 把当前位置和小于区域做交换,并右移增加小于区域右边界
                // 同时下标index++
                swap(arr, index++, ++less);
            } else {
                // 把当前位置和大于区域做交换,并左移扩大大于区域左边界
                // index不变
                swap(arr, index, --more);
            }
        }

        // 此时:L..less  less+1..more-1  more...R-1  R
        // 最后把划分值 arr[R] 和大于区域的左边界交换,使得划分值在 ==arr[R] 右边相邻
        swap(arr, more, R);
        return new int[]{less + 1, more};
    }

    public static void quickSort2(int[] arr) {
        if (arr == null || arr.length < 2) return;
        process2(arr, 0, arr.length - 1);
    }

    public static void process2(int[] arr, int L, int R) {
        if (L > R) return;
        // 中间相等的区域不需要排了
        int[] equalArea = netherlandsFlag(arr, L, R);
        process2(arr, L, equalArea[0] - 1);
        process2(arr, equalArea[1] + 1, R);
    }

经典随机快排如下:

    /**
     * 经典随机快排
     * 时间复杂度: O(NlogN)
     * 空间复杂度: O(logN)
     */
    public static void quickSort3(int[] arr) {
        if (arr == null || arr.length < 2) return;
        process3(arr, 0, arr.length - 1);
    }

    public static void process3(int[] arr, int L, int R) {
        if (L > R) return;
        // 随机选个位置,不以 arr[R] 为划分值
        swap(arr, L + (int) (Math.random() * (R - L + 1)), R);
        // 中间相等的区域不需要排了
        int[] equalArea = netherlandsFlag(arr, L, R);
        process3(arr, L, equalArea[0] - 1);
        process3(arr, equalArea[1] + 1, R);
    }

7. 堆排序

    /**
     * 堆排序:平均时间复杂度O(nlogn) ,最坏时间复杂度O(nlogn) ,最好时间复杂度O(nlogn) ,空间O(1),不稳定
     * <p>
     * 堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。
     * 堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点.
     * <p>
     * 大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列;
     * 小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列;
     * <p>
     * 1.创建一个堆 H[0……n-1];
     * 2.把堆首(最大值)和堆尾互换;
     * 3.把堆的尺寸缩小 1,并调用 shift_down(0),目的是把新的数组顶端数据调整到相应位置;
     * 4.重复步骤 2,直到堆的尺寸为 1
     */
    private void heapSort(int[] array) {
        int len = array.length;
        buildMapHeap(array, len);
        for (int i = len - 1; i > 0; i--) {
            swap(array, 0, i);
            len--;
            heapify(array, 0, len);
        }
    }

    private void buildMapHeap(int[] arr, int len) {
        for (int i = (int) Math.floor(len / 2); i >= 0; i--) {
            heapify(arr, i, len);
        }
    }

    // 堆化
    private void heapify(int[] arr, int i, int len) {
        int left = 2 * i + 1;
        int right = 2 * i + 2;
        int largest = i;

        if (left < len && arr[left] > arr[largest]) {
            largest = left;
        }

        if (right < len && arr[right] > arr[largest]) {
            largest = right;
        }

        if (largest != i) {
            swap(arr, i, largest);
            heapify(arr, largest, len);
        }
    }

8. 计数排序

    /**
     * 计数排序:平均时间复杂度O(n+k) ,最坏时间复杂度O(n+k) ,最好时间复杂度O(n+k) ,空间O(n+k),稳定
     * <p>
     * 核心是将输入的数据值转化为键存储在额外开辟的数组空间中。
     * <p>
     * 1.找出待排序的数组中最大和最小的元素
     * 2.统计数组中每个值为i的元素出现的次数,存入数组C的第i项
     * 3.对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加)
     * 4.反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1
     */
    private void countingSort(int[] array) {
        int maxValue = getMaxValue(array);

        int bucketLen = maxValue + 1; // 计数桶长度
        int[] bucket = new int[bucketLen];
        // 初始化计数桶
        for (int value : array) {
            bucket[value]++;
        }

        int sortedIndex = 0;
        for (int j = 0; j < bucketLen; j++) {
            while (bucket[j] > 0) {
                array[sortedIndex++] = j;
                bucket[j]--;
            }
        }
    }

    private int getMaxValue(int[] arr) {
        int maxValue = arr[0];
        for (int value : arr) {
            if (maxValue < value) {
                maxValue = value;
            }
        }
        return maxValue;
    }

9. 桶排序

    /**
     * 桶排序:平均时间复杂度O(n+k) ,最坏时间复杂度O(n²) ,最好时间复杂度O(n) ,空间O(n+k),稳定
     * <p>
     * 计数排序的升级版。
     * 它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定.
     * <p>
     * 1.在额外空间充足的情况下,尽量增大桶的数量
     * 2.使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中
     */
    private void bucketSort(int[] array) {
        int bucketSize = 5;

        // 寻找最大最小数
        int minValue = array[0];
        int maxValue = array[0];
        for (int value : array) {
            if (value < minValue) {
                maxValue = value;
            } else if (value > maxValue) {
                maxValue = value;
            }
        }

        // 初始化桶
        int bucketCount = (int) Math.floor((maxValue - maxValue) / bucketSize) + 1;
        int[][] buckets = new int[bucketCount][0];

        // 利用映射函数将数据分配到各个桶中
        for (int i = 0; i < array.length; i++) {
            int index = (int) Math.floor((array[i] - minValue) / bucketSize);
            buckets[index] = arrAppend(buckets[index], array[i]);
        }

        int arrIndex = 0;
        for (int[] bucket : buckets) {
            if (bucket.length <= 0) continue;
            // 对每个桶进行排序,这边排序可用前面介绍过的排序算法
            insertSort(bucket);
            for (int value : bucket) {
                array[arrIndex++] = value;
            }
        }
    }

    /**
     * 自动扩容,并保存数据
     */
    private int[] arrAppend(int[] arr, int value) {
        arr = Arrays.copyOf(arr, arr.length + 1);
        arr[arr.length - 1] = value;
        return arr;
    }

参考链接:

十大经典排序算法

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,014评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,796评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,484评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,830评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,946评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,114评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,182评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,927评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,369评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,678评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,832评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,533评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,166评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,885评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,128评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,659评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,738评论 2 351

推荐阅读更多精彩内容