内部排序在数据结构中较为重要,并且开发中经常需要用到,因此归纳相应原理。
目录
一、基础
1.1 定义
内部排序:在内存中进行的排序称为内部排序,
外部排序:而在许多实际应用中,经常需要对大文件进行排序,因为文件中的记录很多,信息量庞大,无法将整个文件拷贝进内存进行排序。因此,需要将带排序的记录存储在外存上,排序时再把数据一部分一部分的调入内存进行排序,在排序中需要多次进行内外存的交互,对外存文件中的记录进行排序后的结果仍然被放到原有文件中。这种排序方法就称外部排序。
1.2 分类
分为几大类:插入排序,交换排序,选择排序,归并排序,基数排序。
工作量区分:
- 简单排序算法:O(n^2)
- 先进排序算法 : O(n*logn)
- 基数排序算法:O(d*n)
二、插入排序与希尔排序
2.1 插入排序
https://baike.baidu.com/item/%E6%8F%92%E5%85%A5%E6%8E%92%E5%BA%8F/7214992?fr=aladdin
插入排序的基本思想是:每步将一个待排序的记录,按其关键码值的大小插入前面已经排序的文件中适当位置上,直到全部插入完为止。
https://www.cnblogs.com/skywang12345/p/3596881.html
平均来说插入排序算法的时间复杂度为O(n^2)。因而,插入排序不适合对于数据量比较大的排序应用。
还有折半插入排序和二路插入排序算法。不详述。
稳定性:
插入排序是在一个已经有序的小序列的基础上,一次插入一个元素。当然,刚开始这个有序的小序列只有1个元素,就是第一个元素。比较是从有序序列的末尾开始,也就是想要插入的元素和已经有序的最大者开始比起,如果比它大则直接插入在其后面,否则一直往前找直到找到它该插入的位置。如果碰见一个和插入元素相等的,那么插入元素把想插入的元素放在相等元素的后面。所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,所以插入排序是稳定的。
代码
void insert_sort(int *array,unsigned int n)
{
int i,j;
int temp;
for(i=1;i<n;i++)
{
temp=*(array+i);
for(j=i;j>0&&*(array+j-1)>temp;j--)
{
*(array+j)=*(array+j-1);
}
*(array+j)=temp;
}
}
或者
void insertion_sort(int array[],int first,int last)
{
int i,j;
int temp;
for(i=first+1;i<last;i++)
{
temp=array[i];
j=i-1;
//与已排序的数逐一比较,大于temp时,该数移后
while((j>=0)&&(array[j]>temp))
{
array[j+1]=array[j];
j--;
}
//存在大于temp的数
if(j!=i-1)
array[j+1]=temp;
}
}
2.2 希尔排序
https://baike.baidu.com/item/%E5%B8%8C%E5%B0%94%E6%8E%92%E5%BA%8F/3229428?fr=aladdin
又称为缩小增量排序
希尔排序(Shell's Sort)是插入排序的一种又称“缩小增量排序”(Diminishing Increment Sort),是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法。该方法因D.L.Shell于1959年提出而得名。
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
图解:https://blog.csdn.net/qq_39207948/article/details/80006224
代码
#include<stdio.h>
#include<math.h>
#define MAXNUM 10
void main()
{
void shellSort(int array[],int n,int t);//t为排序趟数
int array[MAXNUM],i;
for(i=0;i<MAXNUM;i++)
scanf("%d",&array[i]);
shellSort(array,MAXNUM,(int)(log(MAXNUM+1)/log(2)));//排序趟数应为log2(n+1)的整数部分
for(i=0;i<MAXNUM;i++)
printf("%d ",array[i]);
printf("\n");
}
//根据当前增量进行插入排序
void shellInsert(int array[],int n,int dk)
{
int i,j,temp;
for(i=dk;i<n;i++)//分别向每组的有序区域插入
{
temp=array[i];
for(j=i-dk;(j>=i%dk)&&array[j]>temp;j-=dk)//比较与记录后移同时进行
array[j+dk]=array[j];
if(j!=i-dk)
array[j+dk]=temp;//插入
}
}
//计算Hibbard增量
int dkHibbard(int t,int k)
{
return (int)(pow(2,t-k+1)-1);
}
//希尔排序
void shellSort(int array[],int n,int t)
{
void shellInsert(int array[],int n,int dk);
int i;
for(i=1;i<=t;i++)
shellInsert(array,n,dkHibbard(t,i));
}
//此写法便于理解,实际应用时应将上述三个函数写成一个函数。
希尔排序不是稳定的。由于多次插入排序,我们知道一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以shell排序是不稳定的。
三、快速排序算法
快速排序算法在笔试面试考察中较常出现。可以编写几次熟悉此过程,并不难。
它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
3.1 原理及过程
一趟快速排序的算法是:
1)设置两个变量i、j,排序开始的时候:i=0,j=N-1;
2)以第一个数组元素作为关键数据,赋值给key,即key=A[0];
3)从j开始向前搜索,即由后开始向前搜索(j--),找到第一个小于key的值A[j],将A[j]和A[i]的值交换;
4)从i开始向后搜索,即由前开始向后搜索(i++),找到第一个大于key的A[i],将A[i]和A[j]的值交换;
5)重复第3、4步,直到i=j; (3,4步中,没找到符合条件的值,即3中A[j]不小于key,4中A[i]不大于key的时候改变j、i的值,使得j=j-1,i=i+1,直至找到为止。找到符合条件的值,进行交换的时候i, j指针位置不变。另外,i==j这一过程一定正好是i+或j-完成的时候,此时令循环结束)。
3.2 代码
c代码
#include <iostream>
using namespace std;
void quick_sort(int* nums, int left_loc,int right_loc){
int size = right_loc - left_loc + 1;
if (size <= 1)return;
//阈值
int threhood = nums[left_loc];
//左右指针分别向中间遍历,大于阈值则放到右边,小于则放到左边
int left = left_loc;
int right = right_loc;
while (left < right){
//从右向左,如果小于阈值,则换到左边来
while (nums[right] >= threhood && right>left ){
right--;
}
nums[left] = nums[right];
//从左向右,如果大于阈值,则换到右边去
while (nums[left] <= threhood && right>left ){
left++;
}
nums[right] = nums[left];
}
nums[left] = threhood;
quick_sort(nums, left_loc, left - 1);
quick_sort(nums, left + 1, right_loc);
}
int main()
{
int a[10] = { 3, 2, 7, 4, 2, -999, -21, 99, 0, 9 };
int len = sizeof(a) / sizeof(int);
for (int i = 0; i < len; ++i)
cout << a[i] << ' ';
cout << endl;
quick_sort(a, 0, len-1);
for (int i = 0; i < len; ++i)
cout << a[i] << ' ';
cout << endl;
int end; cin >> end;
return 0;
}
c++代码
#include <iostream>
using namespace std;
void Qsort(int arr[], int low, int high){
if (high <= low) return;
int i = low;
int j = high + 1;
int key = arr[low];
while (true)
{
/*从左向右找比key大的值*/
while (arr[++i] < key)
{
if (i == high){
break;
}
}
/*从右向左找比key小的值*/
while (arr[--j] > key)
{
if (j == low){
break;
}
}
if (i >= j) break;
/*交换i,j对应的值*/
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
/*中枢值与j对应值交换*/
int temp = arr[low];
arr[low] = arr[j];
arr[j] = temp;
Qsort(arr, low, j - 1);
Qsort(arr, j + 1, high);
}
int main()
{
int a[] = {57, 68, 59, 52, 72, 28, 96, 33, 24};
Qsort(a, 0, sizeof(a) / sizeof(a[0]) - 1);/*这里原文第三个参数要减1否则内存越界*/
for(int i = 0; i < sizeof(a) / sizeof(a[0]); i++)
{
cout << a[i] << "";
}
return 0;
}/*参考数据结构p274(清华大学出版社,严蔚敏)*/
3.3 相关问题
时间复杂度?
快速排序的平均时间复杂度为( )?
正确答案: B
- O(n)
- O(nlog(n))
- O(log(n))
- O(n^2)
平均时间复杂度 O(nlog(n)),最差时间复杂度 O(n^2)。
- 平均时间复杂度很好理解,一次遍历后,第一个用于比较的元素恰好在中间。
- 最差时间复杂度,对于1,2,3,4,5,6,已经分好,但是每次都需要遍历一遍,才可以进行下次递归。
快速排序算法填充
这个并不是像我们之前编写的程序那样,而是快排算法的另一种编写方式。
inline void swap(int &a, int &b) {
int t = a;
a = b;
b = t;}
int partition(int *a, int p, int r) {
int x = a[_____];
int i = p - 1;
for(int j = p; j < r - 1; ++j) {
if (a[j] <= x) {
___;
swap(___,a[j]);
}
}
swap(a[i+1],___);
return ___;}
void quicksort(int *a, int p, int r) {
if (p < r - 1) {
int q = partition(a, p, r);
quicksort(a, p, q);
quicksort(a, q+1, r);
}
}
int main( ) {
const int N = 100;
int a[N]; // Initialized
quicksort(a, 0, N);
return 0; }
参考答案:
r – 1
++i或 i++
a[i]
a[r-1]
i + 1
四、选择排序
https://baike.baidu.com/item/%E9%80%89%E6%8B%A9%E6%8E%92%E5%BA%8F/9762418?fr=aladdin
简单选择排序、树形选择排序、堆排序,其中堆排序经常出现。
选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理是:第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小(大)元素,然后放到已排序的序列的末尾。以此类推,直到全部待排序的数据元素的个数为零。选择排序是不稳定的排序方法。
4.1 简单选择排序
即每次迭代找到最大(小)的数,与第一个位置的数互换。一直迭代即可。
对比数组中前一个元素跟后一个元素的大小,如果后面的元素比前面的元素小则用一个变量k来记住他的位置,接着第二次比较,前面“后一个元素”现变成了“前一个元素”,继续跟他的“后一个元素”进行比较如果后面的元素比他要小则用变量k记住它在数组中的位置(下标),等到循环结束的时候,我们应该找到了最小的那个数的下标了,然后进行判断,如果这个元素的下标不是第一个元素的下标,就让第一个元素跟他交换一下值,这样就找到整个数组中最小的数了。然后找到数组中第二小的数,让他跟数组中第二个元素交换一下值,以此类推。
稳定性
选择排序是给每个位置选择当前元素最小的,比如给第一个位置选择最小的,在剩余元素里面给第二个元素选择第二小的,依次类推,直到第n-1个元素,第n个元素不用选择了,因为只剩下它一个最大的元素了。那么,在一趟选择,如果一个元素比当前元素小,而该小的元素又出现在一个和当前元素相等的元素后面,那么交换后稳定性就被破坏了。比较拗口,举个例子,序列5 8 5 2 9,我们知道第一遍选择第1个元素5会和2交换,那么原序列中两个5的相对前后顺序就被破坏了,所以选择排序是一个不稳定的排序算法。(被换掉的元素如果是相等的元素,则不稳定)
代码
typedef int ElemType;
void SelectSort(ElemType A[],int n)
{
ElemType temp;
for(int i=1;i<n;++i) //i表示第i个需要挑选的数
{
int k=i-1; //k表示最小的数
for(int j=i;j<n;++j) //j表示需要遍历比较的数
{
if(A[j]<A[k])
{
k=j;
}
}
if(k!=i-1)
{
temp=A[i-1];
A[i-1]=A[k];
A[k]=temp;
}
}
}
4.2 树形选择排序(锦标赛排序)
https://blog.csdn.net/qq_16234613/article/details/52675953
树形选择排序又称锦标赛排序(Tournament Sort),是一种按照锦标赛的思想进行选择排序的方法。首先对n个记录的关键字进行两两比较,然后在n/2个较小者之间再进行两两比较,如此重复,直至选出最小的记录为止。
然而虽然树形选择比较能够减少比较次数,却增加了辅助空间的使用。算法复杂度为 n*logn
实际算法中,我们把需要比较的记录全部作为叶子,然后从叶子开始两两比较,从底向上最后形成一棵完全二叉树。在我们选择出最小关键字后,根据关系的传递,只需要将最小关键字的叶子节点改成无穷大,重新从底到上比较一次就能够得出次小关键字。
五、堆排序
堆(英语:heap)是计算机科学中一类特殊的数据结构的统称。堆通常是一个可以被看做一棵树的数组对象。若将和此次序列对应的一维数组(即以一维数组作此序列的存储结构)看成是一个完全二叉树,则堆的含义表明,完全二叉树中所有非终端结点的值均不大于(或不小于)其左、右孩子结点的值。由此,若序列{k1,k2,…,kn}是堆,则堆顶元素(或完全二叉树的根)必为序列中n个元素的最小值(或最大值)。
树形选择排序方法尚有辅助存储空间较多、和“最大值”进行多余比较等缺点。为了弥补,威洛姆斯(J. willioms)在1964年提出了另一种形式的选择排序——堆排序。
https://www.cnblogs.com/chengxiao/p/6129630.html
堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏,最好,平均时间复杂度均为O(nlogn),它也是不稳定排序。首先简单了解下堆结构。
堆排序运用完全二叉树的性质,将二叉树的节点地址编号为地址,不用构建带有指针的二叉树,只用数组即可实现二叉树。
为什么不稳定排序:举一个反例即可,两个叶子节点上的数相同,但是叶子节点有可能进行swap,也有可能不进行swap,就可能无法保持稳定。
5.1 大顶堆与小顶堆
排序中的堆与队列和程序中的堆与栈不一样,注意区分。
堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。如下图:
注意只需要父节点大于子节点,并不需要子节点之间进行排序。
5.2 基本思想与步骤
堆排序的基本思想是:将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了
再简单总结下堆排序的基本思路:
a.将无需序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆;
b.将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;
c.重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。
步骤一、构造初始堆(大顶堆)
升序或者降序如何选择大顶堆或者小顶堆?
将给定无序序列构造成一个大顶堆(一般升序采用大顶堆,降序采用小顶堆)。容易理解,大顶堆在交换之后最后的元素最大,小顶堆在交换之后最后的元素最小。
a.假设给定无序序列结构如下
调整是自下而上还是自上而下?
自下而上,自后向前。
此时我们从最后一个非叶子结点开始(叶结点自然不用调整,第一个非叶子结点 arr.length/2-1=5/2-1=1,也就是下面的6结点),从左至右,从下至上进行调整。(注意关于堆的性质,有一个性质是,第一个非叶子节点的编号=length/2 -1)
找到第二个非叶节点4,由于[4,9,8]中9元素最大,4和9交换。(堆之中是三个数字进行比较)
调整后是否需要继续调整?需要
这时,交换导致了子根[4,5,6]结构混乱,继续调整,[4,5,6]中6最大,交换4和6。
此时,我们就将一个无需序列构造成了一个大顶堆。
步骤二、顶端与末尾交换
顶端与末尾元素交换之后,是否在迭代进行步骤一?
并不是,步骤一是构造大顶堆的过程,涉及到反复的迭代,但是构造成大顶堆之后,步骤二的调整只需要从顶到底的构造当前节点与下面节点保持大小关系即可。
将堆顶元素与末尾元素进行交换,使末尾元素最大。然后继续调整堆,再将堆顶元素与末尾元素交换,得到第二大元素。如此反复进行交换、重建、交换。
a.将堆顶元素9和末尾元素4进行交换
b.重新调整结构,使其继续满足堆定义
c.再将堆顶元素8与末尾元素5进行交换,得到第二大元素8.
后续过程,继续进行调整,交换,如此反复进行,最终使得整个序列有序
5.3 代码
https://www.cnblogs.com/AlgrithmsRookie/p/5896603.html
创建堆
此为创建大顶堆的代码,这种遍历是自底向顶的遍历
void make_heap(int *a, int len)
{
for(int i = (len-1)/2; i >= 0; --i) //遍历每个 非叶子节点
adjust_heap(a, i, len);//不用考虑那么多, 用面向对象的思乡去考虑,
} //这个函数的作用就是用来使当前节点的子树符合堆的规律
调整当前非叶子节点
当前非叶子节点构建大顶堆,
void adjust_heap(int* a, int node, int size)
{
int left = 2*node + 1;
int right = 2*node + 2;
int max = node;
if( left < size && a[left] > a[max])
max = left;
if( right < size && a[right] > a[max])
max = right;
if(max != node)
{
swap( a[max], a[node]); //交换节点
adjust_heap(a, max, size); //递归
}
}
最后的递归表示,如果node与max的值交换之后,新的a[max]可能比它的两个儿子小,即管不住它的两个儿子,因此需要继续递归。
同时,考虑到如果node为叶子节点,则left和right<size这个判断就能保证了。
完整代码
#include <iostream>
using namespace std;
//调整当前节点以及递归调整以下节点
void adjust_heap(int* heap, int node_loc, int size){
int left_loc = 2 * node_loc + 1;
int right_loc = 2 * node_loc + 2;
//找出左右子树中最大的,定义为max
int max_loc = node_loc;
if (left_loc<size && heap[left_loc]>heap[max_loc]){
max_loc = left_loc;
}
if (right_loc<size && heap[right_loc]>heap[max_loc]){
max_loc = right_loc;
}
//如果左右子树有比根大(不是大顶堆),所以调整堆值,并且往下递归(需要子堆也为大顶堆)
if (max_loc != node_loc){
swap(heap[max_loc], heap[node_loc]);
adjust_heap(heap, max_loc, size);
}
}
//堆排序算法
void heap_sort(int* heap,int size){
//步骤一,构造大顶堆,自下而上调整非叶子节点,算法复杂度 n/2*log(n)
for (int idx = size / 2 - 1; idx >= 0; idx--){
adjust_heap(heap,idx,size);
}
//重复步骤二三,即最大元素调整到最后,然后剩下的部分继续构造大顶堆,算法复杂度 n*log(n)
for (int size_idx = size; size_idx > 1; size_idx--){
swap(heap[0], heap[size_idx - 1]);
adjust_heap(heap, 0, size_idx - 1);
}
}
int main()
{
//堆排序是不稳定排序,比如此过程中的2,排在前面的可能被调到最后
int a[10] = { 3, 2, 7, 4, 2, -999, -21, 99, 0, 9 };
int len = sizeof(a) / sizeof(int);
for (int i = 0; i < len; ++i)
cout << a[i] << ' ';
cout << endl;
heap_sort(a, len);
for (int i = 0; i < len; ++i)
cout << a[i] << ' ';
cout << endl;
int end; cin >> end;
return 0;
}
六、排序稳定性与复杂度归纳
https://www.jianshu.com/p/abe27f16b7b5
https://www.cnblogs.com/codingmylife/archive/2012/10/21/2732980.html
6.1 定义
通俗地讲就是能保证排序前2个相等的数其在序列的前后位置顺序和排序后它们两个的前后位置顺序相同。在简单形式化一下,如果Ai = Aj,Ai原来在位置前,排序后Ai还是要在Aj位置前。
由上面的定义可知道稳定性排序保证了排序前两个相等的数其在序列的前后位置顺序和排序后它们两个的前后位置顺序相同
。
选择排序、快速排序、希尔排序、堆排序不是稳定的排序算法,
冒泡排序、插入排序、归并排序和基数排序是稳定的排序算法
6.2 选择排序
不稳定
选择排序是给每个位置选择当前元素最小的,比如给第一个位置选择最小的,在剩余元素里面给第二个元素选择第二小的,依次类推,直到第n - 1个元素,第n个元素不用选择了,因为只剩下它一个最大的元素了。那么,在一趟选择,如果当前元素比一个元素小,而该小的元素又出现在一个和当前元素相等的元素后面,那么交换后稳定性就被破坏了。
举个例子,序列5 8 5 2 9,我们知道第一遍选择第1个元素5会和2交换,那么原序列中2个5的相对前后顺序就被破坏了,所以选择排序不稳定。
6.3 冒泡排序
冒泡排序就是把小的元素往前调或者把大的元素往后调。比较是相邻的两个元素比较,交换也发生在这两个元素之间。所以,如果两个元素相等,我想你是不会再无聊地把他们俩交换一下的;如果两个相等的元素没有相邻,那么即使通过前面的两两交换把两个相邻起来,这时候也不会交换,所以相同元素的前后顺序并没有改变,所以冒泡排序是一种稳定排序算法。
6.4 插入排序
插入排序是在一个已经有序的小序列的基础上,一次插入一个元素。当然,刚开始这个有序的小序列只有1个元素,就是第一个元素。比较是从有序序列的末尾开始,也就是想要插入的元素和已经有序的最大者开始比起,如果比它大则直接插入在其后面,否则一直往前找直到找到它该插入的位置。如果碰见一个和插入元素相等的,那么插入元素把想插入的元素放在相等元素的后面。所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,所以插入排序是稳定的。
简单排序的 时间复杂度为O(n^2),辅助存储O(1)
6.5 快速排序
快速排序有两个方向,左边的i下标一直往右走,当a[i] <= a[center_index],其中center_index是中枢元素的数组下标,一般取为数组第0个元素。而右边的j下标一直往左走,当a[j] > a[center_index]。如果i和j都走不动了,i <= j,交换a[i]和a[j],重复上面的过程,直到i > j。 交换a[j]和a[center_index],完成一趟快速排序。在中枢元素和a[j]交换的时候,很有可能把前面的元素的稳定性打乱.
比如序列为5 3 3 4 3 8 9 10 11,现在中枢元素5和3(第5个元素,下标从1开始计)交换就会把元素3的稳定性打乱,所以快速排序是一个不稳定的排序算法,不稳定发生在中枢元素和a[j] 交换的时刻。
时间复杂度方面,平均时间为O[n*log(n)],最坏情况为O(n^2),
平均时间为n看作每个数值都进行了比较,log(n)是反复递归调用的次数。
最坏情况为:每个元素进行比较的时候,都排在了剩下位置的最左或者最右。结果剩下的n-1的元素也需要进行调用,时间为O(n^2)
涉及递归,需要调用函数栈,所以辅助存储为O(log(n))
6.6 归并排序
归并排序是把序列递归地分成短序列,递归出口是短序列只有1个元素(认为直接有序)或者2个序列(1次比较和交换),然后把各个有序的段序列合并成一个有序的长序列,不断合并直到原序列全部排好序。可以发现,在1个或2个元素时,1个元素不会交换,2个元素如果大小相等也没有人故意交换,这不会破坏稳定性。那么,在短的有序序列合并的过程中,稳定是是否受到破坏?没有,合并过程中我们可以保证如果两个当前元素相等时,我们把处在前面的序列的元素保存在结果序列的前面,这样就保证了稳定性。所以,归并排序也是稳定的排序算法。
为什么归并排序稳定?
因为两个序列,两两比较,相同的值左边还在左边。比如1,2,1,3,第一次归并为12,13,然后归并为1123,左边还在左边,右边还在右边。
时间复杂度?
平均时间和最坏情况均为O(n*log(n)),每趟排序中n个元素均被比较,所以有因数n,另外,每次排序必会形成2^n长度的序列。
6.7 基数排序
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序,最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以其是稳定的排序算法。
时间复杂度O(d*(n+rd)), 最坏情况O(d*(n+rd)),辅助存储O(rd)
6.8 希尔排序
希尔排序是按照不同步长对元素进行插入排序,当刚开始元素很无序的时候,步长最大,所以插入排序的元素个数很少,速度很快;当元素基本有序了,步长很小, 插入排序对于有序的序列效率很高。所以,希尔排序的时间复杂度会比O(n^2)好一些。由于多次插入排序,我们知道一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以shell排序是不稳定的。
6.9 堆排序
我们知道堆的结构是节点i的孩子为2 * i和2 * i + 1节点,大顶堆要求父节点大于等于其2个子节点,小顶堆要求父节点小于等于其2个子节点。在一个长为n 的序列,堆排序的过程是从第n / 2开始和其子节点共3个值选择最大(大顶堆)或者最小(小顶堆),这3个元素之间的选择当然不会破坏稳定性。但当为n / 2 - 1, n / 2 - 2, ... 1这些个父节点选择元素时,就会破坏稳定性。有可能第n / 2个父节点交换把后面一个元素交换过去了,而第n / 2 - 1个父节点把后面一个相同的元素没 有交换,那么这2个相同的元素之间的稳定性就被破坏了。所以,堆排序不是稳定的排序算法。
//堆排序算法
void heap_sort(int* heap,int size){
//步骤一,构造大顶堆,自下而上调整非叶子节点,算法复杂度 n/2*log(n)
for (int idx = size / 2 - 1; idx >= 0; idx--){
adjust_heap(heap,idx,size);
}
//重复步骤二三,即最大元素调整到最后,然后剩下的部分继续构造大顶堆,算法复杂度 n*log(n)
for (int size_idx = size; size_idx > 1; size_idx--){
swap(heap[0], heap[size_idx - 1]);
adjust_heap(heap, 0, size_idx - 1);
}
}
堆排序最好最坏的情况时间复杂度均为O(n*log(n)),空间复杂度为多一个节点用于swap,辅助空间O(1)
七、归并排序
https://baike.baidu.com/item/%E5%BD%92%E5%B9%B6%E6%8E%92%E5%BA%8F/1639015?fr=aladdin
https://blog.csdn.net/k_koris/article/details/80508543
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
时间复杂度 O(n log n) ,需要和带排记录等数量的辅助空间。
7.1 过程与原理
https://www.jianshu.com/p/33cffa1ce613
归并排序的核心思想是将两个有序的数列合并成一个大的有序的序列。通过递归,层层合并,即为归并。
7.3 代码
#include <stdlib.h>
#include <stdio.h>
void Merge(int sourceArr[],int tempArr[], int startIndex, int midIndex, int endIndex)
{
int i = startIndex, j=midIndex+1, k = startIndex;
while(i!=midIndex+1 && j!=endIndex+1)
{
if(sourceArr[i] > sourceArr[j])
tempArr[k++] = sourceArr[j++];
else
tempArr[k++] = sourceArr[i++];
}
while(i != midIndex+1)
tempArr[k++] = sourceArr[i++];
while(j != endIndex+1)
tempArr[k++] = sourceArr[j++];
for(i=startIndex; i<=endIndex; i++)
sourceArr[i] = tempArr[i];
}
//内部使用递归
void MergeSort(int sourceArr[], int tempArr[], int startIndex, int endIndex)
{
int midIndex;
if(startIndex < endIndex)
{
midIndex = startIndex + (endIndex-startIndex) / 2;//避免溢出int
MergeSort(sourceArr, tempArr, startIndex, midIndex);
MergeSort(sourceArr, tempArr, midIndex+1, endIndex);
Merge(sourceArr, tempArr, startIndex, midIndex, endIndex);
}
}
int main(int argc, char * argv[])
{
int a[8] = {50, 10, 20, 30, 70, 40, 80, 60};
int i, b[8];
MergeSort(a, b, 0, 7);
for(i=0; i<8; i++)
printf("%d ", a[i]);
printf("\n");
return 0;
}
八、二分查找法
设查找表中有100个元素,如果用二分法查找方法查找数据元素X,则最多需要比较( )次就可以断定数据元素X是否在查找表中。
正确答案: C
5,6,7,8
二分查找解析:
如果已知道表中一定存在该元素:(不适用本题,本题是问是否存在表中)
一次查找最多可以定位3个元素的表,两次查找可以定位 7个元素的表, 三次查找最多可以定位15个元素的表,四次查找最多可以定位31个元素的表,所以n次查找可以定位:2^(n+1)-1个元素的表
如果未知表中是否存在该元素:(适用与本题)
一次查找可以判断1个元素的表,2次查找可以判断3个元素的表,3次查找可以判断7个元素的表。故n次查找可以判断 2^n个元素的表。
对于本题而言,2^3=8, 2^6=64, 2^7=128, 100个元素介于 2^6-1 与2^7-1之间,因此最多需要查找7次。
二分查找也称折半查找(Binary Search),它是一种效率较高的查找方法。但是,折半查找要求线性表必须采用顺序存储结构,而且表中元素按关键字有序排列。