初阶数据结构—排序

第一章:排序的概念及其运用

1.1 排序的概念

排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。

稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。

内部排序:数据元素全部放在内存中的排序。

外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。

1.2 排序运用

1.3 常见的排序算法

第二章:常见排序算法的实现

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]插入,原来位置上的元素顺序后移 

//思路:
//将第一个元素视为已排序部分,从第二个元素开始,将该元素依次与已排序部分的元素依次进行比较。
void InsertSort(int* a, int n) { //直接插入排序//i为数组下标,从数组的第一个元素开始(下标为0),依次和后面的元素比较。遍历至倒数第二个元素for (int i = 0; i < n - 1; i++) {int end = i; //已排序部分最后元素int tmp = a[i + 1]; //待排序的元素。暂存tmp中,防止后面排序移动数据时被覆盖while (end >= 0) { //在已排序部分最后元素从后向前查找合适的插入位置if (a[end] > tmp) { //如果已排序部分的元素大于当前要插入的元素a[end + 1] = a[end]; //将已排序元素向后移动一位,为要插入的元素腾出插入位置end--; //继续直向前遍历,}else //直到已排序部分的某个元素小于插入元素时停下break;}//这里说明end指向的已排序元素比插入元素小a[end + 1] = tmp;// 将新元素插入到该数组元素后面}
}

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

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

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

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

版本一: 一组一组排序

预排序,未完全完成排序

//思路:
//从下标0开始,每隔gap(3)选数据(这里是对数据分组),然后用插入排序法排序这些数据。
//再从下标1开始,重复上面步骤,直到下标等于gap-1
//这里是一组一组排序
void ShellSort(int* a, int n) { //希尔排序int gap = 3;for (int j = 0; j < gap; j++) { //循环gap组for (int i = j; i < n - gap; i += gap) { //每组数据插入排序int end = i; //已排序部分最后元素int tmp = a[end + gap]; //要插入的数据。该组数据最后元素end+gap要小于n,end=i,所以i<n-gapwhile (end >= 0) {if (a[end] > tmp) {a[end + gap] = a[end];end -= gap;}elsebreak;}a[end + gap] = tmp;}}
}

版本二:多组并排

减少一层循环,该版本是每组完成一次排序再进行第二次。预排序,未完全完成排序

void ShellSort(int* a, int n) { //希尔排序int gap = 3;//gap=3,被分成了3组数据。//这种写法相当于每组数据分别排序一次//即A组第一次,B组第一次,C组第一次。依次类推排完所有数据for (int i = 0; i < n - gap; i++) {int end = i; //记录当前要插入元素的前一个位置索引int tmp = a[end + gap]; //要插入的数据。该组数据最后元素end+gap要小于n,end=i,所以i<n-gapwhile (end >= 0) {if (a[end] > tmp) {a[end + gap] = a[end];end -= gap;}elsebreak;}a[end + gap] = tmp;}
}

结论:

gap越大,大的数可以更快到后面,小的数可以更快到前面
gap越小,大的小的挪动越慢,但是越接近有序
gap==1,就是直接插入排序

版本三:优化gap

完全完成排序

//继续优化gap版本  -- 时间复杂度O(N^1.3)
void ShellSort(int* a, int n) { //希尔排序int gap = n;//gap要根据数据个数来确定while (gap > 1) { //gap不用等于1,下方商为0时,再+1就能保证最后一次为直接插入排序。且等于1会死循环//这里每排序一次,gap都会更小。//当gap小于3时,商为0。+1既保证了程序正确,也能最后一次排序变为直接插入排序,保证数据有序gap = gap / 3 + 1;for (int i = 0; i < n - gap; i++) {int end = i; //记录当前要插入元素的前一个位置索引int tmp = a[end + gap]; //要插入的数据。该组数据最后元素end+gap要小于n,end=i,所以i<n-gapwhile (end >= 0) {if (a[end] > tmp) {a[end + gap] = a[end];end -= gap;}elsebreak;}a[end + gap] = tmp;}}
}

希尔排序的特性总结:

1. 希尔排序是对直接插入排序的优化。

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

3. 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在好些树中给出的希尔排序的时间复杂度都不固定:

4. 稳定性:不稳定

2.2 选择排序

2.2.1 基本思想:

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

2.2.2 直接选择排序:

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

void SelectSort(int* a, int n) { //选择排序//思路://1.初始化起始、结束位置下标,该区间内为未排序部分。(初始时,整个数组都为未排序部分)	//2.初始化最小、最大元素下标。每次排序前遍历未排序区间的元素,更新最大、最小下标。//3.将最小元素交换到起始位置;将最大元素交换到结束位置//但要注意如果最大(maxi)下标和起始(begin)下标重合,那么交换最小元素(a[mini])和起始元素(a[begin])时,//那么最大元素(a[mxi])就被换到最小下标(mini)处//所以要将最大下标移动至最小下标处后,再交换最大元素和结束位置元素//4.每次排序后,更新起始、结束下标,begin++,end--(即重新指向未排序区间)。然后重复2、3步骤//当begin>=end时,说明没有待排序区间,排序结束int begin = 0, end = n - 1; //起始和结束位置的下标	while (begin < end) {int maxi = begin, mini = begin; //初始化最大值和最小值的下标for (int i = begin; i <= end; i++) { //遍历数组未排序部分		if (a[i] > a[maxi])maxi = i;if (a[i] < a[mini])mini = i;}Swap(&a[begin], &a[mini]);//如果maxi和begin重叠,修正下即可//上述情况因为maxi和begin相等,交换mini和begin也是交换mini和maxi,//即maxi指向最小值,mini指向最大值。但mini的值已经正确移动到begin位置。//所以要调整maxi指向,即将mini指向给maxiif (maxi == begin)maxi = mini;Swap(&a[end], &a[maxi]);begin++;end--;}
}

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

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

2.2.3 堆排序

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

void AdjustDown(HPDataType* a, int n, int parent)//堆的向下调整
{//父亲下标找孩子//leftchild = parent*2 + 1//rightchild = parent*2 + 2//如果是小堆,就找2个子节点中较小那个,因为父节点比小的子节点还小,说明父节点比2个子节点都小。//如果是大堆,就找2个子节点中较大那个,因为父节点比大的子节点还大,说明父节点比2个子节点都大。//1.先初始化当前父节点的左子节点//2.以大堆为例,在比较父子节点之前选出左右子节点较大那个,//3.如果较大子节点比父节点大,那么交换并更新父节点,再找到其子节点//4.如果较大子节点比父节点小,说明已经是大堆,直接跳出循环。//重复2、3、4步骤直至子节点不存在int child = parent * 2 + 1;//假设左子结点小while (child < n) //子节点下标要在数组内才继续{//child为左子结点,child+1为右子节点//选出左右子节点小的那个(前提是右子节点存在)。如果右子节点小,孩子下标++if (child + 1 < n && a[child + 1] > a[child]) //大堆//if (child + 1 < n && a[child + 1] < a[child]) //小堆child++;if (a[child] > a[parent]) { //大堆,如果父节点比子节点小,调整。每个父节点>=子节点//if (a[child] < a[parent]) { //小堆,如果父节点比子节点大,调整。每个父节点<=子节点Swap(&a[child], &a[parent]);//交换父子节点parent = child;//父节点下标指向子节点,以该节点作为新的父节点。child = parent * 2 + 1;//新的子节点}elsebreak;}
}void HeapSort(int* a, int n)
{//升序 建大堆//降序 建小堆//建堆 - 向下调整。(因为向下调整需要左右子树是堆,所以倒着向下调整)//时间复杂度:F(N)=N-log(N+1)    O(N)//从倒数第一个非叶子节点(最后节点的父节点)开始调整//叶节点(即终端节点,没有子节点的节点)不需要调整//孩子找父亲 parent = (child-1)/2。n是元素个数,n-1才是最后节点下标for (int i = (n - 1 - 1) / 2; i >= 0; i--)AdjustDown(a, n, i);//堆排序    时间复杂度:N*logN//1.初始化尾元素下标//2.交换首尾元素,即堆顶元素放到数组最后。//3.向下调整排序,不包含当前堆中的尾元素//4.更新尾元素下标,end--//5.end=0结束,一个元素视为有序(重复2~4步)。int end = n - 1;while (end > 0) //end不用等于0。只剩一个待排序数据时已经有序,不需要在调整。{Swap(&a[0], &a[end]);AdjustDown(a, end, 0);//end是尾元素下标,其数值是n-1,恰好等于去掉最后元素的个数end--;}
}

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

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

2.3 交换排序

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

2.3.1冒泡排序

void BubbleSort(int* a, int n) {for (int j = 0; j < n - 1; j++) {bool exchange = false;for (int i = 1; i < n - j; i++) { //从第二个数(下标为1)开始排序。第一个数默认有序if (a[i - 1] > a[i]) {int tmp = a[i - 1];a[i - 1] = a[i];a[i] = tmp;exchange = true;}}if (exchange == false)break;}
}

冒泡排序的特性总结: 

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

2.3.2 快速排序

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

1. hoare版本

版本一 :此版本判断条件不完善
void PartSort(int* a, int left, int right) {int keyi = left;while (left < right) {//右边找小while (a[right] > a[keyi])right--;//左边找大while (a[left] < a[keyi])left++;Swap(&a[left], &a[right]);}Swap(&a[left], &a[keyi]);
}

上面版本有2个问题:

  1. 当left和right位置的值等于keyi位置的值陷入死循环,即left不比keyi大,left不++;right不比keyi小,right不--。两下标不移动,无限交换两处的值。但当left和right跟keyi比较时,如果仅将条件改为>=或<=,这会引发第二个问题。
  2. 如果数组初始状态第一个元素最小,此时left和keyi同时指向它,那么right一直--,即使left==right,right还会--造成越界。

综上所述,left和right跟keyi比较时,不仅要将条件改为>=或<=避免死循环,还要在加上left<right的条件避免越界

版本二:
//时间复杂度:O(N*logN)
//空间复杂度:O(logN) 类似二叉树高度
int PartSort(int* a, int left, int right) {//left必须从keyi位置开始。如果left在keyi后面且keyi是最小值,那么left不会移动,right会在left位置相遇。//然后错误的交换left和keyiint keyi = left;while (left < right) {//1.左边作key,右边先走;保障了相遇位置的值比key小或key的位置//2.右边作key,坐边先走;保障了相遇位置的值比key大//left和right相遇分2种情况:left遇right;right遇left。//1.left遇right,right先停下,left在走。//因为right先走,所以right停下的位置一定比key小。(right找比key小的,比key大就移动)//此时相遇位置就是right停下位置,一定比key小//2.right遇left,在相遇这轮,left没动,right在移动跟left相遇,相遇位置就是left位置//情况一:left位置就是key位置(left始终没动过,始终指向key)//情况二:交换过一些轮次,相遇时left位置一定比key小。//因为上一轮left和right交换后,left位置比key小;且是right遇left,所以left位置比key小//右边找小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]);//此时left和right相遇的位置就是基准值位置,将left或right的其中一个位置交换给基准值变量//返回keyi的位置,即left和right相遇的位置。//此位置的值作为基准值,继续排序它的左右两边return left;
}void QuickSort(int* a, int begin, int end) { //快速排序if (begin >= end) //当待排序区间只有一个元素Or待排区间不存在说明全部排序完毕,递归结束return;//1.hoare版本int keyi = PartSort(a, begin, end);//获取排序后的keyi//[begin, keyi-1] keyi [keyi+1, end]//分别递归keyi左右区间的数据,注意keyi已经排序到正确位置。QuickSort(a, begin, keyi - 1);QuickSort(a, keyi + 1, end);
}

2. 挖坑法

//2.挖坑法 [left, right]
//创建基准值变量及坑变量并初始化。
//右边找小,左边找大。
//当right找到比key小的值,将该值赋值给坑位置。同时将坑位置更新为right位置。
//当left找到比key大的值,将该值赋值给坑位置。同时将坑位置更新为left位置。
//遍历完数组后,将key的值赋值给坑位置。
int PartSort2(int* a, int left, int right) {int key = a[left];//初始化基准值为left的值int hole = left;//初始化坑下标为leftwhile (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;return hole;
}

3. 前后指针法

//3.前后指针法 [left, right]
//prev初始指向开头,cur指向prev后面。
//若cur指向小于key,则prev++,并将cur与prev交换(若两者相等不交换),然后cur++
//若cur指向大于key,cur继续++
//当cur遍历完,将prev指向的值和key交换
//思路:
//prev指向已排好序部分最后元素。一开始,整个数组可以视为只有第一个元素是已排序的,
//因此 prev 初始化为 left,即数组的起始位置。
//cur负责遍历数组,当他找到比keyi小的值,prev就向后移动一个,然后交换。
int PartSort3(int* a, int left, int right) {int keyi = left;//不会被覆盖(即交换)就用keyi(即下标)//用于指向当前已经处理过的小于分区点值的最后一个元素的位置。//初始时,prev 被设置为 left,即数组的起始位置。这表示在开始时,还没有找到任何小于分区点的元素。int prev = left;//用于遍历数组,查找当前位置的元素是否小于分区点元素。int cur = left + 1;while (cur <= right) {if (a[cur] < a[keyi] && ++prev != cur) //这里无论第二个条件是否为真,++prev都会执行Swap(&a[prev], &a[cur]);cur++;}Swap(&a[prev], &a[keyi]);keyi = prev;return keyi;
}

2.3.3 快速排序优化

1. 三数取中法选key

当key是最大或最小时,排序的时间复杂度接近O(n^2),例如,如果数组已经有序或逆序。使用三数取中法可以大大降低这种最坏情况的概率。

int GetMidIndex(int* a, int left, int right) {int mid = (left + right) / 2;//int mid = left + (rand() % (right - left));//随机数三数取中,避免左右都是最小值if (a[left] < a[mid]) {if (a[mid] < a[right]) //a[left] < a[mid] 且 a[mid] < a[right]return mid;//a[left] < a[mid] 且 a[right] <= a[mid](因为上方a[mid] < a[right]不成立),又因为a[left] < a[right]else if (a[left] < a[right])return right;elsereturn left;}else { //a[left] > a[mid]if (a[mid] > a[right])return mid;else if (a[left] > a[right]) //a[left] > a[mid] 且 a[left] > a[right];a[mid] < a[right]return right;elsereturn left;}
}

a. hoare版本(带三数取中)
int PartSort(int* a, int left, int right) {//三数取中法选keyint midi = GetMidIndex(a, left, right);Swap(&a[left], &a[midi]);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[left], &a[keyi]);return left;
}

b. 挖坑法(带三数取中)
int PartSort2(int* a, int left, int right) {//三数取中法选keyint midi = GetMidIndex(a, left, right);Swap(&a[left], &a[midi]);int key = a[left];//初始化基准值为left的值int hole = left;//初始化坑下标为leftwhile (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;return hole;
}

c. 前后指针法(带三数取中)
int PartSort3(int* a, int left, int right) {//三数取中法选keyint midi = GetMidIndex(a, left, right);Swap(&a[left], &a[midi]);int keyi = left;//不会被覆盖(即交换)就用keyi(即下标)int prev = left;int cur = left + 1;while (cur <= right) {if (a[cur] < a[keyi] && ++prev != cur)Swap(&a[prev], &a[cur]);cur++;}Swap(&a[prev], &a[keyi]);keyi = prev;return keyi;
}

d. 三路划分版本(针对OJ题优化)
int GetMidIndex(int* a, int left, int right) {int mid = left + (rand() % (right - left));//随机数三数取中,避免左右都是最小值if (a[left] < a[mid]) {if (a[mid] < a[right]) //a[left] < a[mid] 且 a[mid] < a[right]return mid;//a[left] < a[mid] 且 a[right] <= a[mid](因为上方a[mid] < a[right]不成立),又因为a[left] < a[right]else if (a[left] < a[right])return right;elsereturn left;}else { //a[left] > a[mid]if (a[mid] > a[right])return mid;else if (a[left] > a[right]) //a[left] > a[mid] 且 a[left] > a[right];a[mid] < a[right]return right;elsereturn left;}
}//1.a[cur]<key,交换cur和left位置的值,++left,++cur
//2.a[cur]>key,交换cur和right位置的值,--right
//3.a[cur]==key,++cur//本质:
//1.小的换到左边,大的换到右边
//2.把key相等的值推到中间
void QuickSort(int* a, int begin, int end) { // 快速排序if (begin >= end) // 当待排序区间只有一个元素Or待排区间不存在说明全部排序完毕,递归结束   return;int left = begin;int right = end;int cur = left + 1;int midi = GetMidIndex(a, left, right);Swap(&a[left], &a[midi]);int key = a[left];while (cur <= right) {if (a[cur] < key) {Swap(&a[cur], &a[left]);++left;++cur;}else if (a[cur] > key) {Swap(&a[cur], &a[right]);--right;}else++cur;}//    小于key        等于key       大于key//[begin, left-1] [left,right] [right+1, end]QuickSort(a, begin, left - 1);QuickSort(a, right + 1, end);
}

2.3.4 快速排序非递归

1. 栈的使用:
栈的主要作用并不是进行排序操作,而是用来管理待排序的子数组范围。
栈被用来存储待排序子数组的起始和结束索引。每次从栈中取出一对索引,表示当前需要处理的子数组范围。

2. 排序过程:
实际的排序操作发生在对子数组进行分区(Partition)的过程中,即调用 PartSort 函数。
这个函数会根据选定的基准元素将子数组分成左右两部分,并返回基准元素的索引。

3. 分区后的处理:
分区完成后,根据基准元素的索引,将未排序的左右子数组范围压入栈中,以便后续继续处理和排序。

4. 栈的管理:
栈的作用类似于递归调用中的函数调用栈,但这里使用循环和栈结构来实现非递归的快速排序。
通过栈,可以有效地管理排序过程中子数组的分割和顺序,确保所有子数组都被正确地排序。

因此,栈的主要目的是帮助确定排序的范围,而不是在栈内直接进行排序操作。
这种非递归的实现方式避免了递归调用可能导致的栈溢出问题,同时保持了快速排序算法的效率和性能优势。
 

void QuickSortNonR(int* a, int begin, int end) {ST st;STInit(&st);//将初始的排序范围[begin, end] 推入栈中。//先进后出,end先进begin后进,所以begin先出end后出STPush(&st, end);STPush(&st, begin);while (!STEmpty(&st)) {//从栈中弹出顶部的两个元素,定义当前的子数组范围[left, right]。int left = STTop(&st);//begin后进先出STPop(&st);int right = STTop(&st);//end先进后出STPop(&st);//对子数组 a[left...right] 进行分区操作,将数组围绕一个基准元素(keyi)进行分隔。int keyi = PartSort(a, left, right);//[begin, keyi-1] keyi [keyi+1, end]//将子数组推入栈中:if (keyi + 1 < right) { //如果keyi+1 < right,表示右侧子数组[keyi+1,right]还有需要排序的元素,将其索引推入栈中。STPush(&st, right);STPush(&st, keyi + 1);		}if (left < keyi - 1) { //如果left < keyi-1,表示左侧子数组[left, keyi-1]还有需要排序的元素,将其索引推入栈中。STPush(&st, keyi - 1);STPush(&st, left);}}STDestroy(&st);
}

快速排序的特性总结:

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

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

2.4 归并排序

基本思想:

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

递归版本

版本一:
//思路:
//通过递归不断将数组平分为两部分,直到每个部分只有一个元素,此时可以认为这个部分是有序的。
//如果部分包含超过一个元素,则会对这两部分进行合并排序操作。
//时间复杂:O(N*logN) logN层,每层N个数
//空间复杂度:O(N) 需要拷贝一个同样大小数组。开辟的栈帧是logN,即空间复杂度函数式:N+logN
void _MergeSort(int* a, int begin, int end, int* tmp) {if (begin == end) //子数组中只有一个元素,直接返回,因为单个元素视为有序。return;int mid = (begin + end) / 2;_MergeSort(a, begin, mid, tmp);_MergeSort(a, mid + 1, end, tmp);int begin1 = begin, end1 = mid;//表示第一部分数组的范围int begin2 = mid + 1, end2 = end;//表示第二部分数组的范围。int i = begin;//i 表示当前存放位置,开始时 i 初始化为 begin。//在归并排序中,确实需要保证在合并阶段时,区间内至少有两个元素才能正常进行合并操作。while (begin1 <= end1 && begin2 <= end2) { //循环直到其中一部分数组被合并完,begin1-end1为左闭右闭区间if (a[begin1] < a[begin2])tmp[i++] = a[begin1++];elsetmp[i++] = a[begin2++];}//循环结束后,可能存在某一部分数组还有剩余元素未处理,分别使用两个 while 循环将剩余元素放入 tmp 数组中。while (begin1 <= end1)tmp[i++] = a[begin1++];while (begin2 <= end2)tmp[i++] = a[begin2++];memcpy(a + begin, tmp + begin, sizeof(int) * (end - begin + 1));
}

版本二:

小区间优化 - 当数据个数较小时(例如10个)不在分组,直接调用其他排序

void _MergeSort(int* a, int begin, int end, int* tmp) {if (begin == end) //子数组中只有一个元素,直接返回,因为单个元素视为有序。return;if (end - begin + 1 < 10) {InsertSort(a + begin, end - begin + 1);//这里是对[begin,end]区间排序,不是a数组排序,所以是a+beginreturn;}int mid = (begin + end) / 2;_MergeSort(a, begin, mid, tmp);_MergeSort(a, mid + 1, end, tmp);int begin1 = begin, end1 = mid;//表示第一部分数组的范围int begin2 = mid + 1, end2 = end;//表示第二部分数组的范围。int i = begin;//i 表示当前存放位置,开始时 i 初始化为 begin。//在归并排序中,确实需要保证在合并阶段时,区间内至少有两个元素才能正常进行合并操作。while (begin1 <= end1 && begin2 <= end2) { //循环直到其中一部分数组被合并完,begin1-end1为左闭右闭区间if (a[begin1] <= a[begin2])tmp[i++] = a[begin1++];elsetmp[i++] = a[begin2++];}//循环结束后,可能存在某一部分数组还有剩余元素未处理,分别使用两个 while 循环将剩余元素放入 tmp 数组中。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);_MergeSort(a, 0, n - 1, tmp);free(tmp);
}

非递归版本

版本一:错误版本
void MergeSortNonR(int* a, int n) { //错误版本int* tmp = (int*)malloc(sizeof(int) * n);int gap = 1;//每组的数据个数。1 2 4 8....while (gap < n) { //不能等于,等于说明所有数据是一组,一组说明都排序完了。int j = 0;for (int i = 0; i < n; i += 2 * gap) { //遍历整个数组,每次根据gap个数排序2组int begin1 = i, end1 = i + gap - 1;//表示第一部分数组的范围。(因为是左闭右闭区间,gap是数据个数,end1是下标int begin2 = i + gap, end2 = i + 2 * gap - 1;//表示第二部分数组的范围。//在归并排序中,确实需要保证在合并阶段时,区间内至少有两个元素才能正常进行合并操作。while (begin1 <= end1 && begin2 <= end2) { //begin1-end1为左闭右闭区间if (a[begin1] < a[begin2])tmp[j++] = a[begin1++];elsetmp[j++] = a[begin2++];}//循环结束后,可能存在某一部分数组还有剩余元素未处理,分别使用两个 while 循环将剩余元素放入 tmp 数组中。while (begin1 <= end1)tmp[j++] = a[begin1++];while (begin2 <= end2)tmp[j++] = a[begin2++];}memcpy(a, tmp, sizeof(int) * n);gap *= 2;}free(tmp);
}

上方版本没有考虑越界的情况,因为数据是两两归并,只能处理个数为2的次方的数据
1.end1,begin2,end2全部越界
2.begin2,end2越界
3.end2越界

版本二:整体拷贝版本
void MergeSortNonR(int* a, int n) {int* tmp = (int*)malloc(sizeof(int) * n);int gap = 1;//每组的数据个数。1 2 4 8....while (gap < n) { //不能等于,等于说明所有数据是一组,一组说明都排序完了。int j = 0;for (int i = 0; i < n; i += 2 * gap) { //遍历整个数组,每次根据gap个数排序2组int begin1 = i, end1 = i + gap - 1;//表示第一部分数组的范围。(因为是左闭右闭区间,gap是数据个数,end1是下标,所以要-1)int begin2 = i + gap, end2 = i + 2 * gap - 1;//表示第二部分数组的范围。//下方前两种情况直接将第二组数据下标改为不存在的区间//这样下方while循环的归并就不会进去。只会将第一组数据直接拷贝if (end1 >= n) { //1.如果第一组数据不满足个数end1 = n - 1;begin2 = n;end2 = n - 1;}else if (begin2 >= n) { //2.如果没有第二组数据begin2 = n;end2 = n - 1;}else if (end2 >= n) //3.如果第三组数据不满足个数end2 = n - 1;while (begin1 <= end1 && begin2 <= end2) { //begin1-end1为左闭右闭区间if (a[begin1] <= a[begin2])tmp[j++] = a[begin1++];elsetmp[j++] = a[begin2++];}while (begin1 <= end1)tmp[j++] = a[begin1++];while (begin2 <= end2)tmp[j++] = a[begin2++];}memcpy(a, tmp, sizeof(int) * n);gap *= 2;}free(tmp);
}

版本三:归并一组,拷贝一组版本(更推荐此版本)
void MergeSortNonR(int* a, int n) {int* tmp = (int*)malloc(sizeof(int) * n);int gap = 1;//每组的数据个数。1 2 4 8....while (gap < n) { //不能等于,等于说明所有数据是一组,一组说明都排序完了。int j = 0;for (int i = 0; i < n; i += 2 * gap) { //遍历整个数组,每次根据gap个数排序2组int begin1 = i, end1 = i + gap - 1;//表示第一部分数组的范围。(因为是左闭右闭区间,gap是数据个数,end1是下标,所以要-1)int begin2 = i + gap, end2 = i + 2 * gap - 1;//表示第二部分数组的范围。//修正越界//这里的 break; 是为了避免对超出边界的无效子数组进行排序操作,//这并不意味着未排序的元素不进行排序,而是在当前的迭代中,这两个子数组的剩余元素无法继续进行合并操作,//因为归并排序要求两个数组必须是相邻且连续的。//在下一次迭代中,这些剩余的元素会被合并到更大的子数组中,直到最终完成整个数组的排序。//这么做的主要理由是为了确保归并排序的每一次合并操作都是有效的,具体原因包括://1.合并操作要求连续性:在归并排序中,要求进行合并的两个子数组必须是连续的、相邻的。//如果 end1 超出了数组边界,说明当前的第一组数据已经超过了数组范围,不再与其后面的数据进行合并是合理的。//2.避免无效操作:如果没有足够的第二组数据(即 begin2 >= n),则无法与第一组数据进行有效的合并排序。//终止当前迭代可以避免对没有足够数据的情况进行无效的合并尝试。//3.优化执行效率:通过这种方式,可以确保每次合并操作都是基于有效数据范围内的,//避免了不必要的操作和可能的数组越界错误		if (end1 >= n || begin2 >= n)break;if (end2 >= n)end2 = n - 1;while (begin1 <= end1 && begin2 <= end2) { //begin1-end1为左闭右闭区间if (a[begin1] <= a[begin2])tmp[j++] = a[begin1++];elsetmp[j++] = a[begin2++];}while (begin1 <= end1)tmp[j++] = a[begin1++];while (begin2 <= end2)tmp[j++] = a[begin2++];//归并一组,拷贝一组//如果上方有数组越界,那么就没有归并到tmp中,这时tmp中有部分随机值。//再将tmp数据拷贝回原数组时就会覆盖有效数据memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));}gap *= 2;}free(tmp);
}

归并排序的特性总结:

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

2.5 非比较排序

思想:计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。 操作步骤:

  1. 统计相同元素出现次数
  2. 根据统计的结果将序列回收到原来的序列中

void CountSort(int* a, int n) { //计数排序int min = a[0];//初始化最大值和最小值int max = a[0];for (int i = 0; i < n; i++) { //遍历数组,找最大和最小if (a[i] < min)min = a[i];if (a[i] > max)max = a[i];}int range = max - min + 1;//计数数组个数,因为左闭右闭区间,所以+1int* countA = (int*)malloc(sizeof(int) * range);if (!countA) {perror("malloc int* countA fail");return;}memset(countA, 0, sizeof(int) * range);//计数数组所有数据初始化为0//相对映射//开辟一个计数数组countA用于存放原数组(a)各元素出现次数//最小值min在第一个位置(下标0),最大值max在最后位置(下标rang-1)//统计次数for (int i = 0; i < n; i++) //循环遍历原数组元素countA[a[i] - min]++;//计算元素在计数数组中的索引://a[i] - min:这一步计算出了当前元素 a[i] 在计数数组 countA 中的索引位置。//由于计数数组是以最小值 min 为基准,所以使用 a[i] - min 可以将元素映射到从0开始的计数数组索引上。//增加计数://countA[a[i] - min]++:这里对应索引位置的计数器加1。//意味着每当发现原数组中某个元素 a[i],就在 countA 中对应的位置增加其出现次数。//排序int k = 0;//初始化一个索引 k,用于逐步填充排序后的数组 a。for (int j = 0; j < range; j++) //遍历计数数组 countA 的所有索引 j。while (countA[j]--) //当 countA[j] 的值大于0时执行。j下标处的值是几,说明出现了几次		//索引 j 表示的是相对于最小值 min 的偏移量,即 j 代表的是原始数组中的元素值与 min 的差值。a[k++] = j + min;//j + min(实际的元素值)
}

 计数排序的特性总结:

  1. 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限。
  2. 时间复杂度:O(MAX(N,范围))
  3.  空间复杂度:O(范围)
  4. 稳定性:稳定

第三章:排序算法复杂度及稳定性分析

 

第四章:选择题练习

1. 快速排序算法是基于( )的一个排序算法。

A 分治法
B 贪心法
C 递归法
D 动态规划法

答案:A

2. 对记录(54,38,96,23,15,72,60,45,83)进行从小到大的直接插入排序时,当把第8个记录45插入到有序表时,为找到插入位置需比较( )次?(采用从后往前比较)

A 3
B 4
C 5
D 6

答案:C

3. 以下排序方式中占用O(n)辅助存储空间的是

A 简单排序
B 快速排序
C 堆排序
D 归并排序

答案:D

4. 下列排序算法中稳定且时间复杂度为O(n2)的是( )

A 快速排序
B 冒泡排序
C 直接选择排序
D 归并排序

答案:B

5. 关于排序,下面说法不正确的是

A 快排时间复杂度为O(N*logN),空间复杂度为O(logN)
B 归并排序是一种稳定的排序,堆排序和快排均不稳定
C 序列基本有序时,快排退化成冒泡排序,直接插入排序最快
D 归并排序空间复杂度为O(N), 堆排序空间复杂度的为O(logN)

答案:D

6. 下列排序法中,最坏情况下时间复杂度最小的是( )

A 堆排序
B 快速排序
C 希尔排序
D 冒泡排序

答案:A

7. 设一组初始记录关键字序列为(65,56,72,99,86,25,34,66),则以第一个关键字65为基准而得到的一趟快速排序结果是()

A 34,56,25,65,86,99,72,66
B 25,34,56,65,99,86,72,66
C 34,56,25,65,66,99,86,72
D 34,56,25,65,99,86,72,66

答案:A(挖坑法)

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

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

相关文章

LIO-SAM编译ubuntu20.04 Noetic

一、下载 mkdir -p ~/lio_sam_ws/src cd ~/lio_sam_ws/src git clone https://github.com/TixiaoShan/LIO-SAM.git cd ..二、编译&&解决报错 catkin_make报错如下 解决方案&#xff1a; 第一步&#xff1a; sudo add-apt-repository ppa:borglab/gtsam-release-4…

数学建模美赛经验小结

图片资料来自网络所听讲座&#xff0c;感谢分享&#xff01;

网络编程的学习之udp

Udp编程过程 Sento不会阻塞 实现聊天室效果 上线 聊天 下线 服务端需要一个地址&#xff0c;去保留名字和ip地址 交互的时候发结构体 下面这个宏只能在c语言里使用 ser.sin_port htons(50000); 上面是端口号50000以上&#xff0c;两边要一样 这里是不要让udp发的太快&am…

Unity Shader学习笔记

Shader类型 类型详情Standard Surface Shader标准表面着色器&#xff0c;基于物理的着色系统&#xff0c;用于模拟各种材质效果&#xff0c;如石头、木材、玻璃、塑料和金属等。Unlit Shader最简单的着色器&#xff0c;不包含光照但包含雾效&#xff0c;只由最基础的Vertex Sh…

30. 梯度下降法及其应用

1. 引言 在深度学习中&#xff0c;损失函数的求解是一个关键步骤。损失函数通常没有解析解&#xff0c;因此需要通过最优化算法来逼近求解。其中&#xff0c;梯度下降法是最常用的优化算法之一。本文将详细介绍梯度下降法的基本概念、理论基础、及其在深度学习中的应用。 2. …

甄选范文“论基于构件的软件开发方法及其应用”,软考高级论文,系统架构设计师论文

论文真题 基于构作的软件开发 (Component-Based Software Development,CBSD) 是一种基于分布对象技术、强调通过可复用构件设计与构造软件系统的软件复用途径。基于构件的软件系统中的构件可以是COTS (Commercial-Off-the-Shelf)构件,也可以是通过其它途径获得的构件(如自…

2970.力扣每日一题7/10 Java(暴力枚举)

博客主页&#xff1a;音符犹如代码系列专栏&#xff1a;算法练习关注博主&#xff0c;后期持续更新系列文章如果有错误感谢请大家批评指出&#xff0c;及时修改感谢大家点赞&#x1f44d;收藏⭐评论✍ 目录 解题思路 解题方法 时间复杂度 空间复杂度 Code 解题思路 incre…

图论---无向图中国邮路的实现

开始编程前分析设计思路和程序的整体的框架&#xff0c;以及作为数学问题的性质&#xff1a; 程序流程图&#xff1a; 数学原理&#xff1a; 本质上是找到一条欧拉回路&#xff0c;考虑图中的边权重、顶点的度数以及如何通过添加最少的额外边来构造欧拉回路&#xff0c;涉及到欧…

链表 OJ(一)

移除链表元素 题目连接&#xff1a; https://leetcode.cn/problems/remove-linked-list-elements/description/ 使用双指针法&#xff0c;开始时&#xff0c;一个指针指向头节点&#xff0c;另一个指针指向头节点的下一个结点&#xff0c;然后开始遍历链表删除结点。 这里要注…

推荐一款功能强大的 GPT 学术优化开源项目GPT Academic:学术研究的智能助手

今天&#xff0c;我将向大家介绍一个强大的开源项目—GPT Academic&#xff0c;它或许正是你一直在寻找的理想工具。 已一跃成为 60.4k Star 的热门项目 GPT Academic 目前在 GitHub 上已经揽获了 60.4k 的 Star&#xff0c;这不仅反映了它的受欢迎程度&#xff0c;更证明了它…

硅纪元AI应用推荐 | 百度橙篇成新宠,能写万字长文

“硅纪元AI应用推荐”栏目&#xff0c;为您精选最新、最实用的人工智能应用&#xff0c;无论您是AI发烧友还是新手&#xff0c;都能在这里找到提升生活和工作的利器。与我们一起探索AI的无限可能&#xff0c;开启智慧新时代&#xff01; 百度橙篇&#xff0c;作为百度公司在202…

【网络安全】Oracle:SSRF获取元数据

未经许可&#xff0c;不得转载。 文章目录 前言正文漏洞利用 前言 Acme 是一家广受欢迎的播客托管公司&#xff0c;拥有庞大的客户群体。与许多大型运营公司一样&#xff0c;Acme 采用了Apiary的服务&#xff0c;使用户能够安全高效地管理他们的播客。 Apiary 于2017年初被Or…

PostgreSQL16安装Mac(brew)

问题 最近需要从MySQL切换到PostgreSQL。我得在本地准备一个PostgreSQL。 步骤 使用brew安装postgresql16: arch -arm64 brew install postgresql16启动postgresql16: brew services start postgresql16配置postgresql环境变量&#xff0c;打开环境变量文件&#xff1a; …

LabVIEW优化氢燃料电池

太阳能和风能的发展引入了许多新的能量储存方法。随着科技的发展&#xff0c;能源储存和需求平衡的方法也需要不断创新。智慧城市倡导放弃石化化合物&#xff0c;采用环境友好的发电和储能技术。氢气系统和储存链在绿色能源倡议中起着关键作用。然而&#xff0c;氢气密度低&…

从零开始实现大语言模型(三):Token Embedding与位置编码

1. 前言 Embedding是深度学习领域一种常用的类别特征数值化方法。在自然语言处理领域&#xff0c;Embedding用于将对自然语言文本做tokenization后得到的tokens映射成实数域上的向量。 本文介绍Embedding的基本原理&#xff0c;将训练大语言模型文本数据对应的tokens转换成Em…

【算法】排序算法介绍 附带C#和Python实现代码

1. 冒泡排序(Bubble Sort) 2. 选择排序(Selection Sort) 3. 插入排序(Insertion Sort) 4. 归并排序(Merge Sort) 5. 快速排序(Quick Sort) 排序算法是计算机科学中的一个基础而重要的部分,用于将一组数据按照一定的顺序排列。下面介绍几种常见的排序算法,…

CSS技巧专栏:一日一例 3.纯CSS实现炫酷多彩按钮特效

大家好,今天是 CSS技巧专栏:一日一例 第三篇《纯CSS实现炫酷多彩按钮特效》 先看图: 开工前的准备工作 正如昨日所讲,为了案例的表现,也处于书写的习惯,在今天的案例开工前,先把昨天的准备工作重做一遍。 清除浏览器的默认样式定义页面基本颜色设定body的样式清除butt…

云视频监控中的高效视频转码策略:视频汇聚EasyCVR平台H.265自动转码H.264能力解析

随着科技的快速发展&#xff0c;视频监控技术已经广泛应用于各个领域&#xff0c;如公共安全、商业管理、教育医疗等。与此同时&#xff0c;视频转码技术作为视频处理的关键环节&#xff0c;也在不断提高视频的质量和传输效率。 一、视频监控技术的演进 视频监控技术的发展历…

SEO之网站结构优化(一)

初创企业搭建网站的朋友看1号文章&#xff1b;想学习云计算&#xff0c;怎么入门看2号文章谢谢支持&#xff1a; 1、我给不会敲代码又想搭建网站的人建议 2、新手上云 网站内的优化大致可以分为两部分&#xff0c;一是网站结构调整&#xff0c;二是页面上针对关键词的相关性优化…

前端八股文 闭包的理解

什么是闭包 闭包是指有权访问另一个函数作用域中的变量的函数 ——《JavaScript高级程序设计》 &#xff08;闭包 内层函数 引用的外层函数的变量&#xff09; 下面就是一个简单的闭包 闭包不一定必须有 return 闭包不一定有内存泄漏 闭包 什么时候用到 return 就是 外部…