快排、堆排、归并排 的理解

快速排序

        快速排序是冒泡排序的进阶版,平均时间复杂度为O ( nlogn ),算法思路如下:

 1.选定一个值作为标量,把 比该值小的数放左边,比该值大的数放右边。

 2.分别对左右两边的数再次进行1中的操作一直递归,直到只剩下一个数为止。

 

比如:  

      7,1,5,8,9,2,4,3 

假定每次选择第一个数为标量 :

下面模拟第一组序列得到的过程:

s:标量

lr:左右指针 

 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

③ (12  (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~~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值