冒泡排序(优化)、快速排序、归并排序和堆排序

本文详细介绍了四种经典的排序算法:冒泡排序、快速排序、归并排序和堆排序。冒泡排序通过相邻元素交换实现排序;快速排序采用分治法,以基准元素划分序列;归并排序同样基于分治,通过不断归并有序子序列完成排序;堆排序利用堆结构,先建堆再逐步调整。四种算法的时间复杂度和空间复杂度各有不同,适用于不同的场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

冒泡排序

原始版本,每次对比相邻元素,把更大的元素换到右边,则每轮比较可以将当前最大的元素移动到最右边,下一轮比较不用比较最后面已经有序的元素。

#include <iostream>

using namespace std;
int main() {
    vector<int> a;
    //初始化
    
    for(int i=0;i<a.size()-1;i++)
    {
        bool sorted = true;
        for(int j=0;j<a.size()-1-i;j++){
            if(a[j]>a[j+1]){
                sorted = false;
                swap(a[j], a[j+1]);
            }
        }
        if(sorted){
            break;
        }
    }
}
优化1

如果某一轮比较中没有移动元素,说明序列已经有序,可以提前结束。

#include <iostream>

using namespace std;
int main() {
    vector<int> a;
    //初始化
    
    for(int i=0;i<a.size()-1;i++)
    {
        bool sorted = true;
        for(int j=0;j<a.size()-1-i;j++){
            if(a[j]>a[j+1]){
                sorted = false;
                swap(a[j], a[j+1]);
            }
        }
        if(sorted){
            break;
        }
    }
}
优化2

假设有序列a[7] =[1, 2, 3, 4, 7, 0, 5] ,那么第一轮比较时可以看到前面四个元素是没有移动的,比较之后的结果是a[7] =[1, 2, 3, 4, 0, 5, 7] 。所以在第二轮比较中,没有必要再从第一个元素开始比较,而是可以直接从数字4开始比较,代码如下

#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> a;
    for(int i=0; i<20; i++){
    	a.push_back(rand()%1000);
	} 
    
    int begin_index = 0; //begin_index是本轮比较中第一个被交换的元素, 下轮交换时直接从他前面那个元素开始即可 
    for(int i=0; i<a.size()-1; i++)
    {
        bool sorted = true;
        for(int j=begin_index; j<a.size()-1-i; j++){
            if(a[j] > a[j+1]){
                sorted = false;
                swap(a[j], a[j+1]);
            }
            else{
            	if(sorted){
            		begin_index = j;
				}
			}
        }
        if(sorted){
            break;
        }
        if(begin_index-1 < 0) begin_index = 0;
		else begin_index = begin_index-1;
		
    }
    
    //验证 
    for(int i=0; i<a.size(); i++){
    	cout<<a[i]<<" ";
	}
}

快速排序

快速排序基于分治法原理,他的过程是这样的,首先在一个无序序列中,随机找出一个元素,比如找第一个元素,作为一个基准进行划分,就是说,序列中比他小的元素放到它的左边,比他大的元素,放到它的右边。最后使得这个基准元素成为中间元素,左边的元素比它小,右边的元素比它大,所以这个元素已经排好序,放到了最终的位置上。

然后再对左边的序列和右边的序列递归的执行相同的划分操作,最后整个序列都排序完毕。在这个过程中,递归树的高度是O(logn),然后每层递归的划分操作需要把序列中未确定位置的元素遍历一遍,所以时间复杂度是O(nlogn)。如果递归栈算进空间复杂度里面的话,它的空间复杂度就是O(logn),在极端情况下树退化成链,空间复杂度就是O(n)。

#include <iostream>
using namespace std;
int partition(vector<int> &a, int low, int high)//划分
{

    int pivot=a[low];//取第一个元素作为基准元素
    while(low<high)
    {
        //先处理high后处理low,因为a[low]是基准元素
        while(low<high && a[high]>=pivot){
            high--;
        }
        //此时a[high]<pivot
        a[low]=a[high];//a[low]交给a[high]之后,a[high]>pivot,符合要求,而且一开始时low=0,这个位置是基准元素,已经记录下来,不怕丢失
        
        
        while(low<high && a[low]<=pivot){
            low++;
        }
        //此时a[low]>pivot
        a[high]=a[low];//a[low]交给a[high]之后,a[high]>pivot,符合要求
        
        
    }
    //此时low=high,划分完毕,把pivot放到正确位置上
    a[low]=pivot
    return low;//pivot元素的最终索引位置
}
void quick_sort(vector<int> &a, int low, int high)
{
    if(low>=high) return ;//到达递归边界
    int pivot_pos=partition(a,low,high);//划分后得到基准元素的索引位置
    //分别递归处理左右部分序列
    quick_sort(a,low,pivot_pos-1);
    quick_sort(a,pivot_pos+1,high);
       
}
int main() {
    vector<int> a;
    //初始化
    
    quick_sort(a,0, s.size()-1);
}

归并排序

归并排序也是基于分治法原理,他的过程是这样的, 首先可以认为一个无序序列里每一个元素是内部有序的,因为它只有它自己,当然是有序的。然后接下来就是要让相邻的两个元素,进行归并操作,归并成一个包含两个元素的有序的序列。操作完成后就是每两个元素内部有序,但是外部无序。接下来就是将两个元素的序列之间归并成为4个元素的有序序列,一直指数上升,直到整个序列都是内部有序的。归并排序的时间复杂度是O(nlogn),空间复杂度是O(n),因为归并操作的时候需要开辟相同大小的空间来存放两个子序列的值。

(实际代码是递归方法,递归到叶子节点时做两两归并,然后回到上一级是归并成包含4个元素的有序区间,以此类推)

堆排序

以小根堆为例,堆排序的思想是先建一个小根堆,其中每个节点都比它的子节点数值要小,所以堆顶元素是堆里最小的元素。然后就可以开始堆排序了。

直接取出堆顶元素,作为有序序列的第一个元素,然后把堆的最后一个元素放到堆顶,接着向下调整堆,使得堆恢复小根堆的性质。重复操作,直到将堆的所有元素取出,就得到了有序序列。

其中初始建堆的时间复杂度为O(n),每次取完节点后调整堆的时间复杂度为O(logn),总共取n次数字,所以堆排序总的时间复杂度是O(nlogn)。

初始建堆的过程:堆虽然可以理解成一棵完全二叉树,但实际上它还是一个数组,是依靠节点之间的索引位置来判断父子关系的,就是说,假设一个节点的索引位置为i,那么它的两个子节点的索引位置为 2i 和 2i+1 。假设数组长度为n,那么建堆是从n/2这个索引位置的元素开始的,首先比较它与它两个子节点的大小,如果某个子节点更小,就交换父子节点的值,然后这个原父节点继续往下对比,直到它比子节点都小为止。调整完这个节点,就继续调整n/2-1这个节点,一直调整到第一个节点,堆就建立完毕了。

往下调整堆的方式:其实就是初始建堆时,每个节点往下调整的操作。

总之,从堆里删除节点时(往往是删除堆顶节点,拿最后一个节点填补),往下调整;往堆底插入节点时,往上调整。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值