排序算法之冒泡,选择,插入和希尔

1,753 阅读8分钟

冒泡排序(Bubble Sort)

冒泡排序的核心部分是双重嵌套循环,持续比较相邻元素,大的挪到后面,因此大的会逐步往后挪,故称之为冒泡。

算法思路

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

图片源自Visualgo

实现

Java

public class BubbleSort {

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

    }

    private static void bubbleSort(int[] array) {
        if (array == null || array.length == 0) { // 非法检查
            return;
        }

        int i, temp, len = array.length;
        boolean changed;
        
        do {
            changed = false;
            len -= 1;
            for (i = 0; i < len; i++) {
                if (arr[i] > arr[i + 1]) {
                    temp = arr[i];
                    arr[i] = arr[i + 1];
                    arr[i + 1] = temp;
                    changed = true;
                }
            }
        } while (changed);
    }
}


Python

#!/usr/bin/env python
# coding=utf-8


def bubble_sort(arrayList):
    length = len(arrayList)

    for i in range(length - 1):
        count = 0

        for j in range(length - 1 - i):
            if (arrayList[j] > arrayList[j + 1]):
                arrayList[j], arrayList[j + 1] = arrayList[j + 1], arrayList[j]
                count += 1

        if count == 0:
            break


if __name__ == "__main__":
    arrayList = [6, 5, 3, 1, 8, 7, 2, 4]
    print("orgin array list: {0}".format(arrayList))
    bubble_sort(arrayList)
    print("after sorted list: {0}".format(arrayList))

时间复杂度和空间复杂度

最好情况下:正序有序,则只需要比较n次。故为 O(n)

最坏情况下:逆序有序,则需要比较 (n-1)+(n-2)+……+1,故为 O(n^2)

因为需要一个临时变量来交换元素位置,(另外遍历序列时自然少不了用一个变量来做索引),所以其空间复杂度为 O(1)

稳定性

排序过程中只交换相邻两个元素的位置。因此,当两个数相等时,是没必要交换两个数的位置的。所以它们的相对位置并没有改变,冒泡排序算法是稳定的。

总结

如果有 n 个数进行排序,只需将 n - 1 个数归位,也就是说要进行 n - 1 趟操作。而“每一趟”都需要从第 1 位开始进行相邻两个数的比较,将较小的一个数放在后面,比较完毕后向后挪一位继续比较下面相邻数的大小,重复此步骤,直到最后一个尚未归位的数,已经归位的数则无需再进行比较。

冒泡排序的核心部分是双重嵌套循环,不难看出冒泡排序的时间复杂度是 O(n^2)。这是一个非常高的时间复杂度。所以说冒泡排序除了它迷人的名字之外,似乎没有什么值得推荐的

选择排序(Selection Sort)

选择排序就是找到数组中最小元素将其和数组第一个元素交换位置,然后在剩下的元素中找到最小元素并将其与数组第二个元素进行交换,以此类推,直至整个数组排序结束。

算法思路

  1. 找到数组中最小元素并将其和数组第一个元素交换位置
  2. 在剩下的元素中找到最小元素并将其与数组第二个元素交换,直至整个数组排序

图片源自Visualgo

实现

Java

public class SelectionSort {

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

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

        int length = array.length;
        int min;
        int temp;
        for (int i = 0; i < length - 1; i++) {

            min = i;
            for (int j = length - 1; j > i; j--) {
                if (array[min] > array[j]) {
                    min = j;
                }
            }

            if (array[i] > array[min]) {
                temp = array[i];
                array[i] = array[min];
                array[min] = temp;
            }
        }
    }
}

Kotlin

object SelectionSortKt {

    fun selectionSort(array: IntArray) {
        if (array.isEmpty()) {
            return
        }

        val size = array.size
        var min: Int

        for (i in 0 until size - 1) {

            min = i
            (size - 1 downTo i)
                    .asSequence()
                    .filter { array[min] > array[it] }
                    .forEach { min = it }

            if (array[min] < array[i]) {
                val temp = array[i]
                array[i] = array[min]
                array[min] = temp
            }
        }
    }
}

fun main(args: Array<String>) {
    val unsortedArray = intArrayOf(6, 5, 3, 1, 8, 7, 2, 4)
    SelectionSortKt.selectionSort(unsortedArray)
    println("After sorted: ")
    unsortedArray.forEach {
        print(" $it")
    }
}

Python3

#!/usr/bin/env python
# coding=utf-8


def selection_sort(arrayList):
    length = len(arrayList)
    minIndex = 0

    for i in range(length - 1):
        minIndex = i

        for j in range(length - 1, i, - 1):
            if (arrayList[minIndex] > arrayList[j]):
                minIndex = j

        if (arrayList[i] > arrayList[minIndex]):
            arrayList[i], arrayList[minIndex] = arrayList[minIndex], arrayList[
                i]


if __name__ == "__main__":
    arrayList = [6, 5, 3, 1, 8, 7, 2, 4]
    print("orgin array list: {0}".format(arrayList))
    selection_sort(arrayList)
    print("after sorted list: {0}".format(arrayList))

时间复杂度

最好情况:交换 0 次,但是每次都要找到最小的元素,因此大约必须遍历 N*N 次,因此为 O(N^2)

最坏情况 & 平均情况下:O(N^2)

稳定性

由于每次都是选取未排序序列 A 中的最小元素 x 与 A 中的第一个元素交换,因此跨距离了,很可能破坏了元素间的相对位置,因此选择排序是不稳定的

总结

选择排序是和冒泡排序差不多的一种排序。和冒泡排序交换相连数据不一样的是,选择排序只有在确定了最小的数据之后,才会发生交换

插入排序(Insertion Sort)

通过构建有序序列,对于未排序序列,从后向前扫描(对于单向链表则只能从前往后遍历),找到相应位置并插入。实现上通常使用 in-place 排序(需用到 O(1) 的额外空间)

算法思路

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

图片源自Visualgo

实现

Java

public class InsertionSort {

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

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

        int length = array.length;
        int i,j;
        for (i = 1; i < length; i ++) {

            int holder = array[i];

            for (j = i; j > 0 && array[j - 1] > holder; j--) {
                    array[j] = array[j - 1];
            }

            // 以下是错误的代码逻辑,因为必须是满足 array[j - 1] > holder 才应该 j--
            // 下面代码是每次都会执行 j--
//            for (j = i; j > 0; j--) {
//                if (array[j - 1] > holder) {
//                    array[j] = array[j - 1];
//                }
//            }

            array[j] = holder;
        }
    }
}

Kotlin

object InsertionSortKt {

    fun insertionSort(array: IntArray) {
        if (array.isEmpty()) {
            return
        }

        val size = array.size
        for (i in 1 until size) {

            val holder = array[i]
            var j = i
            while (j > 0 && array[j - 1] > holder) {
                array[j] = array[j - 1]
                j--
            }

            array[j] = holder
        }
    }
}

fun main(args: Array<String>) {
    val unsortedArray = intArrayOf(6, 5, 3, 1, 8, 7, 2, 4)
    InsertionSortKt.insertionSort(unsortedArray)
    println("After sorted: ")
    unsortedArray.forEach {
        print(" $it")
    }
}

Python3

#!/usr/bin/env python
# coding=utf-8


def insertion_sort(arrayList):
    length = len(arrayList)

    for i in range(1, length):
        holder = arrayList[i]
        j = i
        while(j > 0 and arrayList[j - 1] > holder):
            arrayList[j] = arrayList[j - 1]
            j -= 1

        arrayList[j] = holder


if __name__ == "__main__":
    arrayList = [6, 5, 3, 1, 8, 7, 2, 4]
    print("orgin array list: {0}".format(arrayList))
    insertion_sort(arrayList)
    print("after sorted list: {0}".format(arrayList))

时间复杂度

最好的情况:正序有序(从小到大),这样只需要比较 n 次,不需要移动。因此时间复杂度为 O(n)

最坏的情况:逆序有序,这样每一个元素就需要比较 n 次,共有 n 个元素,因此实际复杂度为 O(n^2)

平均情况:O(n^2)

希尔排序

核心:基于插入排序,使数组中任意间隔为 h 的元素都是有序的,即将全部元素分为 h 个区域使用插入排序。其实现可类似于插入排序但使用不同增量。更高效的原因是它权衡了子数组的规模和有序性

希尔排序是基于插入排序的以下两点性质而提出改进方法的:

  • 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率
  • 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位

算法实现

通过将比较的全部元素分为几个区域来提升插入排序的性能。这样可以让一个元素可以一次性地朝最终位置前进一大步。然后算法再取越来越小的步长进行排序,算法的最后一步就是普通的插入排序,但是到了这步,需排序的数据几乎是已排好的了(此时插入排序较快)

假设有数组 array = [80, 93, 60, 12, 42, 30, 68, 85, 10],首先取 d1 = 4,将数组分为 4 组,如下图中相同颜色代表一组:

然后分别对 4 个小组进行插入排序,排序后的结果为:

然后,取 d2 = 2,将原数组分为 2 小组,如下图:

然后分别对 2 个小组进行插入排序,排序后的结果为:

最后,取 d3 = 1,进行插入排序后得到最终结果:

图片摘自常见排序算法 - 希尔排序 (Shell Sort)

步长序列

步长的选择是希尔排序的重要部分。只要最终步长为1任何步长序列都可以工作。算法最开始以一定的步长进行排序。然后会继续以一定步长进行排序,最终算法以步长为1进行排序。当步长为1时,算法变为插入排序,这就保证了数据一定会被排序。

以下是性能比较好的步长序列

  1. Shell's original sequence: N/2 , N/4 , ..., 1 (repeatedly divide by 2);
  2. Hibbard's increments: 1, 3, 7, ..., 2k - 1 ;
  3. Knuth's increments: 1, 4, 13, ..., (3k - 1) / 2 ;
  4. Sedgewick's increments: 1, 5, 19, 41, 109, ....
    It is obtained by interleaving the elements of two sequences:
    1, 19, 109, 505, 2161,….., 9(4k – 2k) + 1, k = 0, 1, 2, 3,…
    5, 41, 209, 929, 3905,…..2k+2 (2k+2 – 3 ) + 1, k = 0, 1, 2, 3, …

已知的最好步长序列是由Sedgewick提出的(1, 5, 19, 41, 109,...)。这项研究也表明比较在希尔排序中是最主要的操作,而不是交换。用这样步长序列的希尔排序比插入排序要快,甚至在小数组中比快速排序和堆排序还快,但是在涉及大量数据时希尔排序还是比快速排序慢。

实现

Java

public class ShellSort {

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

    }

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

        int length = array.length;

        int gap = length / 2;
        int i, j;

        for (; gap > 0; gap /= 2) { // Shell's original sequence: N/2 , N/4 , ..., 1 (repeatedly divide by 2)
            for (i = gap; i < length; i += gap) {

                int temp = array[i];
                for (j = i; j > 0 && array[j - gap] > temp; j -= gap) {
                    array[j] = array[j - gap];
                }

                array[j] = temp;
            }
        }
    }
}

Kotlin

object ShellSortKt {

    fun shellSort(array: IntArray) {

        if (array.isEmpty()) {
            return
        }

        val size = array.size
        var gap = size / 2

        while (gap > 0) {

            for (i in gap until size step gap) {

                val temp = array[i]
                var j = i

                while (j > 0 && array[j - gap] > temp) {
                    array[j] = array[j - gap]
                    j -= gap
                }

                array[j] = temp
            }

            gap /= 2
        }
    }
}

fun main(args: Array<String>) {
    val unsortedArray = intArrayOf(6, 5, 3, 1, 8, 7, 2, 4)
    ShellSortKt.shellSort(unsortedArray)
    println("After sorted: ")
    unsortedArray.forEach {
        print(" $it")
    }
}

Python3

#!/usr/bin/env python
# coding=utf-8


def shell_sort(arrayList):
    length = len(arrayList)

    gap = length // 2
    while (gap > 0):

        for i in range(gap, length, gap):
            holder = arrayList[i]
            j = i
            while (j > 0 and arrayList[j - gap] > holder):
                arrayList[j] = arrayList[j - gap]
                j -= gap

            arrayList[j] = holder

        gap //= 2


if __name__ == "__main__":
    arrayList = [6, 5, 3, 1, 8, 7, 2, 4]
    print("orgin array list: {0}".format(arrayList))
    shell_sort(arrayList)
    print("after sorted list: {0}".format(arrayList))

总结

一个更好理解的希尔排序实现:将数组列在一个表中并对列排序(用插入排序)。重复这过程,不过每次用更长的列来进行。最后整个表就只有一列了。将数组转换至表是为了更好地理解这算法,算法本身仅仅对原数组进行排序(通过增加索引的步长,例如是用 i += step_size 而不是 i++)

参考