快速排序
快速排序是冒泡排序的进阶版,平均时间复杂度为O ( nlogn ),算法思路如下:
1.选定一个值作为标量,把 比该值小的数放左边,比该值大的数放右边。
2.分别对左右两边的数再次进行1中的操作一直递归,直到只剩下一个数为止。
比如:
7,1,5,8,9,2,4,3
假定每次选择第一个数为标量 :
下面模拟第一组序列得到的过程:
s:标量
l、r:左右指针
s l r
7 1 5 8 9 2 4 3 //起始状态
s l r
7 1 5 8 9 2 4 3 //l右移至第一个比标量大的数, r左移至第一个比标量小的数。
s l r //交换l r所指的值,l右移一位r左移一位
7 1 5 3 9 2 4 8
s l r //同上
7 1 5 3 4 2 9 8
s lr //当l r 指向同一个数时,用该数与标量
2 1 5 3 4 7 9 8 // 比较,若该数小于标量,则交换,
//否则用该数的前一个数与标量交换得到最后的结果
以下为每次的序列:
① (2 1 5 3 4 7 9 8) //第一次以7为标量,把比7小的数放左边,比7大的数放右边
② (1 2 5 3 4 ) 7 ( 8 9) // 对左右两边的数分别进行上面的操作,左右标量分别为2、9
③ (1)2 (4 3 5) 7 ( 8) 9
④ 1 2 (3)(4) 5 7 8 9
注意:快排的具体实现方式有多种,不同的实现方式 每次所得到的序列可能不一样!
通过理解该算法,我们可以想到,如果每一次的递归都是右边的值都比标量大,则该算法的时间复杂度就变为O(n^2)了,这也是为什么说快排的时间复杂度不稳定。
值得一提的是,通过快排可以实现查找第k大的元素,具体实现大家可以思考一下,提示:只需要对一边进行递归。
以上为使用下面代码所实现的序列:
void QuickSort(int *left,int *right){ //读入首尾元素的地址
if(left >= right) return;
int *left_move = left+1; // 以第一个元素为标量,即从第二个元素开始比较
int *right_move = right;
while(left_move < right_move){ //分别从首位开始遍历
while(*left > *left_move) ++left_move; //找到左边第一个大于标量的数
while(*left < *right_move) --right_move;//找到右边第一个小于标量的数
if(left_move<right_move) std::swap(*left_move,*right_move); //交换两个数
}
if(*left < *left_move) --left_move; // 如果当前这个数比标量大,那么交换该数前面的一个数(该数前面的一个数必然比标量小)
std::swap(*left,*left_move); // 否则,交换该数
QuickSort(left,left_move-1); //分别对左右两边的数依次执行上述算法。
QuickSort(left_move+1,right);
}
附上另一种快排的实现方法:(每次以最后一个元素为标量,从头遍历)
void QuickSort3(int *left,int *right){
if(left>=right) return;
int *index=left;
for(int *left_move=left;left_move<right;++left_move)
if(*left_move<*right)
std::swap(*left_move,*(index++));
std::swap(*index,*right);
QuickSort3(left,index-1);
QuickSort3(index+1,right);
}
归并排序
归并排序是插入排序的进阶版,平均时间复杂度为O ( nlogn ),大体的算法思路是,采用分治法将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。
通过下面的例子帮助理解:
7,1,5,8,9,2,4,3
因为归并排序是通过递归使从最小的区间开始排序:
①(1,7) (5,8) (2,9) (3,4)
② (1,5,7,8) (2,3,4,9)
③(1,2,3,4,5,7,8,9)
代码实现如下:
void MergeSort(int *left,int *right){
if(left >= right) return;
int *mid=left+(right-left)/2; //确定中间点
MergeSort(left,mid); //中点左右两边的子序列排序
MergeSort(mid+1,right);
int temp[right-left+5]; //辅助内存,用于左右子序列排序后的序列
int ans=1;
int *i=left,*j=mid+1;
while(i<=mid && j<=right){ //有序合并两数组
if(*i < *j) temp[ans++]=*(i++);
else if(*i > *j)temp[ans++]=*(j++);
else {
temp[ans++]=*(i++);
temp[ans++]=*(j++);
}
}
while(i<=mid) temp[ans++]=*(i++);
while(j<=right) temp[ans++]=*(j++);
for(int l=1;l<ans;++l) *(left++)=temp[l]; //把合并都的数组放入指定位置
}
堆排序
堆排序是选择排序的进阶版,该算法在选择排序之上改进的地方就是在每一次查找最小值上,通过建立小顶堆(二叉树)来实现logn下的查找最小值,其核心也是小顶堆的建立,所以时间复杂度为O(nlogn).
小顶堆:每一个父亲节点的值都小于他的左右子节点的一棵二叉树。(与之对应的还有大顶堆)
该算法的实现思路是:
1.建立一个小顶堆
2.取根节点元素(必然是当前树中最小的)进行存储。
3.把小顶堆最后一个元素移到根节点,更新小顶堆,执行2步骤,直到最后剩下一个节点。
通过该思路我们可以知道,该算法的时间复杂度是稳定的nlogn.
下面我们来看看样例:
7 1 5 8 2 4 3
首先把这些元素存入二叉树,这里用数组模拟二叉树,从0 或1开始都可以,只是注意左右子节点的查找不一样。7
1 5
8 2 4 3
①初始化成小顶堆:
具体思路为:从最后一个节点到开始,把每一个节点所在的子树初始化为小顶堆。
②
取出根节点,存入数组:1
把最后一个点复制给根节点:
更新成最小堆:
③
取出根节点,存入数组:1、2
把最后一个点复制给根节点:
更新成最小堆:
。
。
。
执行以上算法直到最后小顶堆为空时,最后得到的数组为1 2 3 4 5 7 8,排序结束.
代码实现如下:
void Built_min_heap(int temp[],int k,int ans){ //把二叉树temp更新为小顶堆
while(2*k+1<=ans){ //至上而下的更新,直到是叶子节点
if(temp[k]< temp[2*k+1] && temp[k]<temp[2*k]) break; //若父节点小于左右子节点
else if(temp[2*k] < temp[2*k+1]){ //左边子节点小
std::swap(temp[k],temp[2*k]);
k=k*2;
}
else{ //右边子节点小
std::swap(temp[k],temp[2*k+1]);
k=2*k+1;
}
}
if(2*k==ans && temp[2*k] < temp[k]) //特殊情况
std::swap(temp[k],temp[2*k]);
}
void HeapSort(int *start,int *end){
int count=end-start+1;
int temp[100];
for(int i=1;i<=count;++i) temp[i]=start[i-1]; //构造二叉树
for(int i=count;i>=1;--i) Built_min_heap(temp,i,count); //初始化小顶堆
for(int i=count;i>=1;--i){ //每次取根节点存入数组,更新小顶堆
start[count-i]=temp[1];
temp[1]=temp[i];
Built_min_heap(temp,1,i-1);
}
}
以上是个人对三种O(nlogn)排序算法的理解,希望对大家有所帮助 O(∩_∩)O~~