泛型排序算法原理以及实现,基于函数模板【c++与golang】【万字总结】


前言

排序算法是计算机程序中常用的一种算法,它可以将一个无序的数据集合按照某种规则重新排列,使其变为有序的数据集合。
本文实现了基于模板的排序算法,这样的话使用与任何场合,任何类型的数组。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),在数据规模较小时表现较好。因此,对于大规模数据的排序问题,归并排序、快速排序和堆排序通常是更好的选择,而对于小规模数据,插入排序可能更合适。
在这里插入图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值