排序算法

冒泡排序

简介

冒泡排序(Bubble sort)是一种简单的排序算法。由于在算法的执行过程中,较小的元素像是气泡般慢慢「浮」到数列的顶端,故叫做冒泡排序。

它的工作原理是每次检查相邻两个元素,如果前面的元素与后面的元素满足给定的排序条件,就将相邻两个元素交换。当没有相邻的元素需要交换时,排序就完成了。

经过 \(i\) 次扫描后,数列的末尾 \(i\) 项必然是最大的 \(i\) 项,因此冒泡排序最多需要扫描 \(n-1\) 遍数组就能完成排序。

代码实现

def bubble_sort(a, n):
    flag = True
    while flag:
        flag = False
        for i in range(1, n):
            if a[i] > a[i + 1]:
                flag = True
                a[i], a[i + 1] = a[i + 1], a[i]

选择排序

简介

选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理是每次找出第 i 小的元素(也就是 A_{i..n} 中最小的元素),然后将这个元素与数组第 i 个位置上的元素交换。

image

由于 swap(交换两个元素)操作的存在,选择排序是一种不稳定的排序算法。选择排序的最优时间复杂度、平均时间复杂度和最坏时间复杂度均为 O(n^2)。

代码实现

def selection_sort(a, n):
    for i in range(1, n):
        ith = i
        for j in range(i + 1, n + 1):
            if a[j] < a[ith]:
                ith = j
        a[i], a[ith] = a[ith], a[i]

快速排序

简介

快速排序(Quick sort),又称分区交换排序(partition-exchange sort),简称「快排」,是一种被广泛运用的排序算法。

快速排序的工作原理是通过 分治 的方式来将一个数组排序。

快速排序分为三个过程:

  • 将数列划分为两部分(要求保证相对大小关系);

  • 递归到两个子序列中分别进行快速排序;

  • 不用合并,因为此时数列已经完全有序。

和归并排序不同,第一步并不是直接分成前后两个序列,而是在分的过程中要保证相对大小关系。具体来说,第一步要是要把数列分成两个部分,然后保证前一个子数列中的数都小于后一个子数列中的数。为了保证平均时间复杂度,一般是随机选择一个数 m 来当做两个子数列的分界。

之后,维护一前一后两个指针 p 和 q,依次考虑当前的数是否放在了应该放的位置(前还是后)。如果当前的数没放对,比如说如果后面的指针 q 遇到了一个比 m 小的数,那么可以交换 p 和 q 位置上的数,再把 p 向后移一位。当前的数的位置全放对后,再移动指针继续处理,直到两个指针相遇。

其实,快速排序没有指定应如何具体实现第一步,不论是选择 m 的过程还是划分的过程,都有不止一种实现方法。

第三步中的序列已经分别有序且第一个序列中的数都小于第二个数,所以直接拼接起来就好了。

代码实现

【Java实现】

public class QuickSort {
    public int[] quickSort(int[] nums) {
        shuffle(nums);
        quickSort(nums, 0, nums.length - 1);
        return nums;
    }

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

    private int partition(int[] nums, int left, int right) {
        int k = left; // 记录交换的起点
        // 以nums[right]作为基准值:遍历基准值左侧的所有元素
        for (int i = left; i < right; i++) {
            // 将小于基准值的元素归位
            if (nums[i] < nums[right]) {
                swap(nums, k++, i);
            }
        }
        swap(nums, k, right); // 将基准元素归位
        return k;
    }

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

    private void shuffle(int[] nums) {
        Random random = new Random();
        int n = nums.length;
        for (int i = 0; i < n; i++) {
            swap(nums, i, random.nextInt(n - i) + i);
        }
    }
}

归并排序

简介

归并排序(merge sort)是高效的基于比较的稳定排序算法。归并排序基于分治思想将数组分段排序后合并,时间复杂度在最优、最坏与平均情况下均为 \(\Theta (n \log n)\),空间复杂度为 \(\Theta (n)\)

归并排序可以只使用 \(\Theta (1)\) 的辅助空间,但为便捷通常使用与原数组等长的辅助数组。

归并排序最核心的部分是合并(merge)过程:

  • 将两个有序的数组 \(a[i]\)\(b[j]\) 合并为一个有序数组 \(c[k]\)

    • 从左往右枚举 \(a[i]\)\(b[j]\),找出最小的值并放入数组 \(c[k]\)

    • 重复上述过程直到 \(a[i]\)\(b[j]\) 有一个为空时,将另一个数组剩下的元素放入 \(c[k]\)

为保证排序的稳定性,前段首元素小于或等于后段首元素时(\(a[i] <= b[j]\))而非小于时(\(a[i] < b[j]\))就要作为最小值放入 \(c[k]\)

代码实现

public class MergeSort {
    public int[] sort(int[] nums) {
        sort(nums, 0, nums.length - 1);
        return nums;
    }

    private void sort(int[] nums, int left, int right) {
        if (left >= right) {
            return;
        }

        int mid = left + (right - left) / 2;
        sort(nums, left, mid); // 左侧区间:[left, mid]
        sort(nums, mid + 1, right); // 右侧区间:[mid + 1, right]
        merge(nums, left, mid, right);
    }

    private void merge(int[] nums, int left, int mid, int right) {
        // 将区间[left, mid]和[mid + 1, right] 合并
        int[] temp = new int[nums.length];
        for (int i = 0; i < nums.length; i++) {
            temp[i] = nums[i];
        }

        int i = left, j = mid + 1, k = left;
        while (i <= mid && j <= right) {
            if (nums[i] < nums[j]) {
                nums[k++] = temp[i++];
            } else {
                nums[k++] = temp[j++];
            }
        }

        while (i <= mid) {
            nums[k++] = temp[i++];
        }

        while (j <= right) {
            nums[k++] = temp[j++];
        }
    }
}

插入排序

简介

插入排序(Insertion sort)是一种简单直观的排序算法。它的工作原理为将待排列元素划分为「已排序」和「未排序」两部分,每次从「未排序的」元素中选择一个插入到「已排序的」元素中的正确位置。

一个与插入排序相同的操作是打扑克牌时,从牌桌上抓一张牌,按牌面大小插到手牌后,再抓下一张牌。

image

插入排序是一种稳定的排序算法。

代码实现

def insertion_sort(arr, n):
    for i in range(1, n):
        key = arr[i]
        j = i - 1
        while j >= 0 and arr[j] > key:
            arr[j + 1] = arr[j]
            j = j - 1
        arr[j + 1] = key

折半插入排序

简介

插入排序还可以通过二分算法优化性能,在排序元素数量较多时优化的效果比较明显。

代码实现

void insertion_sort(int arr[], int len) {
    if (len < 2) return;
    for (int i = 1; i != len; ++i) {
        int key = arr[i];
        auto index = upper_bound(arr, arr + i, key) - arr;
        // 使用 memmove 移动元素,比使用 for 循环速度更快,时间复杂度仍为 O(n)
        memmove(arr + index + 1, arr + index, (i - index) * sizeof(int));
        arr[index] = key;
    }
}

堆排序

简介

堆排序(Heap sort)是指利用 二叉堆 这种数据结构所设计的一种排序算法。堆排序的适用数据结构为数组。

过程:

  • 首先建立大顶堆,然后将堆顶的元素取出,作为最大值,与数组尾部的元素交换,并维持残余堆的性质;

  • 之后将堆顶的元素取出,作为次大值,与数组倒数第二位元素交换,并维持残余堆的性质;

  • 以此类推,在第 n-1 次操作后,整个数组就完成了排序。

代码实现

def sift_down(arr, start, end):
    # 计算父结点和子结点的下标
    parent = int(start)
    child = int(parent * 2 + 1)
    while child <= end: # 子结点下标在范围内才做比较
        # 先比较两个子结点大小,选择最大的
        if child + 1 <= end and arr[child] < arr[child + 1]:
            child += 1
        # 如果父结点比子结点大,代表调整完毕,直接跳出函数
        if arr[parent] >= arr[child]:
            return
        else: # 否则交换父子内容,子结点再和孙结点比较
            arr[parent], arr[child] = arr[child], arr[parent]
            parent = child
            child = int(parent * 2 + 1)

def heap_sort(arr, len):
  # 从最后一个节点的父节点开始 sift down 以完成堆化 (heapify)
    i = (len - 1 - 1) / 2
    while(i >= 0):
        sift_down(arr, i, len - 1)
        i -= 1
  # 先将第一个元素和已经排好的元素前一位做交换,再重新调整(刚调整的元素之前的元素),直到排序完毕
    i = len - 1
    while(i > 0):
        arr[0], arr[i] = arr[i], arr[0]
        sift_down(arr, 0, i - 1)
        i -= 1

桶排序

简介

桶排序(Bucket sort)是排序算法的一种,适用于待排序数据值域较大但分布比较均匀的情况。

过程:

  • 设置一个定量的数组当作空桶;

  • 遍历序列,并将元素一个个放到对应的桶中;

  • 对每个不是空的桶进行排序;

  • 从不是空的桶里把元素再放回原来的序列中。

代码实现

N = 100010
w = n = 0
a = [0] * N
bucket = [[] for i in range(N)]

def insertion_sort(A):
    for i in range(1, len(A)):
        key = A[i]
        j = i - 1
        while j >= 0 and A[j] > key:
            A[j + 1] = A[j]
            j -= 1
        A[j + 1] = key

def bucket_sort():
    bucket_size = int(w / n + 1)
    for i in range(0, n):
        bucket[i].clear()
    for i in range(1, n + 1):
        bucket[int(a[i] / bucket_size)].append(a[i])
    p = 0
    for i in range(0, n):
        insertion_sort(bucket[i])
        for j in range(0, len(bucket[i])):
            a[p] = bucket[i][j]
            p += 1

希尔排序

简介

希尔排序(Shell sort),也称为缩小增量排序法,是 插入排序 的一种改进版本。希尔排序以它的发明者希尔(英语:Donald Shell)命名。

过程

  • 将待排序序列分为若干子序列(每个子序列的元素在原始数组中间距相同);

  • 对这些子序列进行插入排序;

  • 减小每个子序列中元素之间的间距,重复上述过程直至间距减少为 1。

代码实现

def shell_sort(array, length):
    h = 1
    while h < length / 3:
        h = int(3 * h + 1)
    while h >= 1:
        for i in range(h, length):
            j = i
            while j >= h and array[j] < array[j - h]:
                array[j], array[j - h] = array[j - h], array[j]
                j -= h
        h = int(h / 3)

参考:

posted @ 2023-09-11 16:32  LARRY1024  阅读(14)  评论(0编辑  收藏  举报