常见的排序算法包括:直接插入排序 、希尔排序、 选择排序 、堆排 、快排 、归并排序;
本篇文章将围绕着各种算法的思想、实现以及时间复杂度进行说明。(均已升序为例)
一、直接插入排序
思想:
就是将一个数,往一段有序区间内插入,通过调整使得插入之后的区间有序;
代码实现
//直接插入排序 时间复杂度 O(N^2) 最好 O(N) 本身已经有序 ;最坏O(N^2) 逆序
void InsertSort(int* a,size_t n)
{
for(int i = 0 ;i < n - 1; i++)
{
int end = i ;
int tmp = a[end+1];
while(end >= 0 && tmp < a[end]) // 注意end <= 0 ;和第一个数也要进相比较
{
a[end + 1] = a[end];
--end;
}
a[end+1] = tmp; //end < 0 或者本身有序
}
}
时间复杂度: O(N^2)
最好的情况下(顺序)时间复杂度为O(N) ;
最坏的情况下(逆序)时间复杂度为O(N^2) ;
二、希尔排序
思想:
希尔排序是对直接插入排序的改进,
希尔排序分为两部分:1、预排序 2、直接插入排序;
希尔排序引进了一个参数:gap 表示间距;即按照gap将所需排序的数,分组,然后在组中进行排序(预排序);
注意 :
gap 越大时,后面的数越容易到达前面,预排序效果不明显;
gap越小时,预排序效果明显,但后面的数不易到前面;
因此 gap的选择很重要,一般 gap = n / 3 + 1; (加一保证了最后一次是直接插入排序)
//希尔排序 对直接插入排序的优化
//1.预排序
//2.直接插入排序
void ShellSort(int* a,int n) //时间复杂度 平均O(N^1.3); 最坏情况(已排序O(N^2))
{
int gap = n ;
while(gap > 1)
{
gap = gap / 3 + 1; //分组的间距 + 1 确保了最后一次一定是直接插入排序
for(int i = 0 ;i < n - gap ;i++)
{
int end = i;
int tmp = a[end + gap];
while(end >= 0 && a[end] > tmp)
{
a[end + gap] = a[end];
end -= gap;
}
a[end + gap] = tmp;
}
}
}
时间复杂度 :平均O(N^1.3)
三、选择排序
思想:
1、在一段区间内将找出最小的数,将其放在开始,找出最大的数,将其放在最后;
2、然后缩小区间(起始位置为刚才区间的后一个,终止为刚才区间末尾的前一个),执行 第1步;
重复以上操作直到区间长度为1,则已排好序;
代码:
//选择排序
//时间复杂度 O(N^2)
void SelectSort(int* a,size_t n)
{
int begin = 0;
int end = n - 1 ;
while(begin < end)//多个区间
{
int min = begin;
int max = end;
for(int i = begin ; i <= end ;i++) //一个期间寻找最大值和最小值
{
if(a[i] < a[min])
min = i;
if(a[i] > a[max])
max = i;
}
swap(a[begin],a[min]);
if(begin == max)
{
max = min; // a[max]已经被换掉了
}
swap(a[end],a[max]);
++begin;
--end;
}
}
时间复杂度: O(N^2)
四、堆排序
思想:
首先创建一个大堆
第一步:将堆顶的数(最大值)与最后一个数进行交换,这样便将最大的数放到了最后;
第二步:将最后一个数之前的数看成一个新的堆,然后调整,使其再一次成为大堆;
第三步:重复第一步;直到只剩下一个数;
代码:
void AdjustDown(int* a,int root,size_t n) //向上调整算法
{
int parent = root;
int child = parent * 2 + 1;
while(child < n)
{
if(child + 1 < n && a[child] < a[child+1])
++child; //注意
if(a[parent] < a[child])
{
swap(a[parent],a[child]);
parent = child;
child = parent * 2 + 1;
}
else
break;
}
}
//堆排的时间复杂度:NlogN
void HeapSort(int* a,size_t n)
{
for(int i = (n - 2)/2 ;i >= 0;i--)
{
AdjustDown(a,i,n); //建堆 logN
}
int end = n - 1;
while(end > 0)
{
swap(a[0],a[end]); //将最大的数放在最后
AdjustDown(a,0,end);
--end;
}
}
时间复杂度: O(N logN ) logN:表示log以二为底N的对数
五、快速排序
思想:
在一段区间中选择一个数为key,使得比它小的数都在它的左边,比它大的都在它的右边;将它的左边看成一个区间,将它的右边看成一个区间,即子问题;直到区间长度为一
代码:
void QuickSort(int* a,int left ,int right)
{
if( left >= right)
return;
int div = part3(a,left,right);
QuickSort(a,left,div-1);
QuickSort(a,div+1,right);
}
在一段区间中选择一个数为key,使得比它小的数都在它的左边,比它大的都在它的右边;可以利用三种方法来实现;
1、左右指针法
思想:选择区间的最后一个元素作为key,区间的开始为begin,结束为end;begin寻找比key大的值,end找比key小的值,将begin和end对应的值进行交换;
int part1(int* a,int begin ,int end)
{
int& key = a[end];
while(begin < end)
{
while(begin < end && a[begin] <= key)
++begin;
while(begin < end && a[end] >= key)
--end;
swap(a[begin],a[end]);
}
swap(a[begin],key);
return begin;
}
2、挖坑法
int part2(int* a,int begin ,int end) //挖坑法
{
int key = a[end];
int tmp = end;
while(begin < end)
{
while(begin < end && a[begin] <= key)
++begin;
a[tmp] = a[begin];
tmp = begin;
while(begin < end && a[end] >= key)
--end;
a[tmp] = a[end];
tmp = end;
}
a[tmp] = key;
return begin;
}
3、前后指针法
思想:cur找比key小的值,找到之后使prev++;cur与prev不同的时候交换其各自所对应的值(因为cur总在prev之前,当cur与prev不同时,prev所对应的的值总是比key大的值,而cur对应的值总是比key小的值,这样一交换之后便将比key大的值放到了key之后)
int part3(int* a ,int begin,int end)
{
int& key = a[end];
int cur = begin;
int prev = begin - 1;
while(cur < end)
{
if(a[cur] < key && ++prev != cur)
{
swap(a[prev],a[cur]);
}
++cur;
}
swap(a[++prev],key);
return prev;
}
空间复杂度 : O(logN)
时间复杂度: O(N logN ) logN:表示log以二为底N的对数
从上图可以看出快排的时间复杂度与key的选择有很大的关系,
快排的优化:
1、三数取中法:使得key的选择更加合理
//key的选择 优化:三数取中法
int GetMid(int* a,int begin,int end)
{
int mid = begin + ((begin - end) << 1 );
if(a[begin] > a[mid]) // begin mid
{
if(a[end] > a[begin]) //end begin mid
mid = begin;
else if(a[end] > a[mid]) // begin end mid
mid = end;
}
else // mid begin
{
if(a[begin] > a[end]) // mid begin end
mid = begin;
else if(a[end] < a[mid]) // mid end begin
mid = end ;
}
return mid;
}
2、小区间优化 : 当区间很小时,可以使用直接插入排序
以上的快排都是使用递归方法来实现的,递归对栈的开销太大,
下面使用循环来实现快排:
思想:每次将所需要的区间起始,结束位置压入自己建的栈中,利用循环来实现快排
代码:
//快排的非递归: 递归会消耗堆栈
void QuickSortR(int* a,int left,int right)
{
stack<int> s;
if( left < right)
{
s.push(right);
s.push(left);
}
while(!s.empty())
{
int begin = s.top();
s.pop();
int end = s.top();
s.pop();
int index = part1(a,begin,end);
if(begin < index - 1)
{
s.push(index - 1);
s.push(begin);
}
if(index + 1 < end)
{
s.push(end);
s.push(index + 1);
}
}
}
六、归并排序
思想:
若两个区间都是有序区间时,将这两个区间进行合并,使其成为有序区间
代码:
//归并排序
void _Merger(int* a,int* tmp,int begin1,int end1,int begin2,int end2)
{
int index = begin1;
int start = begin1;
int finish = end2;
//合并两个有序区间
while(begin1 <= end1 && begin2 <= end2) //[begin1,end1] [begin2,end2]
{
if(a[begin1] < a[begin2])
tmp[index++] = a[begin1++];
else
tmp[index++] = a[begin2++];
}
while(begin1 <= end1)
tmp[index++] = a[begin1++];
while(begin2 <= end2)
tmp[index++] = a[begin2++];
//void *memcpy( void* dest, const void* src, size_t count );
while(start <= finish)
{
a[start] = tmp[start];
++start;
}
//memcpy(a + start , tmp + start, finish - start + 1 );
}
void _MergerSort(int* a,int* tmp,int begin,int end)
{
if(begin < end)
{
int mid = begin + ((end - begin) >> 1);
_MergerSort(a,tmp,begin,mid);
_MergerSort(a,tmp,mid + 1,end);
// [begin,mid] ,[mid+1,end] 已经有序
_Merger(a,tmp,begin,mid,mid+1,end);
}
}
void MergerSort(int* a,size_t n)
{
assert(a);
int* tmp = new int[n];
_MergerSort(a,tmp,0,n-1);
delete[] tmp;
}