【排序算法】
排序算法
- 1.冒泡排序
- 2.插入排序
- 3.希尔排序
- 4.选择排序
- 5.快排
- 6.归并排序
- 7、计数排序
- 8、各个排序算法比较情况
1.冒泡排序
冒泡排序:两两之间互换比较交换,比较一趟能确定一个数的位置。假设有n个数,则需要比较n-1趟,就能确定每一个数的位置,第一趟下标范围为[0,n-2],第二趟下标范围为[0,n-3],…则可以写成代码。
void BubbleSort(int* a,int n)
{for (int i = n-1; i > 0; i--){int flag = 1;for (int j = 0; j < i; j++){if (a[j] > a[j + 1]){flag = 0;int tmp = a[j];a[j] = a[j + 1];a[j + 1] = tmp;}}if (flag)break;}
}
2.插入排序
先写排好一个数字,假设[0,end]是一个有序数组,则下一个数用tmp保存,tmp =a[end+1],如果tmp<a[end],a[end]的数移到a[end+1],如果tmp>a[end],把tmp放到下标为end的位置。
由此看来,end是有序数组最后一个数的下标,tmp保存的是下一个要放的数,一个无序数组,则end先指向第一个数,tmp保存第二个数,根据上述获得这两个数的正确排序,则end = n-2,tmp = a[end-1]时,进行最后一次排序,整个数组就排好序了。
//O(N) = O(N^2)
void InsertSort(int* a,int n)
{////[0,end] end+1for (int i = 0; i < n-1; i++){int end = i;int tmp = a[end + 1];while (end >= 0){if (a[end] > tmp){a[end + 1] = a[end];end--;}else{break;}}a[end + 1] = tmp;}
}
直接插入排序的特性总结:
1. 元素集合越接近有序,直接插入排序算法的时间效率越高
2. 时间复杂度:O(N^2)
3. 空间复杂度:O(1),它是一种稳定的排序算法
4. 稳定性:稳定
3.希尔排序
下面分的间隔gap=3,分成3组。
希尔排序在插入排序的基础上升级了一点,把一个无序数据按gap(间隔)分成gap个数组,对这gap个数组分别进行插入排序,先把gap的值取大一点,把一个组小的数排到前面,大的数排到后面,gap进行不断变化取值,当gap = 1时,就是插入排序,排序完成。
先进行排第一个组排序如下。。
//[0,end]有序 tmp保存下一个数int gap = 3; //把一个数组分成3个数组for(int i = 0;i<n-gap;i+=gap){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;}
对gap组数组进行排序,并且gap取值变化,当gap = 1时,完成排序。
void ShellSort(int*a ,int n)
{int gap = n;while(gap>0){//[0,end]有序 tmp保存下一个数gap /= 2; //把一个数组分成3个数组for(int j = 0;j<gap;j++){for(int i = j;i<n-gap;i+=gap){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;}}}
}
时间复杂度:O(N) = N^(1.3)
大致分析如下,不会算!
按照gap = n,gap = gap/3来大致计算时间复杂度。
第一次分组,gap = n/3,则把n个数据排成n/3个数组,每个数组的间隔为gap,每个数组的大小为3个数据,每个数组排序最坏情况移动3次,则移动次数为:3*(n/3) = n次。
… … … … …
最后一次,gap = 1,数组经过多轮预排之后,已经非常接近有序,大致移动n次就可以使数组有序。
4.选择排序
先确定区间[begin,end],然后再这个区间找最大值,最小值,进行交换,begin++,end- -,缩小区间。
void swap(int*px,int*py)
{int tmp =*px;*px = *py;*py = tmp;
}
void SelectSort(int*a,int n)
{int begin = 0, end = n - 1;while (begin < end){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 (maxi == begin){maxi = mini;}swap(&a[end], &a[maxi]);begin++;end--;}}
5.快排
hoare版本。
思路:快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
取key永远为子序列最左边的一个数,定义left指向子序列最左边,right指向子序列最右边,先走最右边,找比key小的值,在走左边,找比keyi大的值,找到之后,两者交换,交换之后,比key小的在左边,比key大的在右边。left与right相遇则结束,然后key与相遇位置的数交换,key已经放到它应有的位置,分割区间[begin,keyi-1][keyi][keyi+1,end],进行递归遍历,放好每一个数。
具体过程如下:
通过写这个算法,明白还是提高代码组织能力,一个算法有了思路,怎么用代码实现的问题。
void QuickSort(int* a, int left,int right)
{if (left >= right)return;int begin = left;int end = right;int keyi = left;while (left < right){//先走右边,右边找比a[key]小的while (left < right && a[keyi] >= a[right]){right--;}//在走左边,左边找比a[key]大的while (left < right && a[keyi] <= a[left]){left++;}swap(&a[left], &a[right]);}swap(&a[keyi], &a[left]);keyi = left;//[begin,keyi-1]keyi[keyi+1,end]QuickSort(a, begin,keyi-1);QuickSort(a, keyi+1,end);
}
理解为啥相遇位置的值比key小?
时间复杂度分析:
时间复杂度一般情况下O(N) = N* log 2 ( N ) \log_{2}(N) log2(N)
最坏情况下:为有序,O(N) = N^2.
快排优化,快排时间复杂度高的原因是数据有序,第一个元素是最小的。
第一种方式随机数选keyi,使得第一个数不是最小的(升序)。具体如下。
int randi = rand()%(right-left+1)+left;
swap(&a[left],&a[randi]);
其实上面这种优化,我觉得还是不靠谱,你怎么能保证你随机选的keyi就不是最小值呢?
void QuickSort(int*a ,int left,int right)
{if(left>=right)return;int begin = left,end = right;//随机数选keyiint randi = rand()%(right-left+1)+left;Swap(&a[left],&a[randi]);int keyi = left;while(left<right){//先走右边,找比a[keyi]小的值while(left<right && a[keyi]<=a[right]){right--;}//在走左边,找比a[keyi]大的值while(left<right && a[keyi]>=a[left]){left++;}Swap(&a[left],&a[right]);}Swap(&a[keyi],&a[left]);keyi = left;//[begin,keyi-1] keyi [keyi+1,end]QuickSort(a,begin,keyi-1);QuickSort(a,keyi+1,end);
}
第二种优化方式三数取中放keyi位置,这个方法比上面好。定义int GetMid(int *a,int left,int right)取每个区间的最左边值和最右边值,中间值,算出大小在中间的值做keyi。
//三数取中,left,mid,right
//大小居中的值,也就是不是最大也不是最小的
int GetMidi(int* a, int left, int right)
{int midi = (left + right) / 2;if (a[left] < a[midi]){if (a[midi] < a[right]){return midi;}else if (a[left] > a[right]) //a[midi]>a[right]{return left;}else{return right;}}else //a[left]>a[midi]{if (a[midi]>a[right]){return midi;}else if (a[left] < a[right]) //a[midi]<a[right]{return left;}else{return right;}}
}void QuickSort(int*a ,int left,int right)
{if(left>=right)return;int begin = left,end = right;//随机数选keyi
// int randi = rand()%(right-left+1)+left;
// Swap(&a[left],&a[randi]);int midi = GetMidi(a,left,right);Swap(&a[left],&a[midi]);int keyi = left;while(left<right){//先走右边,找比a[keyi]小的值while(left<right && a[keyi]<=a[right]){right--;}//在走左边,找比a[keyi]大的值while(left<right && a[keyi]>=a[left]){left++;}Swap(&a[left],&a[right]);}Swap(&a[keyi],&a[left]);keyi = left;//[begin,keyi-1] keyi [keyi+1,end]QuickSort(a,begin,keyi-1);QuickSort(a,keyi+1,end);
}
第三种小区间优化,剩余10个数直接进行插入排序。这种写法递归下去之后在<10个数的区间,直接进行插入排序,不需要返回条件,程序执行完自动返回。
void QuickSort(int*a ,int left,int right)
{if(right-left+1<10){InsertSort(a+left,right-left+1);}else{int begin = left,end = right;int keyi = left;while(left<right){//先走右边,找比a[keyi]小的值while(left<right && a[keyi]<=a[right]){right--;}//在走左边,找比a[keyi]大的值while(left<right && a[keyi]>=a[left]){left++;}Swap(&a[left],&a[right]);}Swap(&a[keyi],&a[left]);keyi = left;//[begin,keyi-1] keyi [keyi+1,end]QuickSort(a,begin,keyi-1);QuickSort(a,keyi+1,end);}
}
第二种写快排的方法:前后指针法。
定义两个指针,prev指向第一个数,cur指向第二个数,如果a[cur]<key,则prev++,然后prev与cur交换,在cur++,如果a[cur]>=key,则cur++,最后cur++出界,循环结束,prev的位置就是key值的位置,至于为什么这么操作,就把key值位置找到了,我也不知道。
void QuickSort(int*a,int left,int right)
{if(left>=right)return;int prev = left;int cur = left+1;int keyi =left;while(cur<=right){if(a[cur]<a[keyi]){++prev;Swap(&a[cur],&a[prev]);++cur;}else{++cur;}}Swap(&a[keyi],&a[prev]);keyi = prev;//[left,keyi-1]keyi[keyi+1,righgt]QuickSort(a,left,keyi-1);QuickSort(a,keyi+1,right);
}
模拟递归过程,使用栈来实现快排。
模拟递归过程,写快排的关键是搞区间,区间确定了,搞一个单趟排就可以了,先把第一个区间[lefe,right]入栈,再走一个单趟排,则一个数字位置已确定,并且分割出两个区间[begin,keyi-1]keyi[keyi+1,end],两个区间在走个单趟,依次循环。
void QuickSortNonR(int*a,int left,int right)
{ST st;STInit(&st);STPush(&st,right);STPush(&st,left);while(!STEmpty(&st)){int begin = STTop(&st);STPop(&st);int end = STTop(&st);STPop(&st);//先把一趟排好int prev = begin;int cur = begin+1;int keyi =begin;while(cur<=right){if(a[cur]<a[keyi]){++prev;Swap(&a[cur],&a[prev]);++cur;}else{++cur;}}Swap(&a[keyi],&a[prev]);keyi = prev;//[begin,keyi-1] keyi [keyi+1,end]if(keyi+1<end){STPush(&st,end);STPush(&st,keyi+1);}if(begin<keyi-1){STPush(&st,keyi-1);STPush(&st,begin);}}STDestory(&st);
}
6.归并排序
基本思想:归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide andConquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。 归并排序核心步骤:
思路:归并排序是走的二叉树的后序,而快排是走的二叉树的前序,归并排序是先把左子树、右子树、遍历到底部,只剩下一个数,返回时,进行比较,进而进行归并。
void _MergeSort(int* a, int begin, int end, int* tmp)
{if (begin == end)return;//后序来搞,分区间int mid = (begin + end) / 2;//[begin,mid][mid+1,end]printf("[%d,%d]-[%d,%d]\n", begin, mid, mid + 1, end);_MergeSort(a, begin, mid, tmp);_MergeSort(a, mid + 1, end, tmp);//大小处理,两个区间处理排序int begin1 = begin, end1 = mid;int begin2 = mid + 1, end2 = end;int i = begin1;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++];}memcpy(a + begin, tmp + begin, sizeof(int) * (end - begin + 1));PrintArray(a + begin, 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);
}
时间复杂度:O(N) = N* log 2 ( N ) \log_{2}(N) log2(N)
归并排序循环实现
一点点写这个算法,慢慢来感受怎么组织程序。
怎么弄呢?递归实现归并排序,用的是后序,那么通过循环实现归并,则要从正面出发来搞这个排序。
首先把数组中的每一个数据看作是一个数组,也就是分割,gap = 1来分割,分割之后,两个数组之间进行排序,每两个一组排好序,gap = 2,再次排序。直到完成这个排序。
先写第一次分割的两个数组的排序
int begin1 = 0, end1 = begin1 + gap - 1;
int begin2 = end1 + 1, end2 = begin2 + gap - 1;
int i = begin1;
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++];
}
memcpy(a, tmp , sizeof(int) * (end2 + 1));
在写一次全部分割的排序
for (int j = 0; j < n; j += gap * 2)
{int begin1 = j, end1 = begin1 + gap - 1;int begin2 = end1 + 1, end2 = begin2 + gap - 1;int i = begin1;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++];}memcpy(a + j, tmp + j, sizeof(int) * (end2 - j + 1));
}
整体来写
void MergeSortNonR(int* a, int n)
{int* tmp = (int*)malloc(sizeof(int) * n);if (tmp == NULL){perror("malloc fail");return;}int gap = 1;while (gap < n){for (int j = 0; j < n; j += gap * 2){int begin1 = j, end1 = begin1 + gap - 1;int begin2 = end1 + 1, end2 = begin2 + gap - 1;int i = begin1;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++];}memcpy(a + j, tmp + j, sizeof(int) * (end2 - j + 1));}gap *= 2;}free(tmp);tmp = NULL;}
边界处理
//特殊情况矫正
//1.如果begin1越界,不用管,前面已经配对成对。
//2.如果end1越界,[begin1,end1),后面没有配对的另一个数组,不用归并
if(end1>=n)
break;
//3.如果begin2越界,[begin1,end1]已经排好序,不用归并,end1后面就没有归并的数组
if(begin1>=n)
break;
//4.如果end2越界,[begin2,end2)还有数据,一定要归并,只不过弄好数据范围
if(end2>=n)
end2 = n-1;
void MergeSortNonR(int*a,int n)
{int* tmp = (int*)malloc(sizeof(int)*n);if(tmp == NULL){perror("malloc fail");return ;}int gap = 1;while(gap<n){for(int j = 0;j<n;j += gap*2){int begin1 = j,end1 =begin1+gap-1;int begin2 =end1+1,end2 = begin2+gap-1;int i = begin1;//特殊情况矫正//1.如果begin1越界,不用管,前面已经配对成对。//2.如果end1越界,[begin1,end1),后面没有配对的另一个数组,不用归并if(end1>=n)break;//3.如果begin2越界,[begin1,end1]已经排好序,不用归并,end1后面就没有归并的数组if(begin1>=n)break;//4.如果end2越界,[begin2,end2)还有数据,一定要归并,只不过弄好数据范围if(end2>=n)end2 = n-1;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++];}memcpy(a+j,tmp+j,sizeof(int)*(end2-j+1));}gap *= 2;}free(tmp);tmp = NULL;}
7、计数排序
算法思想:用相对映射来搞
//时间复杂度:O(N+range)
//空间复杂度:O(range)
void CountSort(int* a, int n)
{int max = a[0], min = a[0];for (int i = 0; i < n; i++){if (a[i] > max)max = a[i];if (a[i] < min)min = a[i];}int range = max - min + 1;int* count = (int*)malloc(sizeof(int) * (range));if (count == NULL){perror("malloc fail");return;}memset(count, 0, sizeof(int) * (range));//统计次数,相对映射for (int i = 0; i < n; i++){count[a[i] - min]++;}//排序int j = 0;for (int i = 0; i <(range); i++){while(count[i] > 0){a[j++] = min + i;count[i]--;}}free(count);count = NULL;
}
8、各个排序算法比较情况
完结!