排序
概述
冒泡排序 Bubble Sort
有长度为n的数组
数据相邻比较,一轮循环后结果极值在最后,因此再进行n-1次循环,每次完成极值位置排序。
第一次从[0] 到 [n-1],第二次从[1]到[n-2]…因此有
for(int i=0;i<length;i++){
//j从1开始防止越界
for(int j=1;j<length-i;j++){
//进行比较
swap(str[j], &str[j-1]);
}
}
稳定性
相对位置不会变化,属于稳定的排序。
优化
- 原本的数列已经完成排序,可以提前终止
数组有序,则表示每次比较都没有交换数据,也就是IF的条件没有成立,因此可以进行标记
如何标记?
排序完毕有两种情况:一种是已经完全排序,另一种是进行某次循环后有序,因此,在每次第一层for循环进行一次标记,即假定每次开始循环前有序,若进行比较则无序。每次内层for循环结束后进行判断。
for(int i=0;i<length;i++){
//j从1开始防止越界
bool =true;
for(int j=1;j<length-i;j++){
//进行比较
swap(str[j], &str[j-1]);
bool =false;
}
if (bool) break;
}
如果不会达到提前完成排序的结果,则会多出bool判断的操作。
- 数列尾部已经完成排序,可以记录最后一次排序的位置,减少次数
如果数组的后半部分有序,则可以在排序的时候省略对他们的排序,减少运算
如何标记?
通过IF判断可以标记,如果IF成立,则表示进行交换,可以设置SortIndex记录交换位置,每次排序后,下次进行的循环到SortIndex为止。
如何设置标记值的初始值?
因为最后标记值设置成end值,那么如果数列完全有序,则一次循环后结束,因此标记值可以设置成1,以达到停止排序的结果。
for(end = array.length; end > 1;){
SortIndex =1; //每次内层循环前设置
for( begin=1;begin<end;begin++){
//进行判断
if(array[begin]>array[begin-1]){
temp=array[begin];
array[begin]=array[begin-1];
array[begin-1]=temp;
SortIndex=begin;
}
}
//下次进行循环则到end为止。
end = SortIndex;
}
注意
使用标记值时,可以去去除for循环内end–的操作,初始Index设置为1,逻辑上表示若不排序则直接停止循环。
时间复杂度为o(N²)
空间复杂度为o(1)
选择排序 Selection Sort
从[0]到[n-1]依次选择数据,与剩下的每个数据进行比较,若满足判断条件则交换。
for(int i=0;i<length-1;i++){
for(int j=i+1;j<length;j++){
if(str[i]>str[j]){
temp=str[i];
str[i]=str[j];
str[j]=temp;
}
}
}
时间复杂度为o(N²)
空间复杂度为o(1)
稳定性
例如[5,8,5,2,9],第一次交换,第一个5和2交换,两个5的相对位置变化,属于不稳定的排序。
堆排序 Heap Sort
堆排序是对于选择排序的优化:将每次选择最值的操作交给堆操作,可以提高效率
步骤
1.原地建堆
2.重复下述操作,直到堆的元素为1:
a. 进行移项(交换堆顶和堆尾操作),堆Size-1;
b. 对0号位置进行siftdown操作
1.建堆:
a.先建立完全二叉树
b.从最后一个非叶节点进行调整:
与左右结点比较:若左右节点大,则与之交换,并考察该节点为根的子树是否满足要求,不满足则递归处理。
int heap_size;
//交换
void swap(int *a,int *b){
int temp = *a;
*a = *b;
*b = temp;
return;
}
//返回左子树索引
int left(int index){
return ((index<<1)+1);
}
//返回右子树索引
int right(int index){
return ((index<<1)+2);
}
//建立堆
void build_heap(int array[],int length){
heap_size = length;
//非叶节点的遍历
for(int i = ((heap_size-1)>>1);i>=0;i--){
max_heap_adjust(array,i);
}
}
//每个非叶节点的处理
void max_heap_adjust(int array[],int index){
int largest=index; //设置初始最大节点为根节点
int right_index = right(index);
int left_index = left(index);
//进行比较(限制节点不能超过length)
if(left_index < heap_size && array[left_index] > array[largest]){
largest=left_index;
}
if(right_index < heap_size && array[right_index] > array[largest]){
largest=right_index;
}
//判断是否交换
if(largest == index){
return;
}
else{
swap(array[index], array[largest]);
//进行递归,判断交换后的子节点是否满足条件。
max_heap_adjust(array, largest);
}
}
2.排序
heap_sort(){
void heap_sort(int array[] , int length){
int old_heap_size = length;
for(int i=heap_size;i>1;i--){
swap( &array[0], &array[i-1]);
//每次进行调换位置后,需要将堆元素-1
heap_size--;
max_heap_adjust(array,0);
}
heap_size = old_heap_size;
}
}
进行堆排序时间复杂度为o(nlog(n))
堆排序不是稳定的排序算法。
插入排序 Insertion Sort
插入排序类似于扑克牌整理顺序的方式:每抓到一张牌,将其放入相应位置。
冒泡插入
从下标为1开始排序,每次都和相邻的元素进行比较完成排序
void insertsort(int array[], int length){
//从1开始
for(int begin = 1;begin < length;begin ++){
int cur = begin; //记录当前begin
while(cur > 0 && array[cur] > array[cur-1]){
swap( &array[cur], &array[cur-1] );
cur--;
}
}
}
该种方法的时间复杂度和数列的逆序对数量成正比,最好为o(N),最坏情况为o(N²)
挪动插入
1.备份待插入元素
2.将头部有序的数和待插元素比较,如果比该值要大(小)则均往前挪一位。之后再将待插元素插入。
void insertsort(int array[], int length){
//从1开始
for(int begin=1;begin<length;begin++){
//记录当前begin
int cur = begin;
//备份数据
int v = array[cur];
//如果当前值比该值大,则该值往前一位
while(cur > 0 && v > array[cur-1]){
array[cur]=array[cur-1];
cur--;
}
array[cur] = v;
}
}
优化
二分法搜索插入
通过二分法搜索插入,减少寻找位置的次数,达到提高效率
将每次要处理的头部已经排好序的数列进行二分搜索,找到应该插入的位置并返回。
int binarySearch(int array[],int index){
int begin = 0;
int end = index;
while(begin < end){
int mid = (begin + end) >> 1;
if(array [mid] > array[index]){
end = mid;
}
else{
begin = mid + 1;
}
}
return begin;
}
void insertsort(int array[], int length){
for(int begin = 1;begin < length; begin++){
int v = array[begin];
int insertindex = binarySearch(array,begin);
//将 [insertindex , begin ]内的元素往后挪一位
for(int i = begin ; i > insertindex; i--){
array[i]=array[i-1];
}
//将待插入元素插入
array[insertindex] = v;
}
}
优化后的时间复杂度依然是o(N²)(减少的仅仅是寻找的次数,挪动的次数依旧没有变化)
归并排序 Merge Sort
1.Divide 将数组不断分成2个子序列,直到每个序列只剩下一个元素
2.Merge 将分好的子序列不断合并
Divide
每次分开,需要进行递归调用,不断的拆分直到子序列只有一个元素。
void MergeSort(int array[],int length,int begin,int end){
//如果只有一个元素
if(end - begin < 2) return;
//进行divide
int mid = (begin + end) >> 1;
MergeSort(array, mid-begin, begin, mid);
MergeSort(array, end-mid, mid, end);
Merge(array, begin, mid, end);
}
Merge
为了将分开的2个序列进行排序,并且完成的序列在原数组中,为了保证尽量少用空间,我们只需要为了左数组再额外建立空间进行排序。
如何合并
对每个数组设置索引:
左数组 Li = 0 ; Le = mid; (左数组在新空间内)
右数组 Ri = mid ; Re = length = end (右数组在原数组内)
原数组 Ai = 0
由于子序列已经排好序,因此每次比较索引点位置数据即可。
特殊情况
若左数组已经全部插入,由于右数组已经在原数组中,因此排序可以提前结束。
若右数组已经全部插入,则将左数组剩余的内容直接插入原数组。
void Merge(int array[],int begin, int mid, int end){
int li = 0, le = mid - begin;
int ri = mid, re = end;
int ai = begin;
//备份左数组:
for(int i = li; i < le; i++)
leftarray[i] = array[begin + i];
//根据左数组进行判断
while(li < le){
if(ri < re && leftarray[li] > array[ri] ){
array[ai++] = array[ri++];
}
else{
array[ai++] = leftarray[li++];
}
}
}
归并排序属于稳定的排序,时间复杂度为O(n log n)
快速排序 Quick Sort
1.从序列中选择一个轴点元素pivot(一般选择0位置元素)
2.利用pivot分割序列(比pivot大的放右边,小的放左边)
3.对子序列不断重复 1,2操作,直到子序列只有一个元素
本质:逐渐对每个元素转换成轴点元素
确定轴点
开始时,对轴点元素进行备份,从end位置开始从右往左扫描:
若end>pivot ,则不需要挪动,标记值end–;
若end<=pivot,则将array [end] 覆盖begin位置(pivot所在位置),同时begin++,从begin处开始从左往右扫描。
当begin = end 时,将pivot覆盖array [begin] 值,构造结束。
如何设置“掉头”?
可以设置2个while循环,在循环内执行到else语句时(即需要转移值时)break
递归调用
确定轴点后,对左右两个子序列分别再次确定轴点,直到子序列无法拆分。
void Quicksort(int array[], int begin, int end){
if(end -begin <2) return;
int pivot = PivotIndex(array,begin,end);
//对轴点两边的2个子序列进行快速排序
Quicksort(array,begin,pivot);
Quicksort(array,pivot+1,end);
}
//返回轴点位置,同时对序列进行轴点化
int PivotIndex(int array[], int begin, int end){
//备份轴点值
int v = array[begin];
//使用的是开区间,比较要从end-1开始
end--;
while(begin < end){
//2个while控制方向
while(begin < end){
if( v < array[end]) //右边元素>轴点元素
end--;
else{
array[begin++] = array [end];
break;
}
}
while(begin < end){
if( v > array[begin]) //左边元素<轴点元素
begin++;
else{
array[end--] = array[begin];
break;
}
}
}
array[begin] = v;
return begin;
}
快速排序属于不稳定的排序。
时间复杂度
当轴点左右元素均匀时,能快速得到结果:
T(n) = 2* T(n/2) +O(n) = O(n log n)
当轴点左右元素极度不均匀,则最坏结果:
T(n) = T(n-1) +O(n) = O(n²)
为了避免出现该情况,可以选择随机的初始轴点元素:
可以在开始选取时,随机选择元素和begin元素进行交换。
希尔排序 Shell Sort
将序列看成矩阵,分为m列,生成一个步长序列,由此步长序列逐渐将m减为1.
每次排序后,逆序对数量逐渐减少,因此希尔排序的底层使用插入排序
希尔给出的序列为N/2的k次。例如20,对应的步数序列为{10,5,2,1};
生成希尔步长序列
使用list结构进行存储。
list <int> StepSequence;
list <int> ::iterator it ;
void ShellStepSequence(int length){
int step = length;
while( (step >>= 1)>0){
StepSequence.push_back(step);
}
}
固定使用希尔序列进行排序(底层使用最简单的插入排序)
void Shellsort(int array[]){
int step =array.length;
while((step = step>>1)>0){
//第几列
for(int col = 0; col <step; col++){
//对 col col+step col+2*step 进行插入排序
for(int begin = col+step; begin< length; begin += step){
int cur =begin;
while (cur > col && array[cur] < array[cur -step] ){
swap( array[cur], array[cur-step]);
cur -= step;
}
}
}
}
}
时间复杂度
使用希尔的步长序列,最坏情况为O(n²)
当前最好的序列,最坏情况是O(n的4/3次)
计数排序 Counting Sort
计数排序不是比较排序,而是用于一定范围内的整数,用空间换时间的做法进行排序
核心
统计每个整数在序列中出现的次数,并推导每个整数在有序序列中的索引。
索引代表的值为该数出现的次数。
创造一个临时计数空间,空间取决于数组的最大值。
void Countingsort(int array[], int length){
//找到最大值
int max = array[0];
for(int i=0;i<length;i++){
if(array[i] > max)
max = array[i];
}
//开辟内存空间并初始化,存储每个整数出现的次数
int *counts = new int [1 + max];
for(int i=0; i<1+max; i++){
counts[i]=0;
}
//统计每个数出现的次数
for(int i=0; i<length; i++){
counts[array[i]]++;
}
//将count内数组输出至array
int index =0 ; //设置array的下标
for(int i=0; i<max+1; i++){
//根据counts[i]的数据决定赋值几次
while(counts[i]-- >0){
array[index++] = i;
}
}
delete counts;
}
该类型的计数排序,时间复杂度为O(N),但是会有如下缺点:
1.无法对负整数实现
2.只求最大值极度浪费空间
3.不是一个稳定的排序
优化
针对上述问题进行改进:
1.由于直接找到数据对应索引,会导致无法排序负整数。因此可以将索引结构进行调整。
2.可以找到最小值和最大值,长度由他们决定(max-min+1)。同时还可以改变原数据索引结构
如原来的元素 k,对应索引 i=k-min 。
3.索引对应数据counts[i]对应的次数,改为每个次数累加上前面的次数,这样代表该元素的位置信息。
如何建立count数组?
1.确定大小为 max-min+1.
2.确定每个元素对应次数(count[i])
3.对每个次数进行累加计算。(累加产生结果为该元素的位置信息。)
如何利用counts排序?
改进后,对原数组array[] 从右到左进行遍历 (这样可以产生稳定的排序)
拿到一个元素,找到对应元素的索引值 count[i], 然后 count[i]-=1,得到在array[]数组中存放的位置。
void Countingsort(int array[],int length,int result[]){
//找到最值
int min = array[0];
int max = array[0];
for(int i = 0;i < length;i++){
if(array[i]>max)
max = array[i];
else if(array[i]<min)
min = array[i];
}
//创建counts数组
int *counts = new int[max -min+1];
for(int i=0;i<max-min+1;i++)
counts[i] = 0;
//添加数据,得到元素位置信息
for(int i =0; i<length;i++){
//得到元素出现次数
counts[array[i] -min] ++;
}
for(int i =1; i<max-min+1;i++){
//累加得到位置信息
counts[i] +=counts[i-1];
}
//对array数组从右往左遍历 ,结果存入result
for(int i =length-1;i>=0;i--){
result[--counts[array[i] -min] ]=array[i];
}
}
最好、最坏、平均时间复杂度为O(N+k);
k为整数取值范围;
属于稳定的排序。