C/C++数据结构——剖析排序算法

 1. 排序的概念及其运用

1.1 排序的概念

https://en.wikipedia.org/wiki/Insertion_sorticon-default.png?t=N7T8https://en.wikipedia.org/wiki/Insertion_sort

排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
内部排序:数据元素全部放在内存中的排序。
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。

1.2 常见的排序算法

                         


// 排序实现的接口
// 插入排序
void InsertSort(int* a, int n);
// 希尔排序
void ShellSort(int* a, int n);
// 选择排序
void SelectSort(int* a, int n);
// 堆排序
void AdjustDwon(int* a, int n, int root);
void HeapSort(int* a, int n);
// 冒泡排序
void BubbleSort(int* a, int n);
// 快速排序递归实现
// 快速排序hoare版本
int PartSort1(int* a, int left, int right);
// 快速排序挖坑法
int PartSort2(int* a, int left, int right);
// 快速排序前后指针法
int PartSort3(int* a, int left, int right);
void QuickSort(int* a, int left, int right);
// 快速排序 非递归实现
void QuickSortNonR(int* a, int left, int right);
// 归并排序递归实现
void MergeSort(int* a, int n);
// 归并排序非递归实现
void MergeSortNonR(int* a, int n);
// 计数排序
void CountSort(int* a, int n);
// 测试排序的性能对比
void TestOP()
{
    srand(time(0));
    const int N = 100000;
    int* a1 = (int*)malloc(sizeof(int) * N);
    int* a2 = (int*)malloc(sizeof(int) * N);
    int* a3 = (int*)malloc(sizeof(int) * N);
    int* a4 = (int*)malloc(sizeof(int) * N);
    int* a5 = (int*)malloc(sizeof(int) * N);
    int* a6 = (int*)malloc(sizeof(int) * N);
    for (int i = 0; i < N; ++i)
    {
        a1[i] = rand();
        a2[i] = a1[i];
        a3[i] = a1[i];
        a4[i] = a1[i];
        a5[i] = a1[i];
        a6[i] = a1[i];
    }
    int begin1 = clock();
    InsertSort(a1, N);
    int end1 = clock();
    int begin2 = clock();
    ShellSort(a2, N);
    int end2 = clock();
    int begin3 = clock();
    SelectSort(a3, N);
    int end3 = clock();
    int begin4 = clock();
    HeapSort(a4, N);
    int end4 = clock();
    int begin5 = clock();
    QuickSort(a5, 0, N - 1);
    int end5 = clock();
    int begin6 = clock();
    MergeSort(a6, N);
    int end6 = clock();
    printf("InsertSort:%d\n", end1 - begin1);
    printf("ShellSort:%d\n", end2 - begin2);
    printf("SelectSort:%d\n", end3 - begin3);
    printf("HeapSort:%d\n", end4 - begin4);
    printf("QuickSort:%d\n", end5 - begin5);
    printf("MergeSort:%d\n", end6 - begin6);
    free(a1);
    free(a2);
    free(a3);
    free(a4);
    free(a5);
    free(a6);
}

排序OJ(可使用各种排序跑这个OJ)力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台备战技术面试?力扣提供海量技术面试资源,帮助你高效提升编程技能,轻松拿下世界 IT 名企 Dream Offer。icon-default.png?t=N7T8https://leetcode-cn.com/problems/sort-an-array/

2. 常见排序算法的解析与实现

2.1 插入排序 
2.1.1 基本思想

直接插入排序是一种简单的插入排序法,其基本思想是:

把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 。

实际中我们玩扑克牌时,就用了插入排序的思想

                                    

2.1.2 直接插入排序

当插入第i(i>=1)个元素时,前面的array[0],array[1],…,array[i-1]已经排好序,此时用array[i]的排序码与array[i-1],array[i-2],…的排序码顺序进行比较,找到插入位置即将array[i]插入,原来位置上的元素顺序后移

2.1.3 直接插入排序的实现
// 定义一个插入排序函数
void InsertSort(int* a,int n)
{
	// 遍历数组,从第一个元素到倒数第二个元素
	for (int i = 0; i < n-1; i++)
	{
		// 定义当前元素的位置
		int end = i;
		// 保存当前元素的值
		int temp = a[end + 1];
		// 循环向前比较,直到找到合适的位置插入当前元素
		while (end >= 0)
		{
			if (temp < a[end])
			{
				a[end + 1] = a[end];
				end--;
			}
			else
			{
				break;
			}
		}
		// 将当前元素插入到合适的位置
		a[end + 1] = temp;	
	}
}

直接插入排序的特性总结:
1. 元素集合越接近有序,直接插入排序算法的时间效率越高
2. 时间复杂度:   Worst : O(N^2) ,  Best : O(N)
3. 空间复杂度:O(1),它是一种稳定的排序算法
4. 稳定性:稳定

                      

2.2 希尔排序
2.2.1 基本思想

希尔排序法又称缩小增量法希尔排序法的基本思想是:先选定一个整数,把待排序文件中所有记录分成个组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工作。当到达=1时,所有记录在统一组内排好序。

2.2.2 希尔排序的实现

一组一组排:

void ShellSort(int* a, int n)
{
    int gap = n; // 初始化间隔为数组长度
    
    // 开始希尔排序
    while (gap > 1) // 当间隔大于1时进行排序
    {
        gap = gap / 3 + 1; // 根据规则更新间隔
        
        // 以下是希尔排序的核心算法
        for (int i = 0; i < n - gap; i++) // 以间隔为步长进行插入排序
        {
            int end = i;
            int temp = a[end + gap]; // 保存当前位置的值
            while (end >= 0)
            {
                if (temp < a[end]) // 如果当前值小于前一个值,则交换位置
                {
                    a[end + gap] = a[end];
                    end -= gap;
                }
                else
                {
                    break;
                }
            }
            a[end + gap] = temp; // 插入当前值到正确位置
        }
    }
}

多组并排:

void ShellSort(int* a, int n)
{
    int gap = n; // 初始化间隔为数组长度
    
    // 开始希尔排序
    while (gap > 1) // 当间隔大于1时进行排序
    {
        gap = gap / 3 + 1; // 根据规则更新间隔
        
        // 以下是希尔排序的核心算法
        for (int j = 0; j < gap; j++) // 以间隔为步长进行插入排序
        {
            for (int i = j; i < n - gap; i += gap)
            {
                int end = i;
                int temp = a[end + gap]; // 保存当前位置的值
                while (end >= 0)
                {
                    if (temp < a[end]) // 如果当前值小于前一个值,则交换位置
                    {
                        a[end + gap] = a[end];
                        end -= gap;
                    }
                    else
                    {
                        break;
                    }
                }
                a[end + gap] = temp; // 插入当前值到正确位置
            }
        }
    }
}

希尔排序的特性总结:
1. 希尔排序是对直接插入排序的优化。
2. 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。
3. 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在好些树中给出的希尔排序的时间复杂度都不固定:

《数据结构(C语言版)》--- 严蔚敏

《数据结构-用面相对象方法与C++描述》--- 殷人昆

https://en.wikipedia.org/wiki/Shellsorticon-default.png?t=N7T8https://en.wikipedia.org/wiki/Shellsort 4. 稳定性:不稳定

2.3 选择排序
2.3.1 基本思想

https://en.wikipedia.org/wiki/Selection_sorticon-default.png?t=N7T8https://en.wikipedia.org/wiki/Selection_sort

每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。

2.3.2 直接选择排序

(1)在元素集合array[i]--array[n-1]中选择关键码最大(小)的数据元素。
(2)若它不是这组元素中的最后一个(第一个)元素,则将它与这组元素中的最后一个(第一个)元素交换。
(3)在剩余的array[i]--array[n-2](array[i+1]--array[n-1])集合中,重复上述步骤,直到集合剩余1个元素。

2.3.3 选择排序的实现
void SelectSort(int* a, int n)
{
    int begin = 0;
    int end = n - 1;
    
    // 开始选择排序
    while (begin < end)
    {
        int min = begin;
        int max = begin;
        
        // 找到当前范围内的最小值和最大值的索引
        for (int i = begin + 1; i <= end; i++)
        {
            if (a[i] < a[min])
            {
                min = i;
            }
            if (a[i] > a[max])
            {
                max = i;
            }
        }
        
        // 将最小值与当前范围的起始位置交换
        int temp1 = a[begin];
        a[begin] = a[min];
        a[min] = temp1;
        
        // 如果最大值的索引等于当前范围的起始位置,更新最大值的索引
        if (max == begin)
        {
            max = min;
        }
        
        // 将最大值与当前范围的结束位置交换
        int temp2 = a[max];
        a[max] = a[end];
        a[end] = temp2;
        
        begin++;
        end--;
    }
}

直接选择排序的特性总结:
1. 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用
2. 时间复杂度:O(N^2)
3. 空间复杂度:O(1)
4. 稳定性:不稳定

2.4 堆排序
2.4.1 基本思想

堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆

https://en.wikipedia.org/wiki/Heapsorticon-default.png?t=N7T8https://en.wikipedia.org/wiki/Heapsort

2.4.2 堆排序的实现
typedef int HeapDataType; // 定义堆数据类型为整型

void Swap(int* x, int* y)
{
    int temp = *x;
    *x = *y;
    *y = temp;
}

// 向上调整建大堆
void AdjustUp1(HeapDataType* a, int child)
{
    int parent = (child - 1) / 2; // 计算父节点索引
    while (child > 0)
    {
        if (a[child] > a[parent]) // 如果子节点大于父节点
        {
            Swap(&a[child], &a[parent]); // 交换子节点和父节点的值
            child = parent; // 更新子节点索引
            parent = (parent - 1) / 2; // 更新父节点索引
        }
        else
        {
            break; // 如果子节点不大于父节点,则退出循环
        }
    }
}

// 向上调整建小堆
void AdjustUp2(HeapDataType* a, int child)
{
    int parent = (child - 1) / 2; // 计算父节点索引
    while (child > 0)
    {
        if (a[child] < a[parent]) // 如果子节点小于父节点
        {
            Swap(&a[child], &a[parent]); // 交换子节点和父节点的值
            child = parent; // 更新子节点索引
            parent = (parent - 1) / 2; // 更新父节点索引
        }
        else
        {
            break; // 如果子节点不小于父节点,则退出循环
        }
    }
}

// 大堆的向下调整
void AdjustDown1(HeapDataType* a, int size, int parent)
{
    int child = parent * 2 + 1; // 计算左孩子节点索引
    while (child < size)
    {
        if (child + 1 < size && a[child] < a[child + 1]) // 如果右孩子存在且大于左孩子
        {
            child++; // 右孩子节点索引加1,选择较大的孩子节点
        }
        if (a[child] > a[parent]) // 如果孩子节点大于父节点
        {
            Swap(&a[child], &a[parent]); // 交换孩子节点和父节点的值
            parent = child; // 更新父节点索引
            child = child * 2 + 1; // 更新孩子节点索引
        }
        else
        {
            break; // 如果孩子节点不大于父节点,则退出循环
        }
    }
}

// 小堆的向下调整
void AdjustDown2(HeapDataType* a, int size, int parent)
{
    int child = parent * 2 + 1; // 计算左孩子节点索引
    while (child < size)
    {
        if (child + 1 < size && a[child] > a[child + 1]) // 如果右孩子存在且小于左孩子
        {
            child++; // 右孩子节点索引加1,选择较小的孩子节点
        }
        if (a[child] < a[parent]) // 如果孩子节点小于父节点
        {
            Swap(&a[child], &a[parent]); // 交换孩子节点和父节点的值
            parent = child; // 更新父节点索引
            child = child * 2 + 1; // 更新孩子节点索引
        }
        else
        {
            break; // 如果孩子节点不小于父节点,则退出循环
        }
    }
}

void HeapSort(HeapDataType* a, int n)
{
#if 1

    // 大堆排序
    for (int i = ((n - 1) - 1) / 2; i >= 0; i++) // 从最后一个非叶子节点开始向上调整建堆
    {
        AdjustDown1(a, n, i); // 向下调整建大堆
    }
    int end = n - 1; // 堆尾索引
    while (end > 0)
    {
        Swap(&a[0], &a[end]); // 将堆顶元素与末尾元素交换
        AdjustDown1(a, end, 0); // 重新调整堆顶元素
        end--; // 缩小堆范围
    }

#endif

#if 0

	for (int  i = 0; i < n; i++)
	{
		AdjustUp2(a, i);
	}
	int end = n - 1;
	while (end > 0)
	{
		Swap(&a[0], &a[end]);
		AdjustDown2(a, end, 0);
		end--;
	}

#endif
}

堆排序的特性总结:
1. 堆排序使用堆来选数,效率就高了很多。
2. 时间复杂度:O(N*logN)
3. 空间复杂度:O(1)
4. 稳定性:不稳定

2.5 冒泡排序

https://en.wikipedia.org/wiki/Bubble_sorticon-default.png?t=N7T8https://en.wikipedia.org/wiki/Bubble_sort

2.5.1 基本思想

基本思想:所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动

2.5.2 冒泡排序的实现
// 定义一个函数,用于交换两个整数指针指向的值
void Swap(int* x, int* y)
{
	int temp = *x; // 用一个临时变量保存x指向的值
	*x = *y; // 将y指向的值赋给x指向的值
	*y = temp; // 将临时变量的值赋给y指向的值
}

// 定义一个函数,用于对整型数组进行冒泡排序
void BubbleSort(int* a, int n)
{
	for (int i = 0; i < n; i++) // 外层循环控制比较的轮数
	{
		int flag = 1; // 设置一个标志位,用于判断是否已经完成排序
		for (int j = 0; j < n - 1 - i; j++) // 内层循环进行相邻元素的比较和交换
		{
			if (a[j] > a[j + 1]) // 如果前一个元素大于后一个元素
			{
				flag = 0; // 将标志位置为0,表示还未完成排序
				Swap(&a[j], &a[j + 1]); // 调用Swap函数交换两个元素的值
			}
		}
		if (flag) // 如果标志位仍为1,说明已经完成排序
			break; // 跳出循环,排序结束
	}
}

 冒泡排序的特性总结:
1. 冒泡排序是一种非常容易理解的排序
2. 时间复杂度:O(N^2)
3. 空间复杂度:O(1)
4. 稳定性:稳定

2.6 快速排序

https://en.wikipedia.org/wiki/Quicksorticon-default.png?t=N7T8https://en.wikipedia.org/wiki/Quicksort

 

2.6.1 基本思想

快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止

// 假设按照升序对array数组中[left, right)区间中的元素进行排序
void QuickSort(int array[], int left, int right)
{
    if(right - left <= 1)
    return;

    // 按照基准值对array数组的 [left, right)区间中的元素进行划分
    int div = partion(array, left, right);

    // 划分成功后以div为边界形成了左右两部分 [left, div) 和 [div+1, right)
    // 递归排[left, div)
    QuickSort(array, left, div);

    // 递归排[div+1, right)
    QuickSort(array, div+1, right);
}
2.6.2 快速排序优化

1. 三数取中法选key
2. 递归到小的子区间时,可以考虑使用插入排序

                ​​​​​​​        ​​​​​​​

这里的partion(分区)方案我选的是三数取中

int partion(int* a, int begin, int end)
{
	int mid = (begin + end) / 2;
	if (a[begin] < a[mid])
	{
		if (a[end] < a[begin])
			return begin;
		else if (a[end] > a[mid])
			return mid;
		else
			return end;
	}
	else
	{
		if (a[end] < a[mid])
			return mid;
		else if (a[end] > a[begin])
			return begin;
		else
			return end;
	}
}

上述为快速排序递归实现的主框架,发现与二叉树前序遍历规则非常像,在写递归框架时可想想二叉树前序遍历规则即可快速写出来,后序只需分析如何按照基准值来对区间中数据进行划分的方式即可。

2.6.3 快速排序的实现
C/C++库函数实现:

qsort:

int compare(const void* a, const void* b)
{
	return (*(int*)a - *(int*)b);
}
int main()
{
	int arr[] = { 3, 2, 6, 8, 4, 10, 0, 9, 5, 7, 1 };
	qsort(arr, sizeof(arr) / sizeof(arr[0]), sizeof(int), compare);
	for (int i = 0; i < sizeof(arr)/sizeof(arr[0]); i++)
		printf("%d ", arr[i]);
	return 0;
}

sort:

 

// 排序算法示例
#include <iostream>     // 标准输入/输出操作
#include <algorithm>    // std::sort 函数
#include <vector>       // std::vector 容器

// 比较两个整数以进行排序的函数
bool myfunction(int i, int j) { return (i < j); }

// 用于比较两个整数进行排序的仿函数结构体
struct myclass {
    bool operator() (int i, int j) { return (i < j); }
} myobject;

int main() {
    int myints[] = { 32,71,12,45,26,80,53,33 };
    std::vector<int> myvector(myints, myints + 8);               // 使用myints中的值初始化向量

    // 使用默认比较函数(operator <)对前4个元素进行排序
    std::sort(myvector.begin(), myvector.begin() + 4);           // 结果: (12 32 45 71) 26 80 53 33

    // 使用函数myfunction作为比较函数对剩余元素进行排序
    std::sort(myvector.begin() + 4, myvector.end(), myfunction); // 结果: 12 32 45 71 (26 33 53 80)

    // 使用对象myobject作为比较函数对整个向量进行排序
    std::sort(myvector.begin(), myvector.end(), myobject);       // 结果: (12 26 32 33 45 53 71 80)

    // 打印向量的内容
    std::cout << "myvector contains:";
    for (std::vector<int>::iterator it = myvector.begin(); it != myvector.end(); ++it)
        std::cout << ' ' << *it;
    std::cout << '\n';

    return 0;
}
 递归方式实现:

1. hoare版本:

 

// 参数:
// a: 指向待排序整数数组的指针
// left: 数组最左边元素的索引
// right: 数组最右边元素的索引
// 返回值:
// 分区后枢纽元素所在的索引
int QuickSortPart1(int* a, int left, int right)
{
    // 找到枢纽元素并对数组进行分区
    int mid = partion(a, left, right); // 假设partion函数在其他地方定义
    // 将枢纽元素与最左边元素交换位置
    Swap(&a[left], &a[mid]);

    // 初始化用于分区的变量
    int begin = left;
    int end = right;
    int key = begin;

    // 执行分区操作
    while (begin < end)
    {
        // 将'end'指针向左移动,直到找到比枢纽元素小的元素
        while (begin < end && a[end] >= a[key])
            end--;

        // 将'begin'指针向右移动,直到找到比枢纽元素大的元素
        while (begin < end && a[begin] <= a[key])
            begin++;
        
        // 交换'begin'和'end'指针指向的元素
        Swap(&a[begin], &a[end]);
    }

    // 将枢纽元素放置到正确的位置
    Swap(&a[begin], &a[key]);

    return begin;
}
//一定注意是右边先走!!!

【注意】

为什么相遇位置比key要小?

 

2. 挖坑法版本:

 

// 参数:
// a: 指向待排序整数数组的指针
// left: 数组最左边元素的索引
// right: 数组最右边元素的索引
// 返回值:
// 分区后枢纽元素所在的索引
int QuickSortPart2(int* a, int left, int right)
{
    // 找到枢纽元素并对数组进行分区
    int mid = partion(a, left, right); // 假设partion函数在其他地方定义
    // 将枢纽元素与最左边元素交换位置
    Swap(&a[left], &a[mid]);

    // 保存枢纽元素的值
    int key = a[left];
    int hole = left;

    // 执行分区操作
    while (left < right)
    {
        // 将'right'指针向左移动,直到找到比枢纽元素小的元素
        while (left < right && a[right] >= key)
            right--;

        // 将找到的元素放入之前枢纽元素的位置
        a[hole] = a[right];
        hole = right;

        // 将'left'指针向右移动,直到找到比枢纽元素大的元素
        while (left < right && a[left] <= key)
            left++;

        // 将找到的元素放入之前枢纽元素的位置
        a[hole] = a[left];
        hole = left;
    }

    // 将枢纽元素放置到正确的位置
    a[hole] = key;

    return hole;
}

3. 前后指针版本:

​​​​​​​

// 参数:
// a: 指向待排序整数数组的指针
// left: 数组最左边元素的索引
// right: 数组最右边元素的索引
// 返回值:
// 分区后枢纽元素所在的索引
int QuickSortPart3(int* a, int left, int right)
{
    // 找到枢纽元素并对数组进行分区
    int mid = partion(a, left, right); // 假设partion函数在其他地方定义
    // 将枢纽元素与最左边元素交换位置
    Swap(&a[left], &a[mid]);

    // 初始化前后指针
    int prev = left;
    int cur = prev + 1;
    int key = left;

    // 执行分区操作
    while (cur <= right)
    {
        // 如果当前元素小于枢纽元素,则交换当前元素和前指针指向的元素
        if (a[cur] < a[key])
        {
            prev++;
            Swap(&a[prev], &a[cur]);
            cur++;
        }
        else
        {
            cur++;
        }
    }

    // 将枢纽元素放置到正确的位置
    Swap(&a[prev], &a[key]);
    key = prev;

    return key;
}
非递归方式实现(借助栈):
// 参数:
// a: 指向待排序结构体数组的指针
// left: 数组最左边元素的索引
// right: 数组最右边元素的索引
// 返回值:
// 分区后枢纽元素所在的索引
int PartSort1(STDataType* a, int left, int right)
{
    // 找到枢纽元素并对数组进行分区
    int mid = GetMid(a, left, right);
    // 将枢纽元素与最左边元素交换位置
    Swap(&a[left], &a[mid]);

    // 初始化起始和结束指针
    int begin = left;
    int end = right;
    int key = begin;

    // 执行分区操作
    while (begin < end)
    {
        // 将'end'指针向左移动,直到找到比枢纽元素小的元素
        while (begin < end && a[end] >= a[key])
            end--;

        // 将'begin'指针向右移动,直到找到比枢纽元素大的元素
        while (begin < end && a[begin] <= a[key])
            begin++;

        // 交换找到的元素的位置
        Swap(&a[begin], &a[end]);
    }

    // 将枢纽元素放置到正确的位置
    Swap(&a[key], &a[begin]);

    return begin;
}

// 参数:
// a: 指向待排序结构体数组的指针
// left: 数组最左边元素的索引
// right: 数组最右边元素的索引
void QuickSortNonR(STDataType* a, int left, int right)
{
    Stack st;
    StackInit(&st);
    StackPush(&st, right);
    StackPush(&st, left);

    // 非递归快速排序
    while (!StackEmpty(&st))
    {
        int begin = StackTop(&st);
        StackPop(&st);
        int end = StackTop(&st);
        StackPop(&st);
        int key = PartSort1(a, begin, end);

        // 将未排序部分压入栈中
        if (key + 1 < end)
        {
            StackPush(&st, end);
            StackPush(&st, key + 1);
        }
        if (key - 1 > begin)
        {
            StackPush(&st, key - 1);
            StackPush(&st, begin);
        }
    }

    StackDestroy(&st);
}

快速排序的特性总结:
1. 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
2. 时间复杂度:O(N*logN)

        ​​​​​​​        ​​​​​​​        

3. 空间复杂度:O(logN)
4. 稳定性:不稳定 

2.7 归并排序

https://en.wikipedia.org/wiki/Merge_sorticon-default.png?t=N7T8https://en.wikipedia.org/wiki/Merge_sort

2.7.1 基本思想

归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide andConquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。 归并排序核心步骤:

 

2.7.2 归并排序的实现 
递归方式实现:
/**
 * @brief 归并排序算法的实现函数,使用递归的方式进行排序
 * 
 * @param a 待排序数组的指针
 * @param begin 子数组的起始位置
 * @param end 子数组的结束位置
 * @param temp 临时数组用于存放排序结果
 */
void _MergeSort(int* a, int begin, int end, int* temp)
{
	if (begin >= end)
		return;
	
	// 计算中间位置
	int mid = (begin + end) / 2;
	
	// 递归调用_MergeSort对左右两个子数组进行排序
	_MergeSort(a, begin, mid, temp);
	_MergeSort(a, mid + 1, end, temp);
	
	// 合并两个有序子数组
	int begin1 = begin;
	int end1 = mid;
	int begin2 = mid + 1;
	int end2 = end;
	int i = begin; // i表示临时数组的下标
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] < a[begin2])
		{
			temp[i] = a[begin1];
			i++;
			begin1++;
		}
		else
		{
			temp[i] = a[begin2];
			i++;
			begin2++;
		}
	}
	
	// 处理剩余元素
	while (begin1 <= end1)
	{
		temp[i] = a[begin1];
		i++;
		begin1++;
	}
	while (begin2 <= end2)
	{
		temp[i] = a[begin2];
		i++;
		begin2++;
	}
	
	// 将排序后的结果拷贝回原数组
	memcpy(a + begin, temp + begin, sizeof(int) * ((end + 1) - begin));
}

/**
 * @brief 归并排序算法的入口函数,分配临时数组并调用_MergeSort函数
 * 
 * @param a 待排序数组的指针
 * @param n 数组的大小
 */
void MergeSort(int* a, int n)
{
	// 分配临时数组
	int* temp = (int*)malloc(sizeof(int) * n);
	if (temp == NULL)
	{
		perror("malloc fail");
		return;
	}
	
	// 调用_MergeSort函数对整个数组进行排序
	_MergeSort(a, 0, n - 1, temp);
	
	// 释放临时数组内存
	free(temp);
}
*/
非递归方式实现: 
// 这个函数以迭代方式实现归并排序算法,而不使用递归
// 参数:
// a:要排序的整数数组
// n:数组中元素的数量
// 返回值:void

void MergeSortNonR(int* a, int n)
{
    // 分配内存用于临时数组以存储排序后的元素
    int* temp = (int*)malloc(sizeof(int) * n);
    
    // 检查内存分配是否成功
    if (temp == NULL)
    {
        printf("malloc失败");
        return;
    }
    
    // 初始化要比较的元素之间的间隔
    int gap = 1;
    
    // 循环直到间隔小于元素数量
    while (gap < n)
    {
        // 以当前间隔大小的增量遍历数组
        for (int i = 0; i < n; i += (gap) * 2) 
        {
            // 定义要合并的两个子数组的边界
            int begin1 = i;
            int end1 = i + gap - 1;
            int begin2 = i + gap;
            int end2 = i + 2 * gap - 1;
            
            // 检查边界是否超过数组大小
            if (end1 >= n || begin2 >= n)
            {
                break;
            }
            
            // 如果结束边界超过数组大小,则调整它
            if (end2 >= n)
            {
                end2 = n - 1;
            }
            
            // 打印正在合并的子数组的边界
           // printf("[%2d,%2d][%2d,%2d]\n", begin1, end1, begin2, end2);

            int j = begin1;
            
            // 按顺序合并两个子数组
            while (begin1 <= end1 && begin2 <= end2)
            {
                if (a[begin1] < a[begin2])
                {
                    temp[j] = a[begin1];
                    j++;
                    begin1++;
                }
                else
                {
                    temp[j] = a[begin2];
                    j++;
                    begin2++;
                }
            }
            while (begin1 <= end1)
            {
                temp[j] = a[begin1];
                j++;
                begin1++;
            }
            while (begin2 <= end2)
            {
                temp[j] = a[begin2];
                j++;
                begin2++;
            }
            memcpy(a + i, temp + i, sizeof(int) * ((end2 - i) + 1));
        }
        gap *= 2;
    }
}

归并排序的特性总结:
1. 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
2. 时间复杂度:O(N*logN)
3. 空间复杂度:O(N)
4. 稳定性:稳定

2.8 计数排序

https://en.wikipedia.org/wiki/Counting_sorticon-default.png?t=N7T8https://en.wikipedia.org/wiki/Counting_sort

2.8.1 基本思想

计数排序是一种根据小正整数键对对象集合进行排序的算法。也就是说,它是一种整数排序算法。它通过对拥有不同键值的对象数量进行计数,并对这些计数应用前缀和来确定每个键值在输出序列中的位置来进行操作

2.8.2 计数排序的实现
// 参数:
// a:要排序的整数数组
// n:数组中元素的数量
// 返回值:void
void CountSort(int* a, int n)
{
    // 寻找数组中的最小值和最大值
    int min = a[0];
    int max = a[0];
    for (int i = 1; i < n; i++)
    {
        if (a[i] <= min)
        {
            min = a[i];
        }
        if (a[i] >= max)
        {
            max = a[i];
        }
    }
    
    // 计算数值范围
    int range = max - min + 1;
    
    // 分配内存用于计数数组
    int* count = (int*)calloc(range, sizeof(int));
    
    // 检查内存分配是否成功
    if (count == NULL)
    {
        printf("calloc失败");
        return;
    }
    
    // 统计每个元素出现的次数
    for (int i = 0; i < n; i++)
    {
        count[a[i] - min]++;
    }
    
    // 将排序后的元素放回原数组
    int i = 0;
    for (int j = 0; j < range; j++)
    {
        while (count[j]--)
        {
            a[i++] = j + min;
        }
    }
}

计数排序的特性总结: 

1. 时间复杂度:O(N+k) , 其中N为数组的长度,k为哈希表的长度
2. 空间复杂度:O(N+k), 需要用到一个哈希表和一个额外数组
3. 稳定性:稳定

3. 排序算法复杂度及稳定性分析

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/393465.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

自然语言编程系列(一):自然语言和程序语言介绍

1.自然语言和程序语言 自然语言和程序语言是两种截然不同但又相互关联的语言体系&#xff0c;它们分别服务于人类日常交流和计算机指令执行。 自然语言&#xff1a; 定义&#xff1a;自然语言是指人类在日常生活中使用的语言&#xff0c;如英语、汉语、法语等。它是非正式且灵…

FPFH特征描述符、对应关系可视化以及ICP配准

一、FPFH特征描述符可视化 C #include <pcl/point_types.h> #include <pcl/point_cloud.h> #include <pcl/search/kdtree.h> #include <pcl/io/pcd_io.h> #include <pcl/features/normal_3d_omp.h>//使用OMP需要添加的头文件 #include <boo…

Python一级考试笔记

Python一级考试笔记【源源老师】 前置知识&#xff1a;&#xff08;了解即可&#xff09; Python常见的几种编程环境&#xff1a;IDLE&#xff08;自带&#xff09;、Visual Studio Code、Jupyter、pyCharm&#xff1b; python版本&#xff1a;python3 和 python2&#xff08;…

开源模型应用落地-工具使用篇-向量数据库(三)

一、前言 通过学习"开源模型应用落地"系列文章&#xff0c;我们成功地建立了一个完整可实施的AI交付流程。现在&#xff0c;我们要引入向量数据库&#xff0c;作为我们AI服务的二级缓存。本文将详细介绍如何使用Milvus Lite来为我们的AI服务部署一个前置缓存。 二、术…

论文阅读_用模型模拟记忆过程

英文名称: A generative model of memory construction and consolidation 中文名称: 记忆构建和巩固的生成模型 文章: https://www.nature.com/articles/s41562-023-01799-z 代码: https://github.com/ellie-as/generative-memory 作者: Eleanor Spens, Neil Burgess&#xff…

python+pytest自动化测试函数测试类测试方法的封装

前言 今天呢&#xff0c;笔者想和大家聊聊pythonpytest接口自动化中将代码进行封装&#xff0c;只有将测试代码进行封装&#xff0c;才能被测试框架识别执行。 例如单个接口的请求代码如下&#xff1a; 1 2 3 4 5 6 import requests headers { "user-agent":…

证明之三条看似显然实则需要证明的陈述

三条看似显然实则需要证明的陈述 “表面显然的数学定理&#xff1a;隐藏的证明之谜” 较高等的数学中&#xff0c;有一点让很多人感到费解&#xff1a;其中有一些定理看上去非常显然&#xff0c;简直无须证明。遇到这样的定理时&#xff0c;人们常常会问&#xff1a;“如果这…

海外媒体发稿:掌握这8个东南亚媒体发稿的技巧-华媒舍

在如今的数字化时代&#xff0c;媒体的地位越来越重要&#xff0c;尤其在东南亚地区。了解如何在关键时刻掌握东南亚媒体发稿的技巧是非常重要的。本文将介绍8个在东南亚地区重要的媒体发稿技巧&#xff0c;帮助您更好地传达信息。 1. 熟悉目标媒体 要掌握东南亚媒体发稿的技巧…

如何基于YAML设计接口自动化测试框架?看完秒会!

在设计自动化测试框架的时候&#xff0c;我们会经常将测试数据保存在外部的文件&#xff08;如Excel、YAML、CSV&#xff09;或者数据库中&#xff0c;实现脚本与数据解耦&#xff0c;方便后期维护。目前非常多的自动化测试框架采用通过Excel或者YAML文件直接编写测试用例&…

.NET Core WebAPI中使用Log4net 日志级别分类并记录到数据库

一、效果 记录日志为文档 记录日志到数据库 二、添加NuGet包 三、log4net.config代码配置 <?xml version"1.0" encoding"utf-8" ?> <log4net><!-- Debug日志 --><appender name"RollingFileDebug" type"log4net…

抓包分析 TCP 协议

TCP 协议是在传输层中&#xff0c;一种面向连接的、可靠的、基于字节流的传输层通信协议。 环境准备 对接口测试工具进行分类&#xff0c;可以如下几类&#xff1a; 网络嗅探工具&#xff1a;tcpdump&#xff0c;wireshark 代理工具&#xff1a;fiddler&#xff0c;charles&…

论文阅读-EMS: History-Driven Mutation for Coverage-based Fuzzing(2022)模糊测试

一、背景 本文研究了基于覆盖率的模糊测试中的历史驱动变异技术。之前的研究主要采用自适应变异策略或集成约束求解技术来探索触发独特路径和崩溃的测试用例&#xff0c;但它们缺乏对模糊测试历史的细粒度重用&#xff0c;即它们在不同的模糊测试试验之间很大程度上未能正确利用…

【PyQt】在PyQt5的界面上集成matplotlib绘制的图像

文章目录 0 前期教程1 概述2 matplotlib2.1 库导入2.2 图片的各个部分解释2.3 代码风格2.4 后端 3 集成matplotlib图像到pyqt界面中3.1 使用到的模块3.2 理解Qt Designer中的“控件提升”3.3 界面与逻辑分离的思路3.4 扩展 0 前期教程 【PyQt】PyQt5进阶——串口上位机及实时数…

Android稳定性相关知识

关于作者&#xff1a;CSDN内容合伙人、技术专家&#xff0c; 从零开始做日活千万级APP。 专注于分享各领域原创系列文章 &#xff0c;擅长java后端、移动开发、商业变现、人工智能等&#xff0c;希望大家多多支持。 目录 一、导读二、概览三、相关方法论3.1 crash3.2 性能3.3 高…

一,STM32cubeMX配置FreeRTOS工程

从这篇文章开始&#xff0c;大家就进入到了FreeRTOS的学习之路。 那么&#xff0c;从这里开启学习的第一课如何使用cubeMX配置FreeRTOS工程 文章目录 前言一、使用 cubeMX 配置 FreeRTOS二、CMSIS 接口总结 前言 一、使用 cubeMX 配置 FreeRTOS 选择 stm32 芯片。 选择外部晶振…

【Linux】简单的网络计算器的实现(自定义协议,序列化,反序列化)

文章目录 前言一、 服务端1.ServerCal.cc&#xff08;服务器主文件&#xff09;2.ServerCal.hpp3.Sock.hpp(套接字封装)4.TcpServer.hpp(服务器)5.Protocol&#xff08;自定义协议&#xff09; 二、用户端1.ClientCal 三、Log.hpp&#xff08;日志&#xff09;四、makefile 前言…

洛谷 P8627 [蓝桥杯 2015 省 A] 饮料换购

参考代码and代码解读 #include <bits/stdc.h> using namespace std; int main() { int n; scanf("%d", &n); int dr;//drdrink; dr n;//把drink赋值于n; while (n > 2) {//剩余的总瓶盖数要大于二,才能换得下一瓶饮料; dr n…

C语言系列-带有副作用的宏参数#和##命名约定宏替换的规则

&#x1f308;个人主页: 会编辑的果子君 &#x1f4ab;个人格言:“成为自己未来的主人~” 目录 带有副作用的宏参数 宏替换的规则 宏函数的对比 #和## #运算符 ##运算符 命名约定 #undef 带有副作用的宏参数 当宏参数在宏的定义中出现超过一次的时候&#xff0c;如果…

树莓派登录方式

目录 1.串口登录树莓派 1.1 USB-TTL连接树莓派串口 1.2 修改系统配置&#xff0c;启用串口登录树莓派 1.3 启动树莓派 2.网络方式登录树莓派 2.1 使树莓派接入网络 2.2 网络SSH 方式登录树莓派 2.2.1 打开ssh功能&#xff0c; 输入命令&#xff1a; 1.串口登录树莓派 1…

MOS管故障排查(G极电阻篇)

我们经常看到&#xff0c;在电源电路中&#xff0c;功率MOS管的G极经常会串联一个小电阻&#xff0c;几欧姆到几十欧姆不等&#xff0c;那么这个电阻用什么作用呢&#xff1f; 如上图开关电源&#xff0c;G串联电阻R13这个电阻的作用有2个作用&#xff1a;限制G极电流&#x…