Bootstrap

数据结构——经典排序详解(堆,冒泡,选择,插入,希尔,归并......)比较类排序

排序的分类

排序:指按照一定的顺序排列一组数据元素的过程,以便对数据进行信息管理以及数据分析。(以下都通过升序进行解释)

排序
比较类
非比较类
冒泡排序
选择排序
插入排序
快速排序
堆排序
希尔排序
归并排序
基数排序
计数排序
桶排序

1. 冒泡排序

将数组头部元素与相邻元素进行比较,若前者大于后者,则交换位置,直至末尾;一轮比较后,最大的元素就置于末尾,接下来,再将除去最大元素的子数组重复上述操作,直至数组有序。
void Swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}
void BubbleSort(int* a, int n)
{
	for (int j = 0; j < n; j++)
	{
		bool exchange = true;
		for (int i = 1; i < n-j; i++)
		{
			if (a[i - 1] > a[i])
			{
				Swap(&a[i-1], &a[i]);
				exchange = false;
			}
		}
		if (exchange == true)
			break;
	}
}

细节:在单趟比较中,给i 赋值1,是为了在最后一次比较中,数组越界;使用布尔类型进行逻辑判断,是为了在数组有序是,无需比较。
时间复杂度最好情况:O(n)
时间复杂度最坏情况:O(n2)
稳定性:好(在元素大小相同时,不需要交换)

2、插入排序

在这里插入图片描述
基本思想:

  • 首先将数组分为已排序和未排序两部分。初始时,已排序部分只有第一个元素,未排序部分包含其余元素。
  • 从未排序部分中取出第一个元素,与已排序部分的所有元素进行比较,找到合适的位置插入,使得已排序部分始终维持有序。
  • 继续从未排序部分取出元素,执行上述插入操作,直到未排序部分为空,整个数组有序。
void InserSort(int* a, int n)
 {
    // 遍历数组,从第二个元素开始,因为只有一个元素时默认已排序
    for (int i = 1; i < n; ++i)
     {
        // 将当前元素作为插入的目标,存储为临时变量tmp
        int tmp = a[i];
        int end = i-1; // end表示已排序部分的最后一个元素的索引

        // 在已排序部分中找到tmp的插入位置
        while (end >= 0) 
        {
         // 从后往前遍历已排序部分
            if (tmp < a[end]) { // 当前元素比tmp大,需要向后移动
                a[end + 1] = a[end]; // 将较大的元素向后移动一位
                end--; // 继续向前比较
            } else 
            {
                break; // 找到合适位置,退出循环
            }
        }

        // 将tmp插入到正确的位置
        a[end + 1] = tmp;
    }
}

3、希尔排序

希尔排序是一种基于插入排序的排序算法。它是插入排序的一种优化版本,通过分组插入排序和缩小增量的方式,大幅度减少了逆序对的数量,从而提高了排序效率。

  • 希尔排序将数组分成若干个子序列,每个子序列通过插入排序进行排序。
  • 初始步长为数组长度的一半,每次循环后步长减半,直到步长为1(当gap为1是,就是插入排序)。
  • 对每个步长,从索引 gap 开始,将每个元素插入到当前分组的正确位置。
  • 通过内部循环,逐步将分组中较大的元素向后移动,为当前元素腾出插入位置。
    在这里插入图片描述
void ShellSort(int* a, int n) 
{
    int gap = n / 2; // 初始化步长,通常从数组长度的一半开始
    while (gap >= 1)
     {
        gap /= 2; // 每次循环将步长减半,直到步长为1
        for (int j = 0; j < gap; j++) 
        {
            // 处理每个分组,每个分组的起始位置为j,步长为gap
            for (int i = j; i < n - gap; i += gap) 
            {
                int end = i; // 当前已排序分组的末尾索引
                int tmp = a[i + gap]; // 需要插入的元素
                while (end >= 0) 
                {
                    // 将较大的元素向后移动,为tmp腾出位置
                    if (tmp < a[end])
                     {
                        a[end + gap] = a[end];
                        end -= gap; // 继续向前比较
                    } else 
                    {
                        break; // 找到合适位置,退出循环
                    }
                }
                a[end + gap] = tmp; // 将tmp插入到正确的位置
            }
        }
    }
}
  • 插入排序的平均和最坏时间复杂杂度为O(n2)
  • 希尔排序的平均时间复杂度为O(n3/2)
  • 在插入排序中,如遇到相同元素,会插入到其后面,保证了相对顺序的不变,故插入排序是稳定的。
  • 在希尔排序中,元素被分组并跳跃式比较与交换,相等元素的相对位置可能会被改变,故希尔排序是不稳定的。

在这里插入图片描述

4、快速排序

快速排序是一种基于分治法的排序算法。从数组中选取一个基准值,通过一趟排序,将其分为独立的两部分,左边数组小于基准值,右边数组大于基准值,再进行递归处理。
思路:

  • 首先找到一个基准值,将其置于数组最左侧(也可以至于最右侧)
  • 从数组的尾部开始寻找小于基准值的数(从右向左),找到后,再从数组的头部寻找大于基准值的数(从左向右)
  • 二者都找到后,在交换它们的值,直至相遇,相遇的值一定比基准值小,再将其与基准值交换
  • 递归其子数组
    在这里插入图片描述
    若想排序变得更加有效率,则需要获得一个合适的基准值,可以在头部,尾部和中部,选取一个中间值作为基准值
// 三数取中函数
int GetMidNumi(int* a, int left, int right) {
	int mid = (left + right) / 2;
		if (a[mid] > a[left])
		{
			if (a[right] > a[mid])
			{
				return mid;
			}
			else if (a[left] > a[right])
			{
				return left;
			}
			else
				return right;
		}
		else//a[mid] <= a[left]
		{
			if (a[right] < a[mid])
			{
				return mid;
			}
			else if (a[right] > a[left])
			{
				return left;
			}
			else
				return right;
		}
}

// 交换函数
void Swap(int* a, int* b) {
	int temp = *a;
	*a = *b;
	*b = temp;
}

// 快速排序函数
void QuickSort(int* a, int left, int right) {
	if (left >= right) {
		return;
	}
	int begin = left, end = right;
	//三数取中
	int midi = GetMidNumi(a, left, right);// 获取基准值索引
	Swap(&a[midi], &a[left]);// 将基准值交换到数组的最左端
	int keyi = left;
	while (left < right)
	{
		// 在右侧找到小于等于基准值的元素
		while (left < right && a[right] >= a[keyi])
			--right;
		// 在左侧找到大于等于基准值的元素
		while (left < right && a[left] <= a[keyi])
			++left;
		Swap(&a[left], &a[right]);
	}
	// 将基准值交换到正确的位置
Swap(&a[keyi], &a[left]);
keyi = left;

	// 递归排序左右子数组
	QuickSort(a, begin, keyi - 1);
    QuickSort(a, keyi+1, end);
}

(挖坑法)
在这里插入图片描述

  • 选择基准值:选择数组中的一个元素作为基准值
  • 从右向左找到第一个小于基准值的元素,将其放入左边的“坑”中。
  • 从左向右找到第一个大于基准值的元素,将其放入右边的“坑”中。
  • 重复上述步骤,直到 left 和 right 相遇或交错。
  • 放置基准值:将基准值放入最终的“坑”中,此时基准值已经归位
void QuickSort(int* a, int left, int right)//挖坑法
{
	if (left >= right)
	{
		return;
	}
	int begin = left, end = right;
	int midi = GetMidNumi(a, left, right);
	Swap(&a[midi], &a[left]);
	int key = a[left];
	int hole = left;
	while (left < right)
	{
		while (left < right && a[right] >= key)
		

			right--;
			a[hole] = a[right];
			hole = right;
		while (left < right && a[left] <= key)
			left++;
			a[hole] = a[left];
			hole = left;
	}
	a[hole] = key;
	QuickSort(a, begin,hole-1);
	QuickSort( a, hole+1, end);
}

(前后指针法)
在这里插入图片描述

  • 选择基准值: 一般取序列的中间元素。
  • 若cur找到比key小的值,++prev;当cur的值和prev的值不相等时,则交换它们的值
  • 继续++cur
  • 若cur的值比key大,则++cur
    1、prev要么紧跟着cur
    2、prev和cur之间隔着一段比key大的区间值
//前后指针法
void QuickSort(int* a, int left, int right)
{
	if (left >= right)
	{
		return;
	}
	int begin = left, end = right;
	int midi = GetMidNumi(a, left, right);
	Swap(&a[midi], &a[left]);
	int key = a[left];
	int keyi = left;
	int prve = left, cur = left + 1;
	while (cur <= right)
	{
		if (a[cur] < key && ++prve != cur)
			Swap(&a[cur], &a[prve]);
		++cur;
	}
	Swap(&a[prve], &a[left]);
	keyi = prve;
	QuickSort(a,begin,keyi-1);
	QuickSort(a,keyi+1,end);
}

QuickSort非递归的写法

5、归并排序

归并排序也是一种基于分治法的排序算法。将待排序数组分为若干小数组,直至小数组只有一个元素,再递归的对子数组进行排序,将相邻的两个有序子数组合并为一个新的有序数组。合并时,通过双指针比较元素,依次选择较小者放入临时数组,剩余元素直接追加。
在这里插入图片描述
在这里插入图片描述

// 归并排序辅助函数
void _Merge(int* a, int begin, int end, int* tmp)
{
    // 如果子数组只有一个元素或空,直接返回
    if (begin >= end) 
    {
        return;
    }

    int mid = (begin + end) / 2; // 计算中间点

    
    _Merge(a, begin, mid, tmp); // 递归排序左半部分
    _Merge(a, mid + 1, end, tmp); // 递归排序右半部分

    int begin1 = begin; // 左子数组的起始索引
    int end1 = mid; // 左子数组的结束索引
    int begin2 = mid + 1; // 右子数组的起始索引
    int end2 = end; // 右子数组的结束索引
    int i = begin1; // 临时数组的索引

    // 合并两个有序子数组到临时数组
    while (begin1 <= end1 && begin2 <= end2)
    {
        if (a[begin1] < a[begin2])
         {
            tmp[i++] = a[begin1++]; // 左子数组的元素较小,放入临时数组
        }
        else 
        {
            tmp[i++] = a[begin2++]; // 右子数组的元素较小,放入临时数组
        }
    }

    // 将左子数组剩余元素拷贝到临时数组
    while (begin1 <= end1) 
    {
        tmp[i++] = a[begin1++];
    }

    // 将右子数组剩余元素拷贝到临时数组
    while (begin2 <= end2)
    {
        tmp[i++] = a[begin2++];
    }

    // 将临时数组中的有序元素拷贝回原数组
    memcpy(a + begin, tmp + begin, sizeof(int) * (end - begin + 1));
}

// 归并排序主函数
void MergeSort(int* a, int n)
{
    int* tmp = (int*)malloc(sizeof(int) * n); // 分配临时数组
    if (tmp == NULL) {
        perror("malloc fail"); // 内存分配失败
        return;
    }

    _Merge(a, 0, n - 1, tmp); // 调用归并排序辅助函数

    free(tmp); // 释放临时数组
}

归并排序非递归的写法

6、选择排序

  • 从数组的第一个元素开始,找到整个数组中的最小值,并将其与数组的第一个元素交换位置。
  • 从数组的第二个元素开始,找到剩余未排序元素中的最小值,并将其与数组的第二个元素交换位置。
  • 不断重复上述过程,每次从当前未排序的部分选择最小元素,并将其交换到已排序部分的末尾,直到整个数组排序完成。
// 交换函数,用于交换两个整数的值
void swap(int* p1, int* p2) 
{
    int tmp = *p1;       
    *p1 = *p2;          
    *p2 = tmp;          
}

// 选择排序函数
void selectsort(int* a, int n) {  
    int left = 0;         // "left" 表示当前已排序部分的末尾索引
    int mini = left;      // "mini" 用于存储最小值的索引
    // 遍历数组,每次确定一个元素的位置
    while (left < n)
     {    
        // 在未排序的部分中寻找最小值的索引
        for (int i = left + 1; i < n; i++)
         {  
            if (a[i] < a[mini])
             { 
                // 如果找到更小的值,更新最小值的索引
                mini = i;  
            }
        }
        // 将找到的最小值与已排序部分末尾的元素交换
        swap(&a[mini], &a[left]);  
        left++;           
    }
}

7、堆排序

详情请点击该链接:堆排序

;