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

news2024/11/28 2:35:09

 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. 排序算法复杂度及稳定性分析

 

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

MySQL篇之主从同步原理

一、原理 MySQL主从复制的核心就是二进制日志。 二进制日志&#xff08;BINLOG&#xff09;记录了所有的 DDL&#xff08;数据定义语言&#xff09;语句和 DML&#xff08;数据操纵语言&#xff09;语句&#xff0c;但不包括数据查询&#xff08;SELECT、SHOW&#xff09;语句。…

【开源】SpringBoot框架开发教学过程管理系统

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 教师端2.2 学生端2.3 微信小程序端2.3.1 教师功能如下2.3.2 学生功能如下 三、系统展示 四、核心代码4.1 查询签到4.2 签到4.3 查询任务4.4 查询课程4.5 生成课程成绩 六、免责说明 一、摘要 1.1 项目介绍 基于JAVAVu…

DS:八大排序之归并排序、计数排序

创作不易&#xff0c;感谢三连支持&#xff01;&#xff01; 一、归并排序 1.1 思想 归并排序&#xff08;MERGE-SORT&#xff09;是建立在归并操作上的一种有效的排序算法,该算法是采用分治法&#xff08;Divide andConquer&#xff09;的一个非常典型的应用。将已有序的子…

序列发生器

一开始想直接FSM&#xff0c;划分出6状态依次输出对应的。但其实只要6比特的移位寄存器&#xff0c;每次输出高位。复位后的默认值时6’b001_011。这样就可以实现循环&#xff0c;这种移位寄存器也叫barrel_shifter。循环移位。也可以使用循环计数器&#xff0c;然后case计数器…

机试指南:3-4章

文章目录 第3章 排序与查找(一) 排序1.sort函数&#xff1a;sort(first,last,comp)2.自定义比较规则3.C函数重载&#xff1a;同一个函数名&#xff0c;有不同的参数列表4.机试考试最重要的事情&#xff1a;能把你曾经做过的题目&#xff0c;满分地做出来5.例题例题1&#xff1a…

找座位 - 华为OD统一考试(C卷)

OD统一考试(C卷) 分值: 100分 题解: Java / Python / C++ 题目描述 在一个大型体育场内举办了一场大型活动,由于疫情防控的需要,要求每位观众的必须间隔至少一个空位才允许落座。 现在给出一排观众座位分布图,座位中存在已落座的观众,请计算出,在不移动现有观众座位…

全国工商企业名录

全国2023年12月份企业名录2.5亿条

成功交易者需要历经多少磨难才能成就辉煌?

前言 王国维在《人间词话》中说&#xff1a;古今之成大事业、大学问者&#xff0c;必经过三种之境界&#xff1a;“昨夜西风凋碧树&#xff0c;独上高楼&#xff0c;望尽天涯路。” 此第一境也。“ 衣带渐宽终不悔&#xff0c;为伊消得人憔悴。” 此第二境也。“众里寻他千百度…

2023年【四川省安全员B证】最新解析及四川省安全员B证新版试题

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 2023年四川省安全员B证最新解析为正在备考四川省安全员B证操作证的学员准备的理论考试专题&#xff0c;每个月更新的四川省安全员B证新版试题祝您顺利通过四川省安全员B证考试。 1、【多选题】《建筑施工安全检查标准…

振弦采集仪在地铁隧道振动监测中的应用研究

振弦采集仪在地铁隧道振动监测中的应用研究 河北稳控科技振弦采集仪是一种用于测量振动信号的设备&#xff0c;广泛应用于结构和地铁隧道等工程监测中。在地铁隧道振动监测中&#xff0c;振弦采集仪可以提供以下方面的应用研究&#xff1a; 1. 地铁隧道振动监测&#xff1a;振…

测试演示文档1770532693631507859

测试录入文档 这是一个标题 正文正文 段落1段落2 随机数YY 第二张图 XXXY ct-1708314487159)] XXXY

第十三章[管理]:13.2:venv虚拟环境

一,虚拟环境 1,虚拟环境是什么? venv是Python的一个标准库,用于创建和管理虚拟环境。 虚拟环境是Python的一个隔离空间,在这个空间中, 你可以安装和管理Python包,而不会影响到系统中的其他Python环境。 2,虚拟环境的特点: 隔离性:每个虚拟环境都有自己的Python解释器…

在script标签写export为什么会抛错|type module import ES5 ES6 预处理 指令序言 JavaScript JS

今天我们进入到语法部分的学习。在讲解具体的语法结构之前&#xff0c;这一堂课我首先要给你介绍一下 JavaScript 语法的一些基本规则。 脚本和模块 首先&#xff0c;JavaScript 有两种源文件&#xff0c;一种叫做脚本&#xff0c;一种叫做模块。这个区分是在 ES6 引入了模块…

Windows制作Windows的U盘启动盘

概要&#xff1a; 本篇演示在Windows10系统中制作Windows10的U盘启动盘 一、下载Windows10的iso文件 自行去微软官方网站下载Windows10的iso文件 二、下载Ultraiso 在浏览器输入https://www.ultraiso.com进入ultraiso官网 点击FREE TRIAL下载免费试用版 下载好的文件&a…

PyCharm 取消所有断点

PyCharm 取消所有断点 1. Run -> View Breakpoints...2. Python Line Breakpoint3. Remove - DoneReferences 1. Run -> View Breakpoints… 2. Python Line Breakpoint ​​​ 3. Remove - Done References [1] Yongqiang Cheng, https://yongqiang.blog.csdn.net/

CrossOver For Mac v24.0.0 Beta3 在Mac运行Windows软件

CrossOver For Mac v24.0.0 是一款系统兼容软件&#xff0c;让您可以在 Mac 和 Linux 系统上运行 Windows 应用&#xff0c;不必购买 Windows 授权&#xff0c;不必重启系统&#xff0c;不必使用虚拟机。通过 CrossOver&#xff0c; 您可以从 dock 直接启动 Windows 应用&#…

Linux环境下HTTP隧道的安全性与性能优化策略

在Linux环境下&#xff0c;HTTP隧道技术为远程办公和数据同步提供了极大的便利。为了确保这种技术的安全性和性能&#xff0c;我们需要采取一系列策略来保障其稳定运行。 安全性策略 安全性是HTTP隧道技术的首要考虑因素。以下是一些建议&#xff0c;以确保HTTP隧道的安全性&…

系统测试计划(直接套用实际)

1测试目的 2测试范围 3资源要求 3.1人力资源 3.2指派干系人 3.3测试环境 3.4测试工具 4测试类型 5测试安排 5.1测试进度 5.2测试策略 5.2.1测试需求 5.2.2测试类型 6测试停止标准 7测试风险 8缺陷管理 8.1缺陷属性 8.2缺陷类型 8.3缺陷严重程度 8.4缺陷优先级 8.5缺陷状态 8.6缺…

SmartForms笔记

目录 表Table表体区域脚标效果展示 带自增编号的结构新建循环设置变量的位置运行结果 表Table 右击“创建”—“表”&#xff1a; 绘制布局如下所示&#xff1a; 选择第一行&#xff0c;选择成“%LTYPE1”&#xff1a; 选择第二行&#xff0c;选择成“%LTYPE2”&#xff0c…

IDEA无法下载源代码(Maven无法下载源代码)

问题 这两天在突击SpringBoot&#xff0c;按ctrl打开一个SpringBoot的类想要下载源代码&#xff0c;居然下载不了&#xff0c;右下角弹窗提示无法下载源代码 百度查了查&#xff0c;在本地仓库里下载失败的目录下有个.lastUpdate的文件记录了一些信息 #NOTE: This is a Maven…