内部排序
排序是计算机内经常进行的一种操作,其目的是将一组"无序"的记录序列调整为"有序"的记录序列。分内部排序和外部排序。若整个排序过程不需要访问外存便能完成,则称此类排序问题为内部排序。反之,若参加排序的记录数量很大,整个序列的排序过程不可能在内存中完成,则称此类排序问题为外部排序。内部排序的过程是一个逐步扩大记录的有序序列长度的过程。
将杂乱无章的数据元素,通过一定的方法按关键字顺序排列的过程叫做排序。假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,ri=rj,且ri在rj之前,而在排序后的序列中,ri仍在rj之前,则称这种排序算法是稳定的;否则称为不稳定的。
排序方法合集
插入:直接插入 折半插入 希尔插入 二路插入
选择:直接选择 二路选择 堆排序 二叉树
交换: 冒泡(起冒) 鸡尾酒 快速排序
归并:二路归并
基数排序
计数排序
直接插入排序
1.简单方法
首先在当前有序区R[1…i-1]中查找R[i]的正确插入位置k(1≤k≤i-1);然后将R[k…i-1]中的记录均后移一个位置,腾出k位置上的空间插入R[i]。
注意:若R[i]的关键字大于等于R[1…i-1]中所有记录的关键字,则R[i]就是插入原位置。
2.改进的方法
一种查找比较操作和记录移动操作交替地进行的方法。具体做法:
将待插入记录R[i]的关键字从右向左依次与有序区中记录Rj的关键字进行比较:
① 若R[j]的关键字大于R[i]的关键字,则将R[j]后移一个位置;
②若R[j]的关键字小于或等于R[i]的关键字,则查找过程结束,j+1即为R[i]的插入位置。
关键字比R[i]的关键字大的记录均已后移,所以j+1的位置已经腾空,只要将R[i]直接插入此位置即可完成一趟直接插入排序。
void insertSort(int arr[],int n){
int i,j;
// i=0,只有一个元素 本身就是有序,不需要插入
//i=1 从第二个元素开始,逐一往前插入 插入之后使之保持有序
for(i=1;i<n;i++){//i=1 1+2+3+4+...+n-1 n(n-1)/2
int key = arr[i];//提前保存要插入的元素 因为前面的元素往后移,会覆盖
//[0,i-1]有序 插入key之后使 [0,i]区间有序
for(j=i-1;j>=0&&arr[j]>key;--j){
arr[j+1] = arr[j];
}
if(i!=j+1){//i==j+1
arr[j+1] = key; //arr[i] = key;
}
}
}
/*
下标为i的元素要插入到[0,i-1]区间,使插入arr[i]元素
局部 [0,i]区间保持有序
把前面的数据作为有序序列,逐次插入一个元素之后,使这保持有序
在最开始时,只有一个元素的时候,有序的
*/
折半插入
方法:
在将一个新元素插入已排好序的数组的过程中,寻找插入点时,将待插入区域的首元素设置为a[low],末元素设置为a[high],则轮比较时将待插入元素与a[m],其中m=(low+high)/2相比较,如果比参考元素小,则选择a[low]到a[m-1]为新的插入区域(即high=m-1),否则选择a[m+1]到a[high]为新的插入区域(即low=m+1),如此直至low<=high不成立,即将此位置之后所有元素后移一位,并将新元素插入a[high+1]。
void insertNum(int arr[],int len,int key){
int left,mid,right;
for(left=0,right=len-2;left<=right;){//left>right
mid = (left+right)/2;
if(key<arr[mid]){
right = mid-1;
}else{
left = mid+1;
}
}
int i;
for(i=len-2;i>right;--i){
arr[i+1] = arr[i];
}
arr[right+1] = key;
}
void showArr(int arr[],int len){
int i;
for(i=0;i<len;i++){
printf("%d ",arr[i]);
}
printf("\n");
}
/*
效率最低的情况:
原始数列: 10 9 8 7 6 5 4 3 2 1 升序
原始序列和最终的序列正好相反的情况,每次插入一个元素
都需要把该元素插入到最前面,需要把该元素之前所有的元素都往后移一次
*/
2-路插入排序
2-路插入排序算法是在折半插入排序的基础上对其进行改进,减少其在排序过程中移动记录的次数从而提高效率。利用一个与排序序列一样大小的数组作为辅助空间,设置left和right指针标记辅助数组开始位置和最后位置。遍历未排序序列,如果待插入元素比已排序序列最小的元素(left位置)小,则left位置前移,将待排序元素插入left位置;如果待插入元素比已排序序列最大的元素(right位置)大,则right位置后移,将待排序元素插入right位置;如果待插入元素比最小大,比最大小,则需要移动元素,过程类似直接插入排序,不过考虑到循环使用数组,对于下标的处理有些许不同。
void twoRoadInsert(int arr[],int n){
int *brr = (int *)malloc(sizeof(arr[0])*n);//brr
int left = -1,right = n;
int i,j;
for(i=0;i<n;i++){
if(left==-1||arr[i]>brr[0]){//大于最左端的元素
for(j=left;j>=0&&arr[i]<brr[j];--j){
brr[j+1] = brr[j];
}
brr[j+1] = arr[i];
++left;
}else{//小于brr[0] 如果要插入左边 左边所有的元素都要移动
for(j=right;j<n&&arr[i]>brr[j];++j){
brr[j-1] = brr[j];
}
brr[j-1] = arr[i];
--right;
}
}
for(i=right;i<n;i++){
arr[i-right] = brr[i];
}
for(i=0;i<=left;i++){
arr[n-right+i] = brr[i];
}
free(brr);
}
希尔排序
//时间复杂度是O(nlogn) 希尔排序 O(n^1.3) 空间复杂度O(1) 不稳定
void shellSort(int arr[],int n){
//分组 步长 分为step小组 组内元素步长step
int step,i,j;
for(step = n/2;step>0;step=step/2){
//组内排序 [0,step) 组内的第一个元素
for(i=step;i<n;i++){//除了组内第一个元素 其它元素都需要进行组内插入
int key = arr[i];
for(j=i-step;j>=0&&key<arr[j];j=j-step){
arr[j+step] = arr[j];//组内的元素往移
}
arr[j+step] = key;
}
}
}
二叉树排序
ps:具体思想看之前的二叉树算法
void binInsertSort(int arr[],int n){
int left,right,mid,i,j,key;
for(i=1;i<n;i++){//arr[1]----arr[n-1]所有的元素依次往前插入
key = arr[i];//保存要插入的元素 arr[i] arr[0]--arr[i-1]
for(left=0,right=i-1;left<=right;){//找到arr[i]插入的位置 折半查找
mid = (left+right)/2;
if(key < arr[mid]){
right = mid-1;
}else{
left = mid+1;
}
}
//right+1 位置要存储arr[i] key
for(j=i-1;j>right;--j){
arr[j+1] = arr[j];
}
arr[right+1] = key;
}
}
选择排序
定义
选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理是每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。 选择排序是不稳定的排序方法(比如序列[5, 5, 3]第一次就将第一个[5]与[3]交换,导致第一个5挪动到第二个5后面)。
算法
对比数组中前一个元素跟后一个元素的大小,如果后面的元素比前面的元素小则用一个变量k来记住他的位置,接着第二次比较,前面"后一个元素"现变成了"前一个元素",继续跟他的"后一个元素"进行比较如果后面的元素比他要小则用变量k记住它在数组中的位置(下标),等到循环结束的时候,我们应该找到了最小的那个数的下标了,然后进行判断,如果这个元素的下标不是第一个元素的下标,就让第一个元素跟他交换一下值,这样就找到整个数组中最小的数了。然后找到数组中第二小的数,让他跟数组中第二个元素交换一下值,以此类推。
void choiceSort(int arr[],int n){
int i,j,max;
for(i=0;i<n-1;i++){//进行n-1次循环 每次选择一个最大值 放到指定位置
//在指定区间选择最大值
max = 0;
for(j=1;j<n-i;j++){//n-1-i [0,n-1] [0,n-2]
if(arr[max]<arr[j]){//1 8 9 7 9 2 3
max = j;
}
}
if(max != n-1-i){
int tmp = arr[max];
arr[max] = arr[n-1-i];
arr[n-1-i] = tmp;
}
}
}
void twoChoiceSort(int arr[],int n){
int i,j,max,min,tmp;
for(i=0;i<n/2;i++){\
max = min = i;
for(j=i+1;j<n-i;j++){//[i,n-1-i]
if(arr[max] < arr[j]){
max = j;
}else if(arr[min] > arr[j]){
min = j;
}
}
if(max != n-1-i){
tmp = arr[max];
arr[max] = arr[n-1-i];
arr[n-1-i] = tmp;
}
if(min == n-1-i){
min = max;
}
if(i != min){
tmp = arr[i];
arr[i] = arr[min];
arr[min] = tmp;
}
}
}
/*
二叉树可以用数组来存储(不需要用指针指向左右子树)
对于下标为i的结点,左子树下标为2*i+1,右子树下标为2*i+2
对于完全二叉树而言,完全可以用数组来存储表示
但对于非完全二叉树,内存的利用率不太高
堆排序
堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。可以利用数组的特点快速定位指定索引的元素。堆分为大根堆和小根堆,是完全二叉树。大根堆的要求是每个节点的值都不大于其父节点的值,即A[PARENT[i]] >= A[i]。在数组的非降序排序中,需要使用的就是大根堆,因为根据大根堆的要求可知,最大的值一定在堆顶。
//index要调整的结点 n是总共结点数
void reHeap(int arr[],int index,int n){
int child = 2*index+1;
int key = arr[index];//需要调整的结点的元素
while(child<n){//叶子结点存在
if(child+1<n && arr[child] < arr[child+1]){//右孩子存在且比左孩子大
++child;
}
//child记录孩子中更大的那个结点下标
if(key<arr[child]){
arr[index] = arr[child];//把孩子的元素放在父结点处
index = child;
child = 2*index+1;
}else{
break;
}
}
arr[index] = key;
}
//时间复杂度是O(nlogn) 空间复杂度O(1) 不稳定
void heapSort(int arr[],int n){
//把完全二叉树调整成大根堆
int i;
for(i=n/2;i>=0;--i)//第一个非叶子结点 n/2 从下往上调整
reHeap(arr,i,n);
for(i=0;i<n-1;i++){//循环进行n-1次
//arr[0]和arr[n-1-i]最后一个元素交换
int tmp = arr[0];
arr[0] = arr[n-1-i];
arr[n-1-i] = tmp;
//不考虑最后一个元素的基础上,重新调整成大根堆 只需要调整0结点
reHeap(arr,0,n-1-i);
}
}
冒泡排序
算法原理
冒泡排序算法的运作如下:(从后往前)
1.比较相邻的元素。如果第一个比第二个大,就交换他们两个。
2.对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
3.针对所有的元素重复以上的步骤,除了最后一个。
4.持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
void bubbleSort(int arr[],int n){
int i,j,tmp;
for(i=0;i<n-1;i++){
int swap = false;
for(j=1;j<n-i;j++){
if(arr[j] < arr[j-1]){
tmp = arr[j];
arr[j] = arr[j-1];
arr[j-1] = tmp;
swap = true;
}
}
if(!swap){//在一次完整的冒泡过程中,没有任何两个元素交换位置 说明已经有序
break;
}
}
}
鸡尾酒排序
就是冒泡排序的变形,不过时间复杂度,空间复杂度没有变,有些鸡肋
//时间复杂度O(n^2) 空间复杂度O(1) 稳定
void cookTailSort(int arr[],int n){
int i,j,tmp;
for(i=0;i<n/2;i++){
bool swap = false;
for(j=i+1;j<n-i;j++){
if(arr[j]<arr[j-1]){
tmp = arr[j];
arr[j] = arr[j-1];
arr[j-1] = tmp;
swap = true;
}
}
for(j=j-2;j>i;j--){
if(arr[j]<arr[j-1]){
tmp = arr[j];
arr[j] = arr[j-1];
arr[j-1] = tmp;
swap = true;
}
}
if(!swap){
break;
}
}
}
归并排序
归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。归并排序是一种稳定的排序方法。
算法描述
归并操作的工作原理如下:
第一步:申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
第二步:设定两个指针,最初位置分别为两个已经排序序列的起始位置
第三步:比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
重复步骤3直到某一指针超出序列尾
将另一序列剩下的所有元素直接复制到合并序列尾
以下给出两种归并的方法
void megerArr(int arr[],int n){
int mid = n/2; //[0,mid) [mid,n-1]有序
int brr[mid]; //brr 保存前半部分数据
int i,j,k;
for(i=0;i<mid;i++)
brr[i] = arr[i];
i=0;j=mid;k=0;
while(i<mid&&j<n){//i brr[0]-brr[mid-1] j arr[mid]--arr[n-1]
if(brr[i]<arr[j]){
arr[k++] = brr[i++];
}else{
arr[k++] = arr[j++];
}
}
while(i<mid){
arr[k++] = brr[i++];
}
//如果是j<n 没有必要再赋值一次 while(j<n){arr[k++] = arr[j++];}
}
//时间复杂度O(nlogn) 空间复杂度O(nlogn)
void megerSort(int arr[],int n){
//megerSorts(arr,0,n-1);
if(n<=1)
return;
int mid = n/2; //n=2 n==1 [0,1) [1,1] 3/2 = 1
megerSort(arr,n/2);//对前半部分进行排序
megerSort(arr+n/2,n-n/2);
megerArr(arr,n);//[0,n/2) 升序 [n/2,n-1]升序 合并成有序
}
void megerArrs(int arr[],int left,int right){
int mid = (left+right)/2;
//[left,mid] [mid+1,right]
int llen = mid-left+1;
int rlen = right-mid;
int brr[llen];
int i,j,k;
for(i=0;i<llen;i++)
brr[i] = arr[left+i];
i=0;j=mid+1;k=left;
while(i<llen && j<=right){
if(brr[i]<arr[j]){
arr[k++] = brr[i++];
}else{
arr[k++] = arr[j++];
}
}
while(i<llen){
arr[k++] = brr[i++];
}
}
void megerSorts(int arr[],int left,int right){//[left,right]
if(left>=right)
return;
int mid = (left+right)/2;//[left,mid] [mid+1,right]
if(mid-left>0)//左边大于1个元素
megerSorts(arr,left,mid);
if(right-mid-1>0)//右边大于1个元素
megerSorts(arr,mid+1,right);
megerArrs(arr,left,right);
}
快速排序
它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
也给出两种快速排序的方法
void quickSort(int arr[],int n){//快速排序
if(n<=1) return;
int key = arr[0];
int i,j;
for(i=0,j=n-1;i<j;){
while(i<j && arr[j]>=key) --j;//从右边找到一个小于key的值 放到i
if(i<j) arr[i++] = arr[j];
while(i<j && arr[i]<=key) ++i;//从在边找到一个大于key的值 放到j
if(i<j) arr[j--] = arr[i];
}
arr[i] = key;
if(i>1)
quickSort(arr,i);//[0,i-1] i
if(n-i-1>1) // [i+1,n-1] n-1-i
quickSort(arr+i+1,n-i-1);
}
void quickSorts(int arr[],int left,int right){
if(left>=right)
return;
int key = arr[left];
int i=left,j=right;
while(i<j){
while(i<j && arr[j]>=key) --j;
if(i<j) arr[i++] = arr[j];
while(i<j && arr[i]<=key) ++i;
if(i<j) arr[j--] = arr[i];
}
arr[i] = key;
quickSorts(arr,left,i-1);//if(i-left>1)
quickSorts(arr,i+1,right);//if(right-i>1)
}
基数排序
基数排序(radix sort)属于"分配式排序"(distribution sort),又称"桶子法"(bucket sort)或bin sort,顾名思义,它是透过键值的部份资讯,将要排序的元素分配至某些"桶"中,藉以达到排序的作用,基数排序法是属于稳定性的排序,其时间复杂度为O (nlog®m),其中r为所采取的基数,而m为堆数,在某些时候,基数排序法的效率高于其它的稳定性排序法。
以LSD为例,假设原来有一串数值如下所示:
73, 22, 93, 43, 55, 14, 28, 65, 39, 81
首先根据个位数的数值,在走访数值时将它们分配至编号0到9的桶子中:
0
1 81
2 22
3 73 93 43
4 14
5 55 65
6
7
8 28
9 39
折叠第二步
接下来将这些桶子中的数值重新串接起来,成为以下的数列:
81, 22, 73, 93, 43, 14, 55, 65, 28, 39
接着再进行一次分配,这次是根据十位数来分配:
0
1 14
2 22 28
3 39
4 43
5 55
6 65
7 73
8 81
9 93
折叠第三步
接下来将这些桶子中的数值重新串接起来,成为以下的数列:
14, 22, 28, 39, 43, 55, 65, 73, 81, 93
这时候整个数列已经排序完毕;如果排序的对象有三位数以上,则持续进行以上的动作直至最高位数为止。
LSD的基数排序适用于位数小的数列,如果位数多的话,使用MSD的效率会比较好。MSD的方式与LSD相反,是由高位数为基底开始进行分配,但在分配之后并不马上合并回一个数组中,而是在每个"桶子"中建立"子桶",将每个桶子中的数值按照下一数位的值分配到"子桶"中。在进行完最低位数的分配后再合并回单一的数组中。
//空间复杂度O(r+n)
void baseSort(int arr[],int n){
SLinketList list[10] = {NULL};
int i,j;
for(i=0;i<10;i++)
list[i] = slinket_list_create();
int max = 0;
for(i=1;i<n;i++){
if(arr[max]<arr[i])
max = i;
}
int key = arr[max];//最大值
int div = 1;
for(;key/div!=0;div=10*div){
for(i=0;i<n;i++){
int w = arr[i]/div%10;
slinket_list_insert_back(list[w],arr[i]);
}
for(i=0,j=0;i<10;i++){
while(!slinket_list_is_empty(list[i])){
slinket_list_delete_front(list[i],&arr[j]);
++j;
}
}
}
for(i=0;i<10;i++){
slinket_list_destroy(list[i]);
}
}
计数排序
计数排序是一个非基于比较的排序算法,该算法于1954年由 Harold H. Seward 提出。它的优势在于在对一定范围内的整数排序时,它的复杂度为Ο(n+k)(其中k是整数的范围),快于任何比较排序算法。 当然这是一种牺牲空间换取时间的做法,而且当O(k)>O(nlog(n))的时候其效率反而不如基于比较的排序(基于比较的排序的时间复杂度在理论上的下限是O(nlog(n))
算法分析:
计数排序对输入的数据有附加的限制条件:
1、输入的线性表的元素属于有限偏序集S;
2、设输入的线性表的长度为n,|S|=k(表示集合S中元素的总数目为k),则k=O(n)。
在这两个条件下,计数排序的复杂性为O(n)。
计数排序的基本思想是对于给定的输入序列中的每一个元素x,确定该序列中值小于x的元素的个数(此处并非比较各元素的大小,而是通过对元素值的计数和计数值的累加来确定)。一旦有了这个信息,就可以将x直接存放到最终的输出序列的正确位置上。
时间复杂度为O(n)的排序,最快的内部排序
//时间复杂度O(max-min+n) 空间复杂度O(max-min+1)
int countSort(int arr[],int n){
int min = arr[0],max = arr[0];
int i,j;
for(i=0;i<n;i++){
if(max<arr[i]){
max = arr[i];
}else if(min>arr[i]){
min = arr[i];
}
}
int count = max-min+1;
int *cuts = calloc(count,sizeof(int));//自动清0
for(i=0;i<n;i++){
cuts[arr[i]-min]++;
}
for(i=0,j=0;i<count;i++){
while(cuts[i]>0){
arr[j++] = i+min;
--cuts[i];
}
}
free(cuts);
}