一篇文章了解常用排序算法

排序

文章目录

  • 排序
  • 直接(插入)排序InsertSort
    • 思想
    • 实现方法:
  • 希尔排序ShellSort(可过OJ)
    • 思想
    • 预排序
    • gap的作用
    • 整体代码
  • 选择排序SelectSort
    • 思想
    • 完整代码
  • 堆排序HeapSort(可过OJ)
    • 思想
      • 大根堆
      • 向下调整
    • 完整代码
  • 冒泡排序BubbleSort
  • 快速排序(快排)QuickSort
    • 缺陷
    • 三数取中法,规避缺陷情况
    • 快排的实现方式
      • 双(左右)指针法
      • 挖坑法
      • 前后指针法
    • 快排的非递归方式
  • 归并排序(外排序)MergeSort (像后序遍历)(可过OJ)
    • 归并排序的内存中(内排序)实现方法
    • 归并排序内排序的非递归
    • 归并排序的外排序实现方法(加餐)
  • 一些小众的排序
    • 计数排序
    • 基数排序
  • 排序算法复杂度及稳定性分析
    • 关于稳定性的理解
  • 递归的缺陷
  • 递归改非递归的方式
  • 递归改非递归为什么
  • 测试排序的接口
  • 测试排序算法的OJ题
  • 一些练习题

直接(插入)排序InsertSort

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

思想

把需要排序的数组按大小依次插入到已经排好序的数组里面去,得到一个新的有序数组。

实现方法:

默认数组只有一个数,因为是一个数,所以一定是有序的,然后把将要排序的数组的第二个数拿过来,再逆序遍历已经排好序的数组,也就是那一个数,跟这个数比较大小,一定是大于小于等于这三个里面的一个,如果是递增排序的话,后面是越来越大,那么这个待排序的数如果比已经拍好的数组的最后一个数大的话,直接插入就可以了。如果待排数比已经排好序数组的最后一个数小,就把排好序的数比较的这个数,用它前面的数覆盖,就相当于数据后移了,再把这个待排的数跟前面的数比较大小,如果还小就再覆盖。

void InsertSort(int* a, int n)
{int end;int tmp;int i = 1;while (i < n){end = i;tmp = a[end];// 去找有序数组中哪一个数比tmp小,当然是倒着找了while (end > 0 && a[end - 1] > tmp){a[end] = a[end - 1];end--;}//  此时end指向的数字是比tmp小的,也可能能找不到比tmp小的,tmp就排到对头上去了a[end] = tmp;i++;// i是a数组的第i个数,如果再插入一个数,放在队尾就好了,再运行一遍函数就可以了}
}

请添加图片描述

不难发现,随着 i 的增大,每次处理数据的数量也在逐增。因为只有一个数据的时候不用比较,所以直接让i从1开始,也就是至少两个数据才开始比较。

每一次都是把将要插入的数据倒着根其他数据比较,找到合适的位置后插入。保持数组一直有序,也不难发现这种方法最坏的情况是逆序,或者接逆序。上面这个样例中当i走到最后的时候,这个1要移动8次。特别浪费时间。

希尔排序ShellSort(可过OJ)

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

希尔排序的特性总结:

  1. 希尔排序是对直接插入排序的优化。
  2. 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。
  3. 希尔排序的时间复杂度不好计算,需要进行推导,推导出来平均时间复杂度: O(N1.3 —N2
  4. 稳定性:不稳定

思想

就是对直接插入排序的优化,对于直接插入排序来说,这个序列趋近于有序,时间复杂度就越低。反之,这个当序列杂乱的时候,或者是与题目要求的顺序相反,就会非常浪费时间。所以,希尔对直接排序的代码进行了优化,增加了一个预排序的过程。

预排序

设置gap(间距),把一个完整的数组按照gap分组,对于每一个组再单独进行直接排序。通过预处理过程,可以尽量的使数列趋近于有序。当数列趋近于有序的时候,再使用直接排序对整个序列排序,时间会大大降低。当数据足够大的时候才能更好的展现预排序对直接排序的好处。

gap的作用

gap是对数组预排序的隔断的存在,当gap最大的时候,也就是数组本身的大小,是无意义的。但是比n小一点点,就会有很大的作用。

比如,a[]={9,5,3,4,6,8,7,2,1};

对于直接排序来说,9需要移动9次才能从第一位移动到最后一位。

对于希尔排序来说,当gap为3的时候,9会依次与4,7,进行比较,很明显正确顺序是4,7,9。经过一次排序后,数组就是{4,5,3,7,6,8,9,2,1}.

数组稍微有序一点点,然后是第二位为首元素的时候,间距为gap的数就是{5,6,2},排序后是{2,5,6}.放到原数组就是{4,2,3,7,5,8,9,6,1}.

然后是{3,8,1}》》》{1,3,8}。数组就是{4,2,1,7,5,3,9,6,8}.

不难发现,9已经到了队尾了。8也差不多。这些较大的数已经足够靠后了,像是1,2这些数也足够靠前了。

再缩小gap的值,因为已经有序了,所以时间会越来越快。当gap为1的时候就是直接排序了。又因为足够有序,所以时间复杂度趋近于O(N).

算上前面预排序的时间,最快是O(N1.3),最慢也不到O(N2)

整体代码

void ShellSort(int* a, int n)
{int end;int tmp;int gap = n + 1; // gap要以实际的n为准,实际的n还要大个1while (gap > 1) // 保证最后一次gap是1,相当于直接排序{gap = (gap / 3) + 1;int i = 0;while (i <= n){end = i;tmp = a[end];while (end > 0 && end-gap >= 0 && a[end - gap] > tmp){a[end] = a[end - gap]; // 和直接排序结构一样,不同的就是直接排序一直是一个一个的比,希尔排序是隔gap位数比一次end -= gap;}a[end] = tmp;i++;}}
}
void Test()
{int a[] = { 3,-1 };ShellSort(a, sizeof(a) / sizeof(int) - 1);Printarray(a, sizeof(a) / sizeof(int));
}

请添加图片描述

首先是以gap为8/2=4开始排序,和直接排序一样,每次排序的数,都跟前面的数比较,但是希尔排序有了gap这个概念,也就是每次比较的数只能跟这个数前面相隔gap的数比较。这样最后一位的1只需要移动((n/gap)+1)次,就可以到达正确的位置。当前在gap等于1之前,这个数列不一定有序,但是接近有序。

选择排序SelectSort

思想

就是遍历一次数组,找数组的最大值和最小值的下标,然后放在数组的头部和尾部。再缩小数组的大小,头begin后移,尾end前移,当他俩相遇的时候就停止。

所以要一直遍历整个数组,无论这个数组是否是有序的,都要完整的遍历一次数组(从begin到end)。

完整代码

void SelectSort(int* a, int n)
{int begin = 0;int end = n - 1;while (begin < end){int min, max;min = begin;max = begin;for (int i = begin + 1; i <= end; i++){if (a[i] < a[min])min = i;if (a[i] > a[max])max = i;}Swap(&a[begin], &a[min]);if (max == begin) // 修正max的位置max = min;Swap(&a[end], &a[max]);begin++;end--;}
}

请添加图片描述

对于第一次循环,最小值是1,最大值是9,所有{1,9}归位,再是{2,8}归位。依次向中间靠拢…

堆排序HeapSort(可过OJ)

堆排序的特性总结:

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

思想

先是建堆。根据需求分为大根堆和小根堆两种情况。

如果是升序,从小到大的顺序,也就是说最小的值在前面。

理清思路后,再好好想想,堆排序只有一次建堆的机会,倘若是建小堆。数组的第一位是最小的数不假。第二次该怎么办呢,是左子节点还是右子节点。如果是右子节点,要不要交换左右子节点的值,后面的节点怎么办?

全是问题。但是逆向思维就好了。比如,大根堆。每次找最大的值,然后把最大的值放在堆底。也就是交换a[0]和a[end]。只需要每次让end前移一次,堆变小了,最二大的数又放到根部了,再交换值。然后向下调整根部。循环即可。

建堆的时间复杂度是O(N),每次向下调整的时间是O(log2N).

差不多就是O(N*logN)了。

大根堆

就是在这个完全二叉树中,根的值,是这颗树最大的值,为了建出这个大根堆,还要保证每个父节点都要大于子节点的值。所以从最后一个父节点开始,逆向向下调整。

向下调整

以当前节点为父节点,判断它的子节点,是否比父节点大,如果子节点比父节点大,就把他们的值交换。再以子节点为父节点,找新的子节点,再调整。

完整代码

// 堆排序 // 建大根堆,先找最大的数,再把最大的数放到最后一位,然后n--,再向下调整
void AdjustDwon(int* a, int n, int root)
{int parent = root;int child = parent * 2 + 1;while (child < n){if (child + 1 < n && a[child + 1] > a[child])child++;if (a[child] > a[parent]){Swap(&a[child], &a[parent]);parent = child;child = parent * 2 + 1;}elsebreak;}
}
void HeapSort(int* a, int n)
{for (int i = (n - 1 - 1) / 2; i >= 0; i--){AdjustDwon(a, n, i);}while (n--){Swap(&a[0], &a[n]);AdjustDwon(a, n, 0);}
}

冒泡排序BubbleSort

冒泡排序的特性总结:

  1. 冒泡排序是一种非常容易理解的排序
  2. 时间复杂度:O(N^2)
  3. 空间复杂度:O(1)
  4. 稳定性:稳定
// 冒泡排序
void BubbleSort(int* a, int n)
{while (n){for (int i = 0; i < n - 1; i++){if (a[i] > a[i + 1])Swap(&a[i], &a[i + 1]);}n--;}
}

快速排序(快排)QuickSort

(都无法通过,卡在2222循环的数据了,也就是快排最怕遇到的情况,有序的,只有一个数的数据,不但大量消耗栈空间,处理数据的过程跟冒泡一样,甚至更麻烦)

选最右边的值做key,那么一定要让左边begin先走,这样能保证他们相遇的位置是一个比key大的位置;选最左边的值做key,同理要让end先走。

啊哈算法里面的那个,快排思想。那里的基准数是现在的key。

反正都是在头或尾选基准数key,然后从另一头开始找值。意思就是key选左边,就从右边开始找值。key在右边,就从左边开始找值。

快速排序的特性总结:

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

缺陷

数列不能是接近有序的,否则时间复杂度是O(N2)。而且会导致栈溢出。因为是递归吗~

三数取中法,规避缺陷情况

防止取key的时候取到最大值或最小值。所以找数组的中位数和最左边和最右边三个数进行比较。选这三个数中间的那个。既不选最小也不选最大。保证排完序之后,div不在两端即可。当然选出来中位数之后,把它和key交换位置。就能避免缺陷了。避免缺陷后,这个排序就是无敌的O(N*logN)了。好吧,也不是无敌的,对于只有一个数的数列,再怎么找也找不到合适的基准数。

int midnomber(int a, int b, int c)
{if (a > b){if (b > c){return b;}if(c > a){return a;}return c;}else{if (c > b){return b;}if (c < a){return a;}return c;}
}

就是找三个数的中间数的函数。

但是正常情况下,三数取中法可以规避掉这个区间的最大值或者最小值,选合适的数作基准数。

快排的实现方式

双(左右)指针法

两个指针begin和end。begin从数组的头开始向后找数,end从数组的尾开始向前找数。当begin遇到比基准数(key)大的就停止,否则就++begin(继续向后找);而end从当前数组的尾开始倒着找比基准数小的数,当end指向的数符合条件,进行交换,把begin和end指向的数交换位置。较大的数就移动到数组后面去,较小的数就排到数组前面了。

// 双指针法
int midnomber(int a, int b, int c)
{if (a > b){if (b > c){return b;}if(c > a){return a;}return c;}else{if (c > b){return b;}if (c < a){return a;}return c;}
}
void QuickSort(int* a, int left, int right)
{if (left >= right) return;int midd = midnomber(left, (left + right) / 2, right);Swap(&a[left], &a[midd]);int key = left;int begin = left;int end = right;while (begin < end){while (begin < end && a[end] >= a[key]){end--;}while (begin < end && a[begin] <= a[key]){begin++;}Swap(&a[begin], &a[end]);}Swap(&a[key], &a[begin]);QuickSort(a, left, begin - 1);QuickSort(a, begin + 1, right);
}

请添加图片描述

递归,分治法,也就是大事化小,小事化了的方式。把[0,8]分成[0,4]和[6,8]。再划分[0,1]和[3,4],对于一个数的区间直接忽略,一个数既是最大也是最小,排不了。

在第一次处理数据的时候,针对的是[0,8]这个区间,不难发现,利用三数取中法,我们取6作为基准数,在第一次排好数组中,6的左边都比6小,右边都比6大。

挖坑法

假设基准数都是最左边的那个。

先把基准数那里的数取出来(保存到key),相当于数组的最左边就为空了,也就是挖了一个坑,然后再从最右边往左找比key小的数。同时把这个小的数移到前面那个坑里面去。(虽然数组里面的数并没有真正消失,变成那个坑。但是在某种意义上它就是坑。)。接着再从左往右找比基准数大的数移到那个坑里去。当这两个指针相遇的时候,就把key基准数填进去。

其实和双指针法差不多。

void QuickSortKeng(int* a, int left, int right)
{if (left >= right) return;int midd = midnomber(left, (left + right) / 2, right);Swap(&a[left], &a[midd]);int key = a[left];int begin = left;int end = right;while (begin < end){while (begin < end && a[end] >= key){end--;}a[begin] = a[end];while (begin < end && a[begin] <= key){begin++;}a[end] = a[begin];}a[begin] = key;QuickSortKeng(a, left, begin - 1);QuickSortKeng(a, begin+1,right);
}

最好也加上三数取中法

请添加图片描述

对于挖坑法,原理和前后指针法差不多,不过前后指针法,需要先找到符合条件的两个数,才能完成交换。而挖坑法是一直保持一个坑用来存放数据。在第一次[0,8]这个区间,也是先用三数取中法以6为基准数,把小于6的放左边,大于6的放右边。最后一个坑放基准数。

前后指针法

创建两个指针prev和cur。cur负责遍历数组,prev负责记录比基准数小的数的最右边的下标。

假设基准数是数组最右边的数。

那么cur和prev就从左边开始向右遍历数组,cur从左到右依次遍历数组,prev也是从左到右遍历。定义prev比cur的起始位置靠左一位。

不同的是,cur的作用是识别当前指向的数的大小。如果cur所指的数小于基准数key,说明它的位置正确,cur和prev同时右移一次。

当cur指向的数大于基准数,prev指向这个数的下标。然后cur继续后移,寻找比基准数小的数。当cur向后移动的时候有两种结果:找到一个比基准数小的数,那么交换cur和prev所指向的数,把大的数后移,同时cur和prev右移一次;cur直到遍历完整个数组都没找到比基准数小的数,因为此时prev还指向比基准数大的数,最后再交换基准数和prev。

然后就是划分下次递归的区间。

// 前后指针法
void QuickSortp(int* a, int left, int right)
{if (left >= right) return;int midd = midnomber(left, (left + right) / 2, right);Swap(&a[right], &a[midd]);int key = a[right];int cur = left;int prev = cur;while (cur < right){if (a[cur] < key && a[prev] < key){cur++;prev++;}else if (a[cur] < key && a[prev] > key){Swap(&a[prev], &a[cur]);prev++;cur++;}else{cur++;}}Swap(&a[prev], &a[cur]);QuickSortp(a, left, prev - 1);QuickSortp(a, prev + 1, right);
}

请添加图片描述

这三种快排排序的效果都不太一样,但是消耗的时间都差不多。

有人说,前后指针法里面的两个指针就像两个火车头,cur在前面移动,prev把符合条件的车厢链接到自己这边,prev遇到不符合条件的车厢就等cur找合适的车厢。

快排的非递归方式

因为快排是递归类型的,在使用递归的时候会大量消耗栈空间。所以为了优化这种情况,面试官会要求用非递归的方式,实现快排。

非递归的方式,就是建立一个栈,这个栈来储存每次递归的区间。当栈清空的时候说明所有数据都被排好序了。

整体思路:

建立一个while循环,循环的条件就是栈里有数据,用StackEmpty来判断栈的情况.当然没学c++呢,只能是用手搓的c语言的栈。还要再导入文件到项目里面,这里就先不实现了。

在进入while循环之前把整个数组的左右下标入栈。然后进入循环。

循环:先从栈里面获取数组的下标,然后调用一次快排,这个快排的返回值是中间值。然后是入栈操作,如果像先处理左区间,就先入右区间。因为栈是后进先出。在入栈的时候,要分情况,可以是建立一个结构体,这个结构体就是存区间的下标。或者不创建结构体,直接把区间的下标存进去。这时候还要分情况,是先入区间的左下标还是右下标。因为取的时候是反过来的。

循环的大概流程:判断栈是否为空,不为空就取数据,进行快排得到中间值下标,再入数据进栈。当栈为空就退出

// 伪代码
int QuickSortPart(int*a,int left,int right)
{int midd = midnomber(left, (left + right) / 2, right); // 三数取中Swap(&a[left], &a[midd]);int key = left;int begin = left;int end = right;while (begin < end){while (begin < end && a[end] >= a[key]){end--;}while (begin < end && a[begin] <= a[key]){begin++;}Swap(&a[begin], &a[end]);}Swap(&a[key], &a[begin]);return begin;
}
void QuickSort(int* a, int left, int right)
{// 建立一个栈Stack st;StackInit(&st);StackPush(&st,right - 1); // 先入的右,就要先取左StackPush(&st,left);while(!StackEmpty(&st)){int begin = StackTop(&st); // 先取区间左下标StackPop(&st);int end = StackTop(&st);StackPop(&st);int div = QuickSortPart(a, begin, end);if((div - 1) - left > 1){StackPush(&st,div-1);StackPush(&st,left);}if(right - (div + 1) > 1){StackPush(&st,right);StackPush(&st,div + 1);}}
}

归并排序(外排序)MergeSort (像后序遍历)(可过OJ)

上面的排序都叫内排序,指在内存中排序效果更好。

归并排序是外排序,使用在硬盘、文件排序。

归并排序的特性总结:

  1. 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。(文件与文件之间排序,可以极少的占用内存)
  2. 时间复杂度:O(N*logN)
  3. 空间复杂度:O(N)
  4. 稳定性:稳定

归并排序的内存中(内排序)实现方法

// 归并排序
void MergeSortPart(int* a,int left,int right,int* tmp)
{if (left >= right) return;// 把目前的数组分成左右两个区间int begin1 = left, end1 = (left + right) / 2; int begin2 = ((left + right) / 2) + 1, end2 = right;MergeSortPart(a, begin1, end1, tmp); // 一直向左区间递归,直到左区间不可被划分MergeSortPart(a, begin2, end2, tmp); // 如果左区间到头了,返回后才是这个递归的开始int i = 0; // 作为两个区间比较后插入到tmp数组的位置while (begin1 <= end1 && begin2 <= end2){if (a[begin1] < a[begin2]){tmp[left+i] = a[begin1];begin1++;}else{tmp[left+i] = a[begin2];begin2++;}i++;}while (begin1 <= end1) // 如果左区间还有剩余的数,再移到tmp数组中{tmp[left+i] = a[begin1++];i++;}while (begin2 <= end2) // 这里是检查右区间{tmp[left+i] = a[begin2++];i++;}for (int i = left; i <= right; i++){a[i] = tmp[i]; // 再把tmp空间的数据移到原数组}
}void MergeSort(int* a, int n)
{int* tmp = (int*)malloc(sizeof(int) * n); // 临时空间来暂存数据MergeSortPart(a, 0, n - 1, tmp);free(tmp); // 排完序后原数组有序,临时空间就没用了
}

请添加图片描述

不难发现,本应是[0,8]的数组被划分成小份小份的。也就是先一直划分到左区间不可被划分的状态才停止,即[0.1]。然后逐层上返至[0,2]等等。

请添加图片描述

大概的过程就是这样。

请添加图片描述

就像二叉树的后序遍历一样,不过这里先是把区间划到最小的可以排序的区间[0,1],也就是开始往回返的开始,也是处理数据的开始。从这一步,排序才开始。

归并排序内排序的非递归

跟快排递归改非递归的方法一样,都是把区间入栈,再从栈中取数据出来处理。等学了c++再实现。

好吧,还是有必要记一下,因为除了用栈,还有另一种方法实现归并排序的非递归。

要先理清一个思路,在递归的时候,第一次数据处理发生的时候,是在小区间,也就是数据两两一组。还有归并排序的特性,即把两组有序的数据合并。

所以,我们从最小的区间开始合并数组。也就是[0,1] [2,3] ;[4,5] [6,7] ;[8,9].

很明显,我们也可能会遇到奇数的i情况,因此还要修正区间的右下标。

因为是两个区间进行合并,防止一个区间被多次合并,我们还要一个特殊的变量。比如,01和23会合并一次.我们希望下次处理的数据是45和67。在0与4之间隔的是2*gap个数据。这里的gap就是每个区间数据的多少。gap从2开始。每次循环gap都要乘2.当gap大于总数据n的时候结束。

下一次再处理数据的时候,这个区间要变成之前的二倍,2gap=4,[0,1,2,3] [4,5,6,7]; [8,9].。

对于后面89这个区间的数据,它不满足2gap个数据。所以要及时修正一下区间的大小。

归并排序的外排序实现方法(加餐)

这是道面试题:给你10亿个数的文件,正常来说绝大多数的排序都是内排序,也就是需要把数据移到内存中,再排序。很明显,10亿个数比较庞大,无法一次性放到内存中去。所以我们可以把这个十分庞大的数据分割成小份的,具体是多小呢?要看你所使用的电脑最大能在内存中处理的数据。如果你的电脑最大可以一次性排100万位数,那就把10亿个数切成1000份小文件。再切这些小文件的时候,进行小范围的排序。保证每个文件里面的数据是拍好序的,可以用快排排序。然后写到文件里面。完成切割之后。

再用归并排序的外排序,可以是把相邻两个文件排序放到新文件,再把两个新文件合并。一直到最后一个。

void MergeSortFile()
{// 打开大文件// 读取大文件的前100万个数据,放到内存中排序,再写到子文件1中。// 一直到大文件全部分割完毕// 再把子文件1和2合并到12.3和4合并到34……// 因为每个子文件都是有序的,所以可以用归并排序进行外排序}

比较的困难的是给子文件取名,还有子文件合并的时候,子文件名称的获取

但是据说c++也有专门的库,先不实现了,把思路写一下。。。

一些小众的排序

计数排序

就是类似之前学的桶排序(虽然不是真正意义上的桶排序),但是这个排序在某些条件下也很快。

条件:数量足够大且特别集中。

意思就是给你一堆数,这些数都在一个比较小的范围里面,这个范围就是最大的数减去最小的数的差值rang。

然后去遍历这些数,把最小的数放在0的位置,最大的数放在rang的位置。

遍历的时候,让得到的数减去最小数,再放到新数组里面去,这个新数组就是统计每个数出现的次数。

遍历完原数组之后,再逆向写到原数组里面,不过这次就是按顺序写进去了。(写到原数组的时候再把最小值加上,复原)

如果原数组是N个数

那么时间复杂度就是O(MAX(N,rang)) (N和rang较大的那个)

空间复杂度是O(rang)

基数排序

但是真正的桶排序是按照位数的大小分类的,比如是三位数,放进一个桶,两位数就放另外一个桶。

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

请添加图片描述

这个表格不要背,要理解。

关于稳定性的理解

数组中相同值,排完序相对顺序可以做到不变就是稳定的否则就不稳定

递归的缺陷

因为递归是在栈上进行的,递归可以以大化小。但是递归再继续往小里划分的时候,栈上堆积的函数栈帧太多了。会爆掉,所以,还可以在函数里面加一个条件,当这个区间不到10个数的时候,就把这个区间交给直接排序。

递归改非递归的方式

  1. 把递归的过程改成循环,比如斐波那契数列求解,当然也仅限于比较简单的递归过程。
  2. 用栈模拟数据存储

递归改非递归为什么

  1. 提高效率 因为递归建立栈帧是有消耗的,虽然现代计算机,已经可以忽略这个问题了
  2. 因为递归是需要建立函数栈帧的,消耗的是栈空间,而且对于系统而言,栈空间都是M级的,也就是按MB来算的;对于非递归来说,数据存储在堆上,堆的空间是G级的,也就是GB。相比之下,还是堆上储存更好。

测试排序的接口

// 排序实现的接口
// 插入排序
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);
// 快速排序
void QuickSort(int* a, int left, int right);
// 归并排序
void MergeSort(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();// 获取程序运行此处的时间,单位:毫秒 // 头文件<time.h>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题

https://leetcode-cn.com/problems/sort-an-array/

一些练习题

  1. 快速排序算法是基于( )的一个排序算法。
    A分治法
    B贪心法
    C递归法
    D动态规划法
    2.对记录(54,38,96,23,15,72,60,45,83)进行从小到大的直接插入排序时,当把第8个记录45
    插入到有序表时,为找到插入位置需比较( )次?(采用从后往前比较)
    A 3
    B 4
    C 5
    D 6
    3.以下排序方式中占用O(n)辅助存储空间的是
    A 简单排序
    B 快速排序
    C 堆排序
    D 归并排序
    4.下列排序算法中稳定且时间复杂度为O(n2)的是( )
    A 快速排序
    B 冒泡排序
    C 直接选择排序
    D 归并排序
    5.关于排序,下面说法不正确的是
    A 快排时间复杂度为O(N*logN),空间复杂度为O(logN)
    B 归并排序是一种稳定的排序,堆排序和快排均不稳定
    C 序列基本有序时,快排退化成冒泡排序,直接插入排序最快
    D 归并排序空间复杂度为O(N), 堆排序空间复杂度的为O(logN)
    6.下列排序法中,最坏情况下时间复杂度最小的是( )
    A 堆排序
    B 快速排序
    C 希尔排序
    D 冒泡排序
    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

答案:
1.A 2.C 3.D 4.B 5.D 6.A 7.A

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

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

相关文章

Unity URP 仿原神角色渲染过程记录

想学一下NPR渲染&#xff0c;话不多说&#xff0c;先搞一只芙再说&#xff0c;边做边学 一、资源整理 终于是把东西全都集齐了 1、纹理设置 首先要把将Diffuse和Lightmap的压缩改成"无"或"高质量"。 法线贴图的纹理类型改成"法线贴图"。 除颜…

HarmonyOS 角落里的知识 —— 状态管理

一、前言 在探索 HarmonyOS 的过程中&#xff0c;我们发现了许多有趣且实用的功能和特性。有些总是在不经意间或者触类旁通的找到。或者是某些开发痛点。其中&#xff0c;状态管理是ArkUI开发非常核心的一个东西&#xff0c;我们进行了大量的使用和测试遇到了许多奇奇怪怪的问…

Android平台下VR头显如何低延迟播放4K以上超高分辨率RTSP|RTMP流

技术背景 VR头显需要更高的分辨率以提供更清晰的视觉体验、满足沉浸感的要求、适应透镜放大效应以及适应更广泛的可视角度&#xff0c;超高分辨率的优势如下&#xff1a; 提供更清晰的视觉体验&#xff1a;VR头显的分辨率直接决定了用户所看到的图像的清晰度。更高的分辨率意…

001.VMware Workstation Pro虚拟平台安装

我 的 个 人 主 页&#xff1a;&#x1f449;&#x1f449; 失心疯的个人主页 &#x1f448;&#x1f448; 入 门 教 程 推 荐 &#xff1a;&#x1f449;&#x1f449; Python零基础入门教程合集 &#x1f448;&#x1f448; 虚 拟 环 境 搭 建 &#xff1a;&#x1f449;&…

《数字图像处理-OpenCV/Python》第16章:图像的特征描述

《数字图像处理-OpenCV/Python》第16章&#xff1a;图像的特征描述 本书京东 优惠购书链接 https://item.jd.com/14098452.html 本书CSDN 独家连载专栏 https://blog.csdn.net/youcans/category_12418787.html 第16章&#xff1a;图像的特征描述 特征通常是针对图像中的目标或…

快排(霍尔排序实现+前后指针实现)(递归+非递归)

前言 快排是很重要的排序&#xff0c;也是一种比较难以理解的排序&#xff0c;这里我们会用递归的方式和非递归的方式来解决&#xff0c;递归来解决是比较简单的&#xff0c;非递归来解决是有点难度的 快排也称之为霍尔排序&#xff0c;因为发明者是霍尔&#xff0c;本来是命名…

基于Spring Boot+VUE旧物置换网站

1前台首页功能模块 旧物置换网站&#xff0c;在系统首页可以查看首页、旧物信息、网站公告、个人中心、后台管理等内容&#xff0c;如图1所示。 图1系统功能界面图 用户注册&#xff0c;在用户注册页面通过填写用户名、密码、姓名、性别、头像、手机、邮箱等内容进行用户注册&…

FlinkCDC介绍及使用

CDC简介 什么是CDC&#xff1f; cdc是Change Data Capture(变更数据获取)的简称。核心思想是&#xff0c;监测并捕获数据库的 变动(包括数据或数据表的插入&#xff0c;更新以及删除等)&#xff0c;将这些变更按发生的顺序完整记录下来&#xff0c;写入到消息中间件以供其它服…

IPv4的报头详解

1.ipv4的报头&#xff1a; 注意&#xff1a;ipv4的报头长度共有20个字节&#xff0c;数据包通过ipv4协议传输后&#xff0c;会进行封装和解封装&#xff1a; 封装&#xff1a;tcp/ip五层参考模型 应用层网络层传输层数据链路层物理层 网络层&#xff1a;tcp/udp 传输层&#…

【odoo】如何开启开发者模式,开启有什么作用?

概要 在 Odoo 中&#xff0c;开发者模式&#xff08;Developer Mode&#xff09;是一种专门为开发和调试提供的模式。启用开发者模式可以让开发人员访问到更多的功能和信息&#xff0c;从而更方便地进行模块开发、调试和测试。 启用方式&#xff08;主要两种&#xff09; 1.设…

「iOS」UI——无限轮播图实现与UIPageControl运用

「OC」UI 文章目录 「OC」UI无限轮播图的实现以及UIPageControl的实际运用明确要求简单滚动视图的实现UIPageControl的实现设置NSTimer实现自动移动补充实现 进行无限滚动视图的修改思路实现 完整代码展示 无限轮播图的实现以及UIPageControl的实际运用 明确要求 我们要实现一…

四、C#类型转换

在C#中&#xff0c;类型转换是将一个数据类型的值转换为另一个数据类型的过程。 C#中的类型转换可以分为两种&#xff1a;隐式类型转换和显式类型转换&#xff08;也称为强制类型转换&#xff09;。 隐式类型转换 隐式转换是不需要编写代码来指定的转换&#xff0c;编译器会…

SSM旅游系统

摘要 旅游业正处于快速发展阶段&#xff0c;旅游系统的建设已经成为了旅游业发展的重要核心问题。在这样的背景下&#xff0c;SSM框架正逐步发展为一种主要的架构。但目前青海省旅游业信息化的发展仍面临诸多问题&#xff0c;包括系统功能不完善、用户体验不佳、数据管理不规范…

c语言中的字符函数

1.字符分类函数 c语言中有一系列函数是专门做字符分类的&#xff0c;也就是一个字符属于什么类型的字符。这些函数的使用需要包含一个头文件是ctype.h 可能你看这些感觉很懵&#xff0c;我以islower举例 #include<ctype.h> int main() {int retislower(A);printf("…

Apache Doris 之 Docker 部署篇

前言 在现代数据驱动的商业环境中&#xff0c;实时数据分析和高并发查询能力是企业成功的关键因素之一。传统的数据仓库和分析工具在面对大规模数据处理和实时分析需求时&#xff0c;往往力不从心。Apache Doris 作为一个现代的 MPP 数据库管理系统&#xff0c;凭借其强大的查…

uni微信小程序使用lottie

在uni插件市场找到 lottie-uni https://ext.dcloud.net.cn/plugin?id1044按照文档要求安装 HBuilderX 引入 下载或导入示例获取插件 import lottie from /common/lottie-miniprogram.jsindex.vue <template><uni-popupref"popup"type"center"ba…

俄罗斯ozon爆款推荐丨ozon学生受众产品

在俄罗斯电商平台OZON上&#xff0c;学生受众是一个庞大且活跃的群体。为了满足他们的需求&#xff0c;OZON平台上涌现出了一系列受学生欢迎的爆款产品。以下是一些针对学生受众的OZON爆款推荐&#xff1a; OZON选品工具&#xff1a;D。DDqbt。COM/74rD Top1 UNO纸牌游戏 俄语…

【OpenGL学习】OpenGL不同版本渲染管线汇总

文章目录 一、《OpenGL编程指南》第6版/第7版的渲染管线二、《OpenGL编程指南》第8版/第9版的渲染管线 一、《OpenGL编程指南》第6版/第7版的渲染管线 图1. OpenGL 2.1、OpenGL 3.0、OpenGL 3.1 等支持的渲染管线 二、《OpenGL编程指南》第8版/第9版的渲染管线 图2. OpenGL …

windows 下安装Nuclei 详细教程

一、软件介绍 Nuclei是一款基于YAML语法模板的开发的定制化快速漏洞扫描器。它使用Go语言开发&#xff0c;具有很强的可配置性、可扩展性和易用性。 二、下载安装 官网&#xff1a;https://docs.projectdiscovery.io/tools/nuclei/overview Nuclei项目地址&#xff1a;​​…

记一次 .NET某机械臂上位系统 卡死分析

一&#xff1a;背景 1. 讲故事 前些天有位朋友找到我&#xff0c;说他们的程序会偶发性的卡死一段时间&#xff0c;然后又好了&#xff0c;让我帮忙看下怎么回事&#xff1f;窗体类的程序解决起来相对来说比较简单&#xff0c;让朋友用procdump自动抓一个卡死时的dump&#x…