数据结构排序小结

news2025/1/17 0:18:47

排序类型小结

  • 💦 插入排序
    • 直接插入排序
    • 希尔排序
  • 💦 选择排序
    • 直接选择排序
    • 堆排序
  • 💦 交换排序
    • 冒泡排序
    • 快速排序🐾
      • ==霍尔版本==
      • ==补坑位版本==
      • ==前后指针版本==
      • ==非递归版本==
  • 💦 归并排序
      • ==递归版本==
      • ==非递归版本==
  • 💦 性能测试

💦 插入排序

直接插入排序

🔑 核心思想 🔑
请添加图片描述

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

❗ 过程:❕

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

❗ 直接插入排序的特性总结:❕

1️⃣ 元素集合越接近有序,直接插入排序算法的时间效率越高

2️⃣ 时间复杂度:O(N^2)

3️⃣ 空间复杂度:O(1),它是一种稳定的排序算法

4️⃣ 稳定性:稳定
  
🐾代码实现:

void InsertSort(int* a, int n)
{
	//注意这里的n-1
	for (int i = 0; i < n-1; i++)
	{
		int end = i;
		int tmp = a[end + 1];
		while (end >= 0)
		{
			if (tmp < a[end])
			{
				//后移数组
				a[end + 1] = a[end];
				end--;
			}
			else
			{
				break;
			}
		}
		a[end + 1] = tmp;
	}
}

希尔排序

希尔排序 (缩小增量排序)

🔑 核心思想 🔑

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

在这里插入图片描述

🤖希尔排序的时间复杂度并不好计算,因为 gap 的取值方法很多,导致很难去计算,因此在好些数中给出的希尔排序的时间复杂度都不固定,官方给出的时间复杂度是 O(N1.3)

以下是其粗略的计算方法,可见非常复杂

在这里插入图片描述
🐾代码实现

//希尔排序
void ShellSort(int* a, int n)
{
	int gap = n;
	//gap>1时是预排,目的是让他接近有序
	//gap==1是直接插入排序,目的是让他有序
	while (gap > 1)
	{
		gap = gap / 3 + 1;
		//多组并排
		for (int i = 0; i < n - gap; i++)
		{
			int end = i;
			int tmp = a[end + gap];
			while (end >= 0)
			{
				if (tmp < a[end])
				{
					a[end + gap] = a[end];
					end -= gap;
				}
				else
					break;
			}
			a[end + gap] = tmp;
		}
	}
}

❗ 希尔排序特性总结 ❕

1️⃣ 希尔排序是对直接插入排序的优化

2️⃣ 当 gap > 1 时都是预排序,目的是让数组更接近于有序。当 gap == 1 时,其实就是直接插入排序,且数组已经接近有序的了。整体而言,可以达到优化的效果,我们实现后可以进行性能测试的对比

3️⃣ 希尔排序的时间复杂度并不好计算,因为 gap 的取值方法很多,导致很难去计算,因此在好些数中给出的希尔排序的时间复杂度都不固定,官方给出的时间复杂度是 O(N1.3)

4️⃣ 稳定性:不稳定

💦 选择排序

直接选择排序

🔑 核心思想 🔑

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

❗ 过程:❕

1️⃣ 在元素集合 array[i] - array[n-1] 中选择关键码最大 (小) 的数据元素

2️⃣ 若它不是这组元素中的最后一个(或第一个)元素,则将它与这组元素中的最后一个(或第一个)元素交换

3️⃣ 在剩余的 array[i] - array[n-2] (array[i+1]–array[n-1]) 集合中,重复上述步骤,直到集合剩余 1 个元素

❗ 直接选择排序的特性总结:❕

1️⃣ 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用

2️⃣ 时间复杂度:O(N^2) - 最好 / 最坏都是如此,当数据极其无序时,比冒泡排序还要拉跨

3️⃣ 空间复杂度:O(1)

4️⃣ 稳定性:不稳定

❗ 动图演示:❕
请添加图片描述
🐾代码实现


//与上述动图一样的代码
void Swap(int* px, int* py)
{
	int temp = *px;
	*px = *py;
	*py = temp;
}
void SelectSort(int* a, int n)
{
	int i = 0;
	int begin = 0;
	while (begin < n)
	{
		int mini = begin;
		//选最小
		for (i = begin; i < n; i++)
		{
			if (a[i] < a[mini])
			{
				mini = i;
			}
		}
		//交换
		Swap(&a[begin], &a[mini]);
		//迭代
		begin++;
	}
}

//优化版本,最大值最小值同时找
void SelectSort(int* a, int n)
{
	int begin = 0, end = n - 1;
	while (begin < end)
	{
	  //一趟排序,将最大数最小数的下标都初始化成begin
		int mini = begin, maxi = begin;
		//遍历数组,找到最大数和最小数的下标
		for (int i = begin + 1; i <= end; ++i)
		{
			if (a[i] < a[mini])
			{
				mini = i;
			}
			if (a[i] > a[maxi])
			{
				maxi = i;
			}
		}

		Swap(&a[begin], &a[mini]);
		//如果没有这个if判断,最大值的下标指向会随着begin的mini的交换使得max的值丢失
		//当maxi==begin时,maxi指向min,下一次交换就会出问题!!!
		if (maxi == begin)
		{
			maxi = mini;
		}
		Swap(&a[end], &a[maxi]);
        //迭代
		++begin;
		--end;
	}
}

堆排序

🔑 核心思想 🔑

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

❗ 堆排序的特性总结:❕

1 堆排序使用堆来选数,效率就高了很多。

2. 时间复杂度:O(N*logN)

3.空间复杂度:O(1)

4.稳定性:不稳定

void Swap(int* px, int* py)
{
	int temp = *px;
	*px = *py;
	*py = temp;
}
//向上调整算法
void AdjustUp(int*a,int child)
{
  int parent=(child-1)/2;
  while(child > 0)
  {
       //这里的符号决定调整为大堆还是小堆,这里以大堆为例
     if(a[child]>a[parent])
     {
      Swap(&a[child],&a[parent]);
      child=parent;
      parent=(child-1)/2;
     }
     else
     {
       break;
     }
     
  }
}
//向下调整算法
void AdjustDown(int* a, int size, int parent)
{

	int child = parent * 2 + 1;
	while (child < size)
	{
	//假设左孩子小,如果假设错了,更新一下
	//这样操作后,child指向的就是两个孩子中较小的那一个
		if (a[child+1] < a[child] && child + 1 < size)
		{  
			child++;
		}
		//如果孩子大,就交换(决定了大堆还是小堆)
		if (a[child] > a[parent])
		{
			Swap(&a[child], &a[parent]);
			//交换后,更新下标
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}
//排升序
void HeapSort(int* a, int n)
{
  //建大堆
  for(int i=0;i<n;i++)
  {
    AdjustUp(a,i); 
  }
  int end=n-1;
  while(end>0)
  {
    //交换后最大的数就排好了
     Swap(&a[0],&a[end]);
     //将前面的数再调整,选出次小的数
     AdjustDown(a,end,0);
     end--;
  }
}

方法二:用向下调整算法建堆

void Swap(int* px, int* py)
{
	int temp = *px;
	*px = *py;
	*py = temp;
}

//排升序
void HeapSort(int* a, int n)
{
	//建大堆
	int i = 0;
	//n-1就是最后一个元素下标,再-1,除以二就是其父节点,也就是倒数第一个非叶子节点
	//从这开始从下往上向下建堆
	for (i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(a, n, i);
	}
	int end = n - 1;
	//交换并删除堆中最后一个元素
	while (end > 0)
	{
	   //首尾交换
		Swap(&a[0], &a[end]);
		//重新建大堆
		AdjustDown(a, end, 0);
		end--;
	}
}

  • 倒着调整叶子节点不需要处理(因为叶子节点没有子节点无法向下比较),从倒数第一个非叶子节点开始,即最后一个节点的父节点开始调整,从下往上向下建堆。

  • 在HeapSort函数中,第一个循环调用了AdjustDown函数,将待排序数组构建成了一个大堆。但是,这个大堆并不是完全有序的,只是满足了大堆的性质,即每个节点的值都大于或等于其左右子节点的值。因此,需要进行第二个while循环,将大堆根中的元素依次取出,交换堆顶元素和数组末尾元素,并重新调整大堆,直到整个数组有序。

  • 第二个while循环中,将堆顶元素与数组末尾元素交换,然后将剩余元素重新调整为大根堆。这样,每次交换后,数组末尾的元素就是当前大根堆中的最大值,而剩余元素仍然满足大根堆的性质。重复以上步骤,直到整个数组有序。

💦 交换排序

冒泡排序

请添加图片描述

// 时间复杂度:O(N^2)
// 最好情况是多少:O(N)
void BubbleSort(int* a, int n)
{
	for (int j = 0; j < n; j++)
	{
	   //定义exchange可以优化冒泡排序,当数据已经有序时,可以提前结束排序
		bool exchange = false;
		for (int i = 1; i < n-j; i++)
		{
			if (a[i - 1] > a[i])
			{
				Swap(&a[i - 1], &a[i]);
				exchange = true;
			}
		}

		if (exchange == false)
			break;
	}

快速排序🐾

🔑 核心思想 🔑

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

❗ 过程:❕

霍尔版本

请添加图片描述

1️⃣ 选出一个关键字 key,一般是头或者尾

2️⃣ 经过一次单趟后,key 放到了正确的位置,key 左边的值比 key 小,key 右边的值比 key 大

3️⃣ 再让 key 的左边区间有序、key 的右边区间有序

为什么相遇的位置的数不会比key大呢?
因为右边先走🐳🐳🐳🐳!!!
我们来分析一波,首先要明确R的任务是找小数,L的任务是找大数,相遇无非就分两种情况
1.R遇到L:这种情况下又分两种
第一种情况:L指向begin开始的位置,压根没动,这样当R走来是相遇于key这个位置,二者相等并不影响排序结果
第二种情况:R成功找到一个比key小的数,L也成功找到一个比key大的数,按照快排规则,此时L和R指向的数应该交换了,那么在交换后,L位置的数就比key要小了(这个过程可能不止一趟),最终R会与L相遇在L位置,这个位置的数必定是比key要小的
2.L遇到R:因为刚开始是R先走的,那么R停下一定是遇到了比key要小的数,(这个过程也可能不止一次)然后L开始向右走,与R在R位置相遇,这个位置的值一定是比key要小的

//简易版本
QuickSort(int* a,int begin,int end)
{
  	if(begin>=end)
  	return;
  	//注意left的起始位置,如果这里left=begin+1,当数组有序时会出Bug
  	int keyi=begin,left=begin,right=end;
  	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[left],&a[keyi]);
   //更新下一次递归时区间边界
   keyi=left;
   //与二叉树的先序遍历有异曲同工之妙
   QuickSort(a,begin,keyi-1);
   QuickSort(a,keyi+1,end);
}

在这里插入图片描述

上述代码每次递归都将keyi的值给成begin左边,在最坏情况下,时间复杂度会成为O(N^2),并且调用函数次数与数据个数密切相关,当待排序的数组数据过多时会出现栈溢出,导致被覆盖的内存区域中的数据损坏,从而导致程序崩溃或运行不正常。

要想优化上述情况,就要使得每次选取的key尽量在中间,我们可以选begin,end,midi三个下表对应数组中的值的中位数,用GetMidi函数分装,返回中间值的下标,然后在主函数中交换a[begin]和a[midi]的值,这样操作后a[begin]的值在整个数组中处于更居中的位置,再递归子区间就会缩短递归的区间长度和次数,大大优化代码。

//优化代码
//三数取中函数
int GetMidi(int* a, int begin, int end)
{
	int midi = (begin + end) / 2;
	// begin end midi三个数选中位数
	if (a[begin] < a[midi])
	{
		if (a[midi] < a[end])
			return midi;
		else if (a[begin] > a[end])
			return begin;
		else
			return end;
	}
	else//a[begin] > a[midi]
	{
		if (a[midi] > a[end])
			return midi;
		else if (a[begin] > a[end])
			return end;
		else
			return begin;
	}
}

void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)
		return;

	int midi = GetMidi(a, begin, end);
	Swap(&a[midi], &a[begin]);

	int left = begin, right = end;
	int keyi = begin;

	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[left], &a[keyi]);
	keyi = left;

	// [begin, keyi-1] keyi [keyi+1, end]
	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi + 1, end);
}

对于优化后的代码,还存在小区间占用过多栈空间的问题,如下图,假定在理想递归情况下,我们每次寻找的key都排到了数组中间,那么递归的函数栈空间展开就是一颗二叉树,当我们递归到二叉树后三层之后,开辟的栈空间居然占了整棵树的87.5%!!
然而后三层需要排的数据个数并不多,根本不需要浪费这么多空间,我们可以针对后三层单独采用插入排序进行再优化。
在这里插入图片描述

//对小区间处理的插入排序函数
void InsertSort(int* a, int n)
{
	//注意这里的n-1
	for (int i = 0; i < n-1; i++)
	{
		int end = i;
		int tmp = a[end + 1];
		while (end >= 0)
		{
			if (tmp < a[end])
			{
				//后移数组
				a[end + 1] = a[end];
				end--;
			}
			else
			{
				break;
			}
		}
		a[end + 1] = tmp;
	}
}
//找中位数函数
int GetMidi(int* a, int begin, int end)
{
	int midi = (begin + end) / 2;
	// begin end midi三个数选中位数
	if (a[begin] < a[midi])
	{
		if (a[midi] < a[end])
			return midi;
		else if (a[begin] > a[end])
			return begin;
		else
			return end;
	}
	else//a[begin] > a[midi]
	{
		if (a[midi] > a[end])
			return midi;
		else if (a[begin] > a[end])
			return end;
		else
			return begin;
	}
}
//快排函数
void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)
		return;
		//end-begin+1是数据个数,规定其小于10时采用插入排序
   if(end-begin+1<10)
   //注意这里的递归起始区间位置是a+begin
    InsertSort(a+begin,end-begin+1);
    else
	{
    int midi = GetMidi(a, begin, end);
	Swap(&a[midi], &a[begin]);

	int left = begin, right = end;
	int keyi = begin;

	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[left], &a[keyi]);
	keyi = left;

	// [begin, keyi-1] keyi [keyi+1, end]
	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi + 1, end);
    }
}
  • 下图是测试优化小区间后的快排效果,数字代表跑完程序所用的毫秒数,测试方法文末附有,我们对由10万个随机数构成的数组进行排序,为了使得对比更明显,我们在debug(调试版本)下进行对比,debug下,每个函数栈帧添加了许多调试信息,占用的空间更大,小区间优化的效果更加明显。而release(发布版本)对调试信息进行了优化,函数栈帧过多影响不大,多敲的代码可能还会降低排序速度,起到适得其反的效果。

在这里插入图片描述
可以看到效果是有的,但是不是非常明显,想要大幅度改进,还需要从思想上改变的新的方法

为了方便和后续新的方法对比,我们把霍尔版本的单趟排序抽离出来,定义为PartSort1函数

int PartSort1(int* a, int begin, int end)
{
	int midi = GetMidi(a, begin, end);
	Swap(&a[midi], &a[begin]);
	int left = begin, right = end;
	int keyi = begin;

	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[left], &a[keyi]);

	return left;
}
//函数 PartSort1 返回了 left 指针指向的值
//这个值表示基准值在分区后的位置。在快速排序算法中,
//这个值会被用来确定下一次分区的区间范围。
void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)
		return;
		//end-begin+1是数据个数,规定其小于10时采用插入排序
   if(end-begin+1<10)
   //注意这里的递归起始区间位置是a+begin
    InsertSort(a+begin,end-begin+1);
    else
	{
    int keyi=PartSort1(a,begin,end);
	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi + 1, end);
    }
}

补坑位版本

动图展示💦
请添加图片描述

注意,不同方法的快速排序第一趟排序的结果可能不同
在这里插入图片描述

//补坑位版本
int PartSort2(int* a,int begin,int end)
{
   int midi=GetMidi(a, begin, end);
   Swap(&a[midi],&a[begin]);
   int key=a[begin];
   int hole=begin;
   while(begin<end)
   {
     //右边找小,找到后填到左边坑位
     while(begin<end && key>=a[end])
     {
       end--;
     }  
     a[hole]=a[end];
     hole=end;
     //左边找大,找到后填到右边坑位
     while(begin<end&&key<=a[begin])
     {
      begin++;
     }
     a[hole]=a[begin];
     hole=begin;
   }
   a[hole]=key;
   return hole;
}

前后指针版本

请添加图片描述
第一趟交换后的结果
在这里插入图片描述

  • 最开始prev和cur相邻的
  • 当cur遇到比key小的值,++perv,交换prev和cur位置的值
  • 当cur遇到比key的大的值以后,++cur
  • prev和cur之间的值都是比key大的值,prev之前的包括交换后prev上的值都比key小
  • 相当于把大的翻滚式往右边推,同时把小的换到左边
int PartSort3(int*a,int begin,int end)
{
  int midi=GetMidi(a,begin,end);
  Swap(&a[begin],&a[midi]);
  int prev=begin;
  int cur=prev+1;
  int key=a[begin];
 while(cur<=end)
 {
   if(key<=a[cur])
   {
    cur++;
   }
   else
   {
     prev++;
     Swap(&a[prev],&a[cur]); 
     cur++;
   }
 }
 Swap(&a[prev],&key);
 return prev;
}

ok了这个测试后也没啥问题
在这里插入图片描述
在这里插入图片描述

int PartSort3(int* a, int begin, int end)
{
	int midi = GetMidi(a, begin, end);
	Swap(&a[midi], &a[begin]);
	int keyi = begin;

	int prev = begin;
	int cur = prev + 1;
	while (cur <= end)
	{
	//循环内部其实还可以进一步简化代码
		if (a[cur] < a[keyi] && ++prev != cur)
			Swap(&a[prev], &a[cur]);

		++cur;
	}

	Swap(&a[prev], &a[keyi]);
	keyi = prev;
	return keyi;
}

非递归版本

通常我们将递归代码改为非递归有两种方式

  • 写成循环版本
  • 借助数据结构----栈

如果想将快排仅仅利用循环是不现实的,因为递归的子区间左右区间不好控制,这里我们需要借助栈的结构

1️⃣将begin和end压入栈中,利用单趟快排找到keyi并且pop掉这两个元素
2️⃣将由keyi分出的子区间压入栈中,并且让每次将出栈的左右区间的子区间进栈
3️⃣循环多次
4️⃣当分出的子区间左区间大于等于右区间不需要入栈在这里插入图片描述
在这里插入图片描述

void QuickSortNonR(int* a, int begin, int end)
{
	ST s;
	STInit(&s);
	//第一次入栈要将整个数组的左右区间进栈
	STPush(&s, end);
	STPush(&s, begin);

	while(!STEmpty(&s))
	{
	  //记录左右区间后出栈
		int left = STTop(&s);
		STPop(&s);
		int right = STTop(&s);
		STPop(&s);
       //利用存好的左右区间进行一趟快排,找到keyi
		int keyi = PartSort3(a, left, right);
		// [left, keyi-1] keyi [keyi+1, right]
		//当子区间合法时,继续入栈
		if (left < keyi - 1)
		{
			STPush(&s, keyi - 1);
			STPush(&s, left);
		}

		if (keyi + 1 < right)
		{
			STPush(&s, right);
			STPush(&s, keyi+1);
		}
	}

	STDestroy(&s);
}

💦 归并排序

基本思想: 归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。

动图展示
请添加图片描述

在这里插入图片描述

递归版本

void _MergeSort(int* a, int begin, int end, int* tmp)
{
	if (begin >= end)
		return;

	int mid = (begin + end) / 2;
	// [begin, mid][mid+1, end]
	_MergeSort(a, begin, mid, tmp);
	_MergeSort(a, mid+1, end, tmp);

	// [begin, mid][mid+1, end]归并
	int begin1 = begin, end1 = mid;
	int begin2 = mid + 1, end2 = end;
	int i = begin;
	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++];
	}
   //将tmp数组中的值拷贝回去
	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;
	}
   
	_MergeSort(a, 0, n - 1, tmp);
	free(tmp);
}

小结

  • 主函数接受两个参数,一个整数数组a和一个整数n,n 表示数组的长度。

  • MergeSort 函数首先为tmp数组开辟待空间。

  • 调用_MergeSort函数进行排序。

  • 释放tmp的空间。

  • 在子函数中,首先计算中间位置mid,并递归地对数组的两部分进行排序。这是分治的思想,将大问题分解成小问题,使用四个指针begin1和begin2、end1和end2,分别指向两个部分的开始位置和结束位置,

  • 然后看三个while循环的比较插入过程,每次分割后两部分分别从头开始比较,把较小的插入tmp数组,某一部分的数全部插入数组后,结束第一个while循环。继续检查哪个数组还有剩余元素,剩下的都是较大的,直接插入tmp数组中。

  • 接下来,我们需要从最小的子序列到最大依次往上进行排序插入,所以这里引用递归的思想完成排序:

  • 在函数_MergeSort中,首先判断begin是否等于end,如果相等,则当前子序列只有一个元素,不需要排序,直接返回。

  • 如果不相等,则计算中间位置mid,然后递归调用_MergeSort函数对左半部分和右半部分进行排序。在排序完成后,将左半部分和右半部分合并成一个有序数组tmp。

  • 每层递归排序后,使用memcpy函数将临时数组tmp中的元素复制回原数组a中。

非递归版本

  • 归并排序不适合用栈来改造,归并排序类似于树的后序遍历,在返回时还需要合并这样的复杂操作,栈模拟递归是不能实现的,对比之前的快速排序的非递归版本,快速排序是前序遍历,即使返回时不做任何操作也可以达到排序目的。
  • 我们可以先将gap初始化为1,然后每次将gap乘以2,直到gap大于等于数组的长度为止。在每次循环中,我们将数组分成若干个大小为gap的子数组,然后对每个子数组进行排序和合并。这样,我们就可以通过循环来实现归并排序,而不需要使用递归。在这里插入图片描述
void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc fail");
		return;
	}
      //通过gap控制归并的子数组大小实现非递归的归并排序
	int gap = 1;
	while (gap < n)
	{
		printf("gap:%2d->", gap);
		for (size_t i = 0; i < n; i += 2 * gap)
		{
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + 2 * gap - 1;

			// 边界的处理
			if (end1 >= n || begin2 >= n)
			{
				break;
			}

			if (end2 >= n)
			{
				end2 = n - 1;
			}

	      //开始归并
			int j = begin1;
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] < a[begin2])
				{
					tmp[j++] = a[begin1++];
				}
				else
				{
					tmp[j++] = a[begin2++];
				}
			}

			while (begin1 <= end1)
			{
				tmp[j++] = a[begin1++];
			}

			while (begin2 <= end2)
			{
				tmp[j++] = a[begin2++];
			}

			memcpy(a + i, tmp + i, sizeof(int) * (end2-i+1));
		}

		printf("\n");

		gap *= 2;
	}


	free(tmp);
}

在这里插入图片描述
在这里插入图片描述

💦 性能测试

void TestInsertSort()
{
	int a[] = { 3, 2, 6, 8, 4, 6, 0, 9, 5, 7, 1 };
	InsertSort(a, sizeof(a) / sizeof(int));
	PrintArray(a, sizeof(a) / sizeof(int));
}

void TestBubbleSort()
{
	int a[] = { 3, 2, 6, 8, 4, 6, 0, 9, 5, 7, 1 };
	BubbleSort(a, sizeof(a) / sizeof(int));
	PrintArray(a, sizeof(a) / sizeof(int));
}

void TestShellSort()
{
	//int a[] = { 3, 2, 6, 8, 4, 6, 0, 9, 5, 7, 1 };
	int a[] = { 13, 2, 6, 8, 4, 6, 0, 9, 5, 7, 1 };
	ShellSort(a, sizeof(a) / sizeof(int));
	PrintArray(a, sizeof(a) / sizeof(int));
}

void TestSelectSort()
{
	//int a[] = { 3, 2, 6, 8, 4, 6, 0, 9, 5, 7, 1 };
	int a[] = { 13, 2, 6, 8, 4, 6, 0, 9, 5, 7, 1};

	SelectSort(a, sizeof(a) / sizeof(int));
	PrintArray(a, sizeof(a) / sizeof(int));
}

void TestHeapSort()
{
	//int a[] = { 3, 2, 6, 8, 4, 6, 0, 9, 5, 7, 1 };
	int a[] = { 13, 2, 6, 8, 4, 6, 0, 9, 5, 7, 1 };

	HeapSort(a, sizeof(a) / sizeof(int));
	PrintArray(a, sizeof(a) / sizeof(int));
}

void TestQuickSort()
{
	//int a[] = { 3, 2, 6, 8, 4, 6, 0, 9, 5, 7, 1 };
	//int a[] = {6,1,2,7,9,3,4,5,10,8};
	int a[] = { 6,1,2,6,7,9,3,4,6,10,8 };
	PrintArray(a, sizeof(a) / sizeof(int));

	//QuickSort(a, 0, sizeof(a) / sizeof(int)-1);
	QuickSortNonR(a, 0, sizeof(a) / sizeof(int) - 1);

	PrintArray(a, sizeof(a) / sizeof(int));
}

void TestMergeSort()
{
	//int a[] = { 3, 2, 6, 8, 4, 6, 0, 9, 5, 7, 1 };
	//int a[] = {6,1,2,7,9,3,4,5,10,8};
	//int a[] = { 6,1,2,6,7,9,3,4,6,10,8 };
	int a[] = { 10,8,7,1,3,9,4,2,9,10,1,1,2,3};
	PrintArray(a, sizeof(a) / sizeof(int));

	MergeSortNonR(a, sizeof(a) / sizeof(int));

	PrintArray(a, sizeof(a) / sizeof(int));
}

void TestCountSort()
{
	int a[] = {1,3,9,1,5,1,2,3,-5,-5,-2 };
	PrintArray(a, sizeof(a) / sizeof(int));

	CountSort(a, sizeof(a) / sizeof(int));

	PrintArray(a, sizeof(a) / sizeof(int));
}

// 测试排序的性能对比
void TestOP()
{
	srand(time(0));
	const int N = 10000000;
	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);
	int* a7 = (int*)malloc(sizeof(int) * N);
	int* a8 = (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];
		a7[i] = a1[i];
		a8[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();

	int begin7 = clock();
	//BubbleSort(a7, N);
	int end7 = clock();

	int begin8 = clock();
	CountSort(a8, N);
	int end8 = 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);
	printf("BubbleSort:%d\n", end7 - begin7);
	printf("CountSort:%d\n", end8 - begin8);

	free(a1);
	free(a2);
	free(a3);
	free(a4);
	free(a5);
	free(a6);
	free(a7);
	free(a8);
}

int main()
{
	//TestInsertSort();
	//TestBubbleSort();
	//TestShellSort();
	//TestSelectSort();
	//TestHeapSort();
	//TestQuickSort();
	//TestMergeSort();
	//TestCountSort();

	TestOP();

	return 0;
}

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

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

相关文章

Ps:根据 HSB 调色(以可选颜色命令为例)

在数字色彩中&#xff0c;RGB 和 HSV&#xff08;又称 HSB&#xff09;是两种常用的颜色表示方式&#xff08;颜色模型&#xff09;。 在 RGB 颜色模式下&#xff0c;Photoshop 的红&#xff08;Red&#xff09;、绿&#xff08;Green&#xff09;、蓝&#xff08;Blue&#xf…

韦东山嵌入式Liunx入门笔记一

文章目录 一、嵌入式Linux二、Ubuntu系统2-1 安装软件2-2 Linux文件(1) 文件架构(2)文件属性(3)文件命令(4) 解压、压缩文件(5) 网络命令 2-3 vi编辑器2-4 Ubuntu下包管理 三、配置网卡四、安装后续学习使用的软件4-1 MobaXterm4-2 FileZilla4-3 Source Insight4.04-4 下载BSP4…

sqli-labs-master 下载、搭建

sqli-labs-master sqli-labs-master 是一个帮助用户学习和测试 SQL 注入漏洞的开源项目。它提供了一系列的环境&#xff0c;用户可以在这些环境中进行实验&#xff0c;学习如何检测、利用和防御 SQL 注入攻击。 sqli-labs 下载地址&#xff1a; github.com/Audi-1/sqli-labs 搭…

刘知远团队大模型技术与交叉应用L5-BMSystem

为什么需要BMTrain&#xff1f; PLM越来越大。为了使训练更高效和廉价。我们有必要 1.分析GPU的显存去哪了&#xff1f; 2.理解GPU间的合作模式是如何的&#xff1f; 显存都去了哪里&#xff1f; CPU vs GPU CPU适合复杂逻辑运算。GPU适合大量重复的数值运算。 显存成分 1.前…

【Java】SpringMVC参数接收(二):JSON、URI、文件

1、获取JSON参数 RequestMapping("/hello") RestController public class HelloSpring {RequestMapping("/t10")public String t10(RequestBody UserInfo userInfo){return userInfo.toString();} } 2、获取URI中的参数 &#xff08;1&#xff09;获取单…

面对.pings勒索病毒威胁:深度解析如何对抗.pings勒索病毒的危害

导言&#xff1a; 随着科技的发展&#xff0c;互联网的普及&#xff0c;网络犯罪也在不断演变。其中一种恶意软件&#xff0c;.pings勒索病毒&#xff0c;威胁着个人和企业的数据安全。本文91数据恢复将介绍.pings勒索病毒的特点、如何恢复被加密的数据文件&#xff0c;以及有…

Codeforces Round 921 (Div. 2)

Codeforces Round 921 (Div. 2) Codeforces Round 921 (Div. 2) A. We Got Everything Covered! 题意&#xff1a;找到一个字符串s&#xff0c;使得所有可能长度为n的字符串都可以用前k个小写字母组成&#xff0c;并为s的子序列。 思路&#xff1a;A的题意理解对C很有用 首…

Leetcode 206 反转链表

反转链表 准备工作1&#xff09;ListNode基本结构2&#xff09;初始化ListNode集合 解法一&#xff1a;遍历创建新节点解法二&#xff1a;两组List&#xff0c;面向对象操作解法三&#xff1a;递归调用解法四&#xff1a;直接移动解法五&#xff1a;解法二的面向过程 Leetcode …

Java异常处理集合

Java异常处理 Java语言在执行后会中断&#xff0c;也就是在出错位置后的代码都不会被执行&#xff0c;为了使非致命错误后的程序仍然能够执行&#xff0c;引入异常处理机制。 异常 可处理的异常用Exception表示&#xff0c;不可处理的异常用Error表示&#xff0c;通常是栈内…

ShardingSphere之ShardingJDBC客户端分库分表下

目录 ShardingJDBC实战 STANDARD标准分片策略 COMPLEX_INLINE复杂分片策略 CLASS_BASED自定义分片策略 HINT_INLINE强制分片策略 ShardingJDBC实战 上篇已经将需要用到的类、数据库表都准备好了&#xff0c;本篇主要介绍分片配置文件。 STANDARD标准分片策略 如果按照上篇文…

Python笔记14-实战小游戏飞机大战(上)

文章目录 功能规划安装pygame绘制游戏窗口添加玩家飞机图像屏幕上绘制飞船代码重构驾驶飞船全屏模式射击 功能规划 玩家控制一艘最初出现在屏幕底部中央的飞船。玩家可以使用箭头键左右移动飞船&#xff0c;还可使用空格键射击。游戏开始时&#xff0c;一群外星人出现在天空中…

如何从 Android SD 卡恢复已删除的照片

您是否不小心从 Android SD 卡中删除了一些照片&#xff1f;您是否尝试访问昨天拍摄的照片&#xff0c;但无论您在哪里查看都找不到它们&#xff1f;您的 Android 手机的外部存储是否已损坏&#xff0c;其内容无法访问&#xff1f; 在这种情况下&#xff0c;您应该尽快采取行动…

LV老板重夺全球首富 再次超过马斯克;新东方安徽总部大厦启用;中国与泰国签署互免签证协定

今日精选 • LV老板重夺全球首富 再次超过马斯克• 新东方安徽总部大厦启用• 中国与泰国签署互免签证协定 投融资与企业动态 • ​​传Temu将于3月在美国上线半托管业务• 国内数字支付解决方案提供商 “连连数字” 估值150亿&#xff0c;即将IPO• 滴滴与宁德时代宣布成立…

【Javaweb程序设计】【C00164】基于SSM的飞机订票系统(论文+PPT)

基于SSM的飞机订票系统&#xff08;论文PPT&#xff09; 项目简介项目获取开发环境项目技术运行截图 项目包运行、免费远程调试 项目简介 这是一个基于ssm的飞机订票系统 本系统分为前台用户模块和后台管理员模块。 前台用户模块&#xff1a;当游客打开系统的网址后&#xff0…

营销领域有哪些著名的模型?如销售漏斗等

一、金字塔原理 模型 适用场景&#xff1a;提案 沟通 思考理论 模型 来源&#xff1a;麦肯锡 芭芭明托 1、表达的逻辑 遵循金字塔模型的逻辑&#xff0c;先说结论&#xff0c;后说论据。所有表达的内容都可归纳出一个核心论点。这个核心论点由N个论据作为支持&#xff0c;而…

Qt扩展-QXlsx读写Excel配置使用

QXlsx读写Excel配置使用 一、概述1. 功能概述2. 其他维护 二、安装1. 下载源码2. 配置项目3. 测试代码4. 运行结果 一、概述 项目介绍&#xff1a;https://qtexcel.github.io/QXlsx/Example.html GitHub&#xff1a;https://github.com/QtExcel/QXlsx/tree/master QXlsx 是一个…

时序分析中的去趋势化方法

时序分析中的去趋势化方法 时序分析是研究随时间变化的数据模式的一门学科。在时序数据中&#xff0c;趋势是一种随着时间推移而呈现的长期变化趋势&#xff0c;去趋势化是为了消除或减弱这种趋势&#xff0c;使数据更具平稳性。本文将简单介绍时序分析中常用的去趋势化方法&a…

java自定义异常处理操作

目录 引入 介绍 一、为我们的方法创建异常类 二、使用我们定义的异常类 引入 我们在编写java代码的时候,想要在我们自己写的方法中加入我们的异常处理 介绍 一、为我们的方法创建异常类 我们先单独创建一个类,来当作我们抛出异常的类 public class AgeIllegalException ex…

JavaScript DOM属性和方法之element元素对象

在HTML DOM中&#xff0c;elment对象表示HTML与纳素&#xff0c;可以包含的节点类型有元素u节点、文本节点、注释节点。它们有响应的属性和方法&#xff0c;有很多都是我们之前用过的。 一、element对象属性 1、attributes 2、childNodes 3、className 4、clientWidth、of…

计算机中丢失mfc100u.dll怎么解决,详细解析mfc100u.dll丢失的解决方法

遭遇“无法找到mfc100u.dll”的错误不必过分担忧&#xff0c;这是一个普遍现象。许多用户在启动某些软件或游戏的时候可能会碰到这样的情况。通常&#xff0c;这个错误信息表明你的计算机系统中缺失了一个关键的动态链接库(DLL)文件&#xff0c;它可能会妨碍应用程序的顺利启动…