排序算法
冒泡排序
简介
冒泡排序(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 个位置上的元素交换。
由于 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)是一种简单直观的排序算法。它的工作原理为将待排列元素划分为「已排序」和「未排序」两部分,每次从「未排序的」元素中选择一个插入到「已排序的」元素中的正确位置。
一个与插入排序相同的操作是打扑克牌时,从牌桌上抓一张牌,按牌面大小插到手牌后,再抓下一张牌。
插入排序是一种稳定的排序算法。
代码实现
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)
参考: