文章目录
前言
排序算法是计算机程序中常用的一种算法,它可以将一个无序的数据集合按照某种规则重新排列,使其变为有序的数据集合。
本文实现了基于模板的排序算法,这样的话使用与任何场合,任何类型的数组。c++与go实现
一、排序算法模板的定义以及使用
c++实现
以冒泡排序为例子。函数模板支持两种类型的参数:vector& arr 和 Compare cmp。vector& arr 表示待排序的数组,cmp表示比较函数,它的功能是判断两个对象(这里是vector类型)与给定的规则是否满足关系: 如果a在b之前,返回true;否则返回false。第一个参数cmp的类型为typename Compare,表示比较函数,第二个参数T& arr的类型为typename T,表示要排序的数组类型。
template<typename T,typename Compare>
void bubble_sort(vector<T> & arr,Compare cmp){
//排序算法部分
int n=arr.size();
for(int i=0;i<n-1;i++){
for (int j = 0; j < n - i - 1; ++j) {
if (!cmp(arr[j], arr[j+1])) {
swap(arr[j], arr[j + 1]);
}
}
}
}
使用起来也很简单。比如说要字典权值排序。
vector<string> v{"小明","小亮","小东","小西","小丽"};
map<string,int> m;
m["小明"]=90;
m["小亮"]=70;
m["小东"]=80;
m["小西"]=50;
m["小丽"]=60;
bubble_sort(v, [&](string a, string b){
return m[a] > m[b];
});
go语言
go语言实现中我们首先定义了一个 BubbleSort 函数,使用 [T any] 来声明一个泛型类型 T,并传入一个泛型切片 arr 和一个比较函数 cmp。在函数内部,我们使用泛型类型 T 和比较函数 cmp 来进行元素的比较和交换。
package main
import (
"fmt"
)
// 泛型冒泡排序算法
func BubbleSort[T any](arr []T, cmp func(T, T) bool) {
n := len(arr)
for i := 0; i < n-1; i++ {
for j := 0; j < n-i-1; j++ {
if !cmp(arr[j], arr[j+1]) {
arr[j], arr[j+1] = arr[j+1], arr[j]
}
}
}
}
// 测试代码
func main() {
v := []string{"小明", "小亮", "小东", "小西", "小丽"}
m := map[string]int{
"小明": 90,
"小亮": 70,
"小东": 80,
"小西": 50,
"小丽": 60,
}
fmt.Println("Before sorting:", v)
BubbleSort(v, func(a, b string) bool {
return m[a] > m[b]
})
fmt.Println("After sorting:", v)
}
1、插入排序
插入排序是一种简单直观的排序算法,它的原理是将待排序的元素逐个插入到已排序的序列中,形成一个有序的序列。在实现插入排序时,我们需要通过比较和交换元素的位置来完成排序过程。过程如下图所示:
步骤详解
插入排序算法的原理可以描述为以下几个步骤:
1、将第一个元素视为已排序序列。
2、从第二个元素开始,逐个将待排序的元素插入已排序序列的合适位置。
3、每次插入之后,已排序序列的长度增加一个元素,直到所有元素都被插入完毕。
具体来说,假设我们有一个待排序的序列 arr,它包含 n 个元素。我们将第一个元素 arr[0] 视为已排序序列,然后从第二个元素 arr[1] 开始,逐个将待排序的元素插入已排序序列中。
在插入的过程中,我们需要将待排序元素与已排序序列中的元素进行比较,找到合适的位置插入。如果待排序元素小于已排序序列中的某个元素,就将该元素后移,为待排序元素腾出位置。依此类推,直到找到待排序元素的合适位置,然后将其插入。
c++实现
c++函数中的变量 n 表示向量的大小,循环从第二个元素开始遍历。在每次循环中,我们将待排序元素 arr[i] 存储在变量 key 中,并将索引值 j 初始化为 i - 1。
接下来,通过一个 while 循环来找到待排序元素的合适位置。循环条件为 j >= 0(确保不越界)且 !cmp(arr[j], key)(待排序元素不小于已排序序列中的元素)。在循环中,我们将已排序序列中的元素逐个后移,直到找到合适的位置。
最后,将待排序元素插入到 arr[j + 1] 的位置,完成一次插入操作。重复上述步骤,直到所有元素都被插入完毕,整个向量就被排序完成。
template<typename T, typename Compare>
void insertion_sort(vector<T>& arr, Compare cmp) {
int n = arr.size();
for (int i = 1; i < n; i++) {
T key = arr[i];
int j = i - 1;
while (j >= 0 && !cmp(arr[j], key)) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = key;
}
}
go实现
go语言中首先定义了一个名为 InsertionSort 的泛型函数,它接收两个参数:切片 arr 和比较函数 cmp,用于确定元素之间的大小关系。
首先,我们获取数组的长度 n。然后,使用一个 for 循环来遍历数组,从索引 1(即第二个元素)开始。
在每一轮的循环中,我们先将当前元素 arr[i] 存储到一个变量 key 中,用于将其插入到正确的位置。
接下来,我们定义了一个变量 j,它初始化为 i-1,代表着要和 key 进行比较的前一个元素。
然后,我们使用一个循环来将比 key 大的元素向后移动。条件 j >= 0 保证了我们不会越界,并且 !cmp(arr[j], key) 确保了我们按照 cmp 函数的规则进行比较。
func InsertionSort[T any](arr []T, cmp func(T, T) bool) {
n := len(arr)
for i := 1; i < n; i++ {
key := arr[i]
j := i - 1
for j >= 0 && !cmp(arr[j], key) {
arr[j+1] = arr[j]
j--
}
arr[j+1] = key
}
}
2、冒泡排序
冒泡排序算法的原理非常直观。它从数组的第一个元素开始,比较相邻的两个元素的大小。如果顺序不正确,就交换这两个元素的位置。通过多次扫描和交换,最大(或最小)的元素逐渐“冒泡”到数组的末端,所以称为冒泡排序。过程如下图所示:
步骤详解
1、对给定的数组进行遍历。
2、比较当前元素和下一个相邻元素的大小。
3、如果顺序不正确,交换这两个元素的位置。
4、继续遍历数组,重复步骤2和3,直到整个数组按照指定顺序排列。
c++实现
template<typename T, typename Compare>
void bubble_sort(vector<T> &arr, Compare cmp) {
int n = arr.size();
for (int i = 0; i < n - 1; i++) { // 遍历数组
for (int j = 0; j < n - i - 1; j++) { // 逐个比较相邻元素
if (!cmp(arr[j], arr[j + 1])) { // 根据比较结果决定是否交换位置
swap(arr[j], arr[j + 1]);
}
}
}
}
go实现
func BubbleSort[T any](arr []T, cmp func(T, T) bool) {
n := len(arr)
for i := 0; i < n-1; i++ { // 遍历切片
for j := 0; j < n-i-1; j++ { // 逐个比较相邻元素
if !cmp(arr[j], arr[j+1]) { // 根据比较结果决定是否交换位置
arr[j], arr[j+1] = arr[j+1], arr[j]
}
}
}
}
3、选择排序
它将数组划分为两个部分:已排序的部分和未排序的部分。每次迭代,选择排序都会在未排序的部分中找到最小(或最大)的元素,并将其放置在已排序部分的末尾。通过重复此过程,将所有元素有序排列。
步骤详解
1、设定数组范围为从索引0到n-1(n为数组长度)。
2、在未排序的部分中找到最小(或最大)的元素。
3、将找到的最小(或最大)元素与未排序部分的第一个元素交换位置。
4、将已排序部分的长度增加1,将未排序部分的长度减少1。
5、重复步骤2至4,直到所有元素有序排列。
c++实现
template<typename T, typename Compare>
void selection_sort(vector<T> &arr, Compare cmp) {
int n = arr.size();
for (int i = 0; i < n - 1; i++) { // 遍历数组范围
int min_idx = i;
for (int j = i + 1; j < n; j++) { // 在未排序部分中找到最小元素的索引
if (cmp(arr[j], arr[min_idx])) {
min_idx = j;
}
}
swap(arr[i], arr[min_idx]); // 将最小元素与未排序部分的第一个元素交换
}
}
go实现
func SelectionSort[T any](arr []T, cmp func(T, T) bool) {
n := len(arr)
for i := 0; i < n-1; i++ { // 遍历切片范围
minIdx := i
for j := i + 1; j < n; j++ { // 在未排序部分中找到最小元素的索引
if cmp(arr[j], arr[minIdx]) {
minIdx = j
}
}
arr[i], arr[minIdx] = arr[minIdx], arr[i] // 将最小元素与未排序部分的第一个元素交换
}
}
4、归并排序
归并排序的原理非常巧妙。它采用分而治之的策略,将数组递归地分成较小的部分,然后将这些部分排序并合并起来,直到整个数组有序。归并排序的核心思想是合并两个已排序的数组。。原理如下图所示:
步骤详解
1、将数组划分为长度为1的子数组,这些子数组被认为是已排序的。
2、递归地将相邻的子数组合并成较大的已排序数组。
3、重复合并步骤,直到最终将整个数组合并成一个有序数组。
c++实现
template<typename T, typename Compare>
void merge(vector<T> &arr, int left, int mid, int right, Compare cmp) {
int n1 = mid - left + 1;
int n2 = right - mid;
vector<T> left_arr(n1), right_arr(n2);
for (int i = 0; i < n1; i++) {
left_arr[i] = arr[left + i];
}
for (int i = 0; i < n2; i++) {
right_arr[i] = arr[mid + 1 + i];
}
int i = 0, j = 0, k = left;
while (i < n1 && j < n2) {
if (cmp(left_arr[i], right_arr[j])) {
arr[k] = left_arr[i];
i++;
} else {
arr[k] = right_arr[j];
j++;
}
k++;
}
while (i < n1) {
arr[k] = left_arr[i];
i++;
k++;
}
while (j < n2) {
arr[k] = right_arr[j];
j++;
k++;
}
}
template<typename T, typename Compare>
void merge_sort(vector<T> &arr, int left, int right, Compare cmp) {
if (left < right) {
int mid = left + (right - left) / 2;
merge_sort(arr, left, mid, cmp);
merge_sort(arr, mid + 1, right, cmp);
merge(arr, left, mid, right, cmp);
}
}
go实现
func mergeSort[T any](arr []T, cmp func(T, T) bool) []T {
n := len(arr)
if n <= 1 {
return arr
}
mid := n / 2
left := mergeSort(arr[:mid], cmp)
right := mergeSort(arr[mid:], cmp)
return merge(left, right, cmp)
}
func merge[T any](left []T, right []T, cmp func(T, T) bool) []T {
size, i, j := len(left)+len(right), 0, 0
result := make([]T, size)
for k := 0; k < size; k++ {
if i < len(left) && (j >= len(right) || cmp(left[i], right[j])) {
result[k] = left[i]
i++
} else {
result[k] = right[j]
j++
}
}
return result
}
func main() {
arr := []int{9, 2, 7, 5, 1, 6, 8, 3, 4}
fmt.Println("排序之前:", arr)
cmp := func(a, b int) bool {
return a < b
}
sortedArr := mergeSort(arr, cmp)
fmt.Println("排序之后:", sortedArr)
}
5、快速排序
快速排序采用了分治的策略,通过将一个数组分成更小的子数组,然后分别对子数组进行排序,最后将子数组的排序结果合并,从而实现整个数组的有序排列。其主要思想是选择一个基准元素,通过排列使得基准元素左边的元素均小于基准元素,右边的元素均大于基准元素。通过递归地执行此操作,最终可以得到一个有序的数组。 原理下图所示
步骤详解
1、选择一个元素作为基准(pivot),通常选择数组的第一个元素。
2、将数组划分为两个部分,使得左边的部分的所有元素小于基准元素,右边的部分的所有元素大于基准元素。这可以通过交换元素的位置来实现,即将小于等于基准元素的元素放在基准元素的左边,将大于基准元素的元素放在右边。
3、对划分出的两个部分递归地执行步骤1和步骤2,直到每个部分只剩下一个元素。
4、合并结果:将所有部分的排序结果按顺序合并,即将左边部分的元素、基准元素和右边部分的元素依次连接起来。
c++实现
template<typename T, typename Compare>
int partition(vector<T> &arr, int low, int high, Compare cmp) {
T pivot = arr[high];
int i = low - 1;
for (int j = low; j < high; j++) {
if (cmp(arr[j], pivot)) {
i++;
swap(arr[i], arr[j]);
}
}
swap(arr[i + 1], arr[high]);
return i + 1;
}
template<typename T, typename Compare>
void quickSort(vector<T> &arr, int low, int high, Compare cmp) {
if (low < high) {
int pivotIdx = partition(arr, low, high, cmp);
quickSort(arr, low, pivotIdx - 1, cmp);
quickSort(arr, pivotIdx + 1, high, cmp);
}
}
template<typename T, typename Compare>
void quickSort(vector<T> &arr, Compare cmp) {
int n = arr.size();
quickSort(arr, 0, n - 1, cmp);
}
go实现
func partition[T any](arr []T, low, high int, cmp func(T, T) bool) int {
pivot := arr[high]
i := low - 1
for j := low; j < high; j++ {
if cmp(arr[j], pivot) {
i++
arr[i], arr[j] = arr[j], arr[i]
}
}
arr[i+1], arr[high] = arr[high], arr[i+1]
return i + 1
}
func quickSort[T any](arr []T, low, high int, cmp func(T, T) bool) {
if low < high {
pivotIdx := partition(arr, low, high, cmp)
quickSort(arr, low, pivotIdx-1, cmp)
quickSort(arr, pivotIdx+1, high, cmp)
}
}
func QuickSort[T any](arr []T, cmp func(T, T) bool) {
n := len(arr)
quickSort(arr, 0, n-1, cmp)
}
6、堆排序
堆排序的主要思想是将待排序的数组视为一个完全二叉树,并通过一系列的比较和交换操作,使得树中每个父节点的值都大于(或小于)其子节点的值,如下图所示:
步骤详解
1、构建最大堆(Max Heap),使得每个父节点的值都大于其子节点的值。
2、将堆顶元素(最大值)与数组末尾元素交换位置,并将最后一个元素从堆中移除。
3、重复步骤1和步骤2,直到堆为空。
4、最终数组中的元素按照从小到大的顺序排列。
c++实现
template<typename T, typename Compare>
void heapify(vector<T> &arr, int n, int i, Compare cmp) {
int largest = i;
int left = 2 * i + 1;
int right = 2 * i + 2;
if (left < n && cmp(arr[left], arr[largest])) {
largest = left;
}
if (right < n && cmp(arr[right], arr[largest])) {
largest = right;
}
if (largest != i) {
swap(arr[i], arr[largest]);
heapify(arr, n, largest, cmp);
}
}
template<typename T, typename Compare>
void heapSort(vector<T> &arr, Compare cmp) {
int n = arr.size();
for (int i = n / 2 - 1; i >= 0; i--) {
heapify(arr, n, i, cmp);
}
for (int i = n - 1; i >= 0; i--) {
swap(arr[0], arr[i]);
heapify(arr, i, 0, cmp);
}
}
go实现
func heapify[T any](arr []T, n, i int, cmp func(T, T) bool) {
largest := i
left := 2*i + 1
right := 2*i + 2
if left < n && cmp(arr[left], arr[largest]) {
largest = left
}
if right < n && cmp(arr[right], arr[largest]) {
largest = right
}
if largest != i {
arr[i], arr[largest] = arr[largest], arr[i]
heapify(arr, n, largest, cmp)
}
}
func HeapSort[T any](arr []T, cmp func(T, T) bool) {
n := len(arr)
for i := n/2 - 1; i >= 0; i-- {
heapify(arr, n, i, cmp)
}
for i := n - 1; i >= 0; i-- {
arr[0], arr[i] = arr[i], arr[0]
heapify(arr, i, 0, cmp)
}
}
6种排序算法对比
插入排序(Insertion Sort):将数组分为已排序和未排序两部分,每次从未排序部分取出一个元素插入到已排序部分的正确位置。时间复杂度为平均情况和最坏情况下都是O(n^2),最好情况(数组已经有序)下为O(n)。
冒泡排序(Bubble Sort):通过重复地交换相邻的元素,将较大(或较小)的元素逐渐移动到数组末尾。时间复杂度为平均情况和最坏情况下都是O(n^2),最好情况(数组已经有序)下为O(n)。
选择排序(Selection Sort):从未排序部分选择最小(或最大)的元素,并与未排序部分的第一个元素交换位置。时间复杂度为平均情况和最坏情况下都是O(n^2)。
归并排序(Merge Sort):将数组递归地分成两半,分别进行排序,然后将两个有序的子数组合并成一个有序数组。时间复杂度为平均情况、最坏情况和最好情况下都是O(nlogn)。
快速排序(Quick Sort):选择一个基准元素,将数组分成比基准元素小和大两部分,然后通过递归地对这两部分进行快速排序。时间复杂度为平均情况和最好情况下都是O(nlogn),最坏情况下为O(n^2)。
堆排序(Heap Sort):通过构建最大堆(或最小堆)并重复地将堆顶元素与数组末尾元素交换,然后移除末尾元素,从而实现排序。时间复杂度为平均情况、最坏情况和最好情况下都是O(nlogn)。
综上所述,归并排序、快速排序和堆排序的时间复杂度都为O(nlogn),在平均情况下表现较好;而插入排序、冒泡排序和选择排序的时间复杂度都为O(n^2),在数据规模较小时表现较好。因此,对于大规模数据的排序问题,归并排序、快速排序和堆排序通常是更好的选择,而对于小规模数据,插入排序可能更合适。