内排序算法

排序算法是面试中常见的问题,不同算法的时间复杂度、稳定性和适用场景各不相同。按照数据量和存储方式可以将排序算法分为 内排序(Internal Sorting)和 外排序(External Sorting)。

内排序是指对所有待排序的数据都可以一次性加载到内存中进行排序。这意味着所有的数据都可以直接在计算机的内存中操作,无需借助外部存储设备(如硬盘)。内排序算法的设计可以更加灵活,因为内存操作速度远远高于磁盘操作速度。常见的内排序算法包括快速排序、归并排序、插入排序、冒泡排序等。

外排序是指对大量数据进行排序时,由于数据无法一次性加载到内存中,需要借助外部存储设备进行排序。通常在外排序中,数据会被分成若干个小块,每次只处理其中一部分数据,然后将部分排序好的数据存储回外部存储设备,再进行合并等操作。外排序算法需要考虑数据的分块、读写外部存储的效率,以及合并有序数据等问题。常见的外排序算法有多路归并排序、置换选择排序等。

本文介绍的都是常见的内排序算法:插入排序、冒泡排序、选择排序、希尔排序、归并排序、快速排序、堆排序、桶排序。

1. 插入排序

插入排序的核心是将待排序的元素逐个插入到已经排好序的序列中,以构建有序的输出序列:

void inssort(int A[],int n){
    for(int i=1;i<n;i++){
        for(int j=i;j>0;j--){
            if(A[j]<A[j-1]){        //A[j]<A[j-1](此处小于即排在前面)
                swap(A,j,j-1);
            }
            else{
                break;              //A[j]已达到正确位置(时间优化)
            }
        }
    }
}

插入排序的平均时间复杂度为 O(n2),最好情况为 O(n),最坏情况为 O(n2),适用于小规模的数据集或者已经基本有序的数据。插入排序的空间复杂度为 O(1),是一种稳定排序算法。

2. 冒泡排序

冒泡排序的核心思想是通过比较相邻的元素并交换位置,逐渐将最小的元素 “冒泡” 到正确的位置。冒泡排序的过程类似于冒泡泡沫在水中升起的过程,较大的元素会逐渐向序列的开头 “冒泡”:

void bubsort(int A[],int n){
    for(int i=0;i<n-1;i++){
        for(int j=n-1;j>i;j--){
            if(A[j]<A[j-1]){
                swap(A,j,j-1);
            }
        }
    }
}

冒泡排序的平均时间复杂度为 O(n2),最好情况和最坏情况都为 O(n2)。冒泡排序的空间复杂度为 O(1),是一种稳定排序算法。

3. 选择排序

选择排序的核心思想是在未排序序列中选择最小的元素,然后将它与未排序序列的起始位置交换,使得已排序序列逐步增长:

void selsort(int A[],int n){
    for(int i=0;i<n-1;i++){
        int index=i;
        for(int j=n-1;j>i;j--){
            if(A[j]<A[index]){
                index=j;
            }
        }
        swap(A,i,index);
    }
}

选择排序的平均时间复杂度为 O(n2),最好情况和最坏情况都为 O(n2)。选择排序的空间复杂度为 O(1),是一种稳定排序算法。

4. 希尔排序

希尔排序利用了插入排序最佳时间代价特性,在不相邻的记录之间进行交换和比较。核心思想是将整个序列分成多个较小的子序列来逐步减少序列的无序度,从而在最后一步进行一次插入排序,达到提高排序速度的效果。过程如图:
在这里插入图片描述

void inssort2(int A[],int n,int incr){      //n为传入数组长度,incr为数组内元素间隔
    for(int i=incr;i<n;i+=incr){            //incr相当于第1位元素,+incr相当于间隔
        for(int j=i;(j>=incr)&&(A[j]<A[j-incr]);j-=incr){
            int tmp=A[j];
            A[j]=A[j-incr];
            A[j-incr]=tmp;
            cnt1++;
        }
    }
}
void shellsort(int A[],int n){
    for(int i=n/2;i>2;i/=2){            //i表示组数或者间隔
        for(int j=0;j<i;j++){           //j表示有i组数据时从0到i遍历
            inssort2(&A[j],n-j,i);      //对第j组数据进行插入排序(起始位置,长度,间隔)
        //第j组数据在A中下标:j,j+i,j+2i...用inssort2中传入参数表示为:&A[j],&A[j]+i,&A[j]+2i...
        }
    }
    inssort2(A,n,1);                    //对整个数组插入排序
}

希尔排序在现实中没有对应的直观解释,也无法证明其时间复杂度,一般认为平均时间复杂度为 O(n1.5),最好情况为 O(nlogn),最坏情况为 O(n2)。希尔排序的空间复杂度为 O(1),是一种不稳定排序算法。

5. 归并排序

归并排序的核心思想是 “二分+合并”,即将一个未排序的数组分割成两个子数组,递归地对这两个子数组排好序后再合并为一整个有序的数组:

void mergesort(int A[],int tmp[],int left,int right){	//left表示A[0]下标,right表示A[n-1]下标
    if(left==right) return;
    int mid=(left+right)/2;
    mergesort(A,tmp,left,mid);      //向下递归,二分集合并排序
    mergesort(A,tmp,mid+1,right);
    for(int i=left;i<=right;i++){
        tmp[i]=A[i];
    }
    int i1=left,i2=mid+1;           //i1,i2表示两个待合并数组(各自都已排序)首个未排序元素下标
    for(int curr=left;curr<=right;curr++){
        if(curr==mid+1){            //left数组已完成合并
            A[curr]=tmp[i2++];
        }
        else if(i2>right){          //right数组已完成合并
            A[curr]=tmp[i1++];
        }
        else if(tmp[i1]<tmp[i2]){
            A[curr]=tmp[i1++];
        }
        else{
            A[curr]=tmp[i2++];
        }
    }
}

归并排序的平均时间复杂度为 O(nlogn),最好情况和最坏情况也都为 O(nlogn)。归并排序的空间复杂度为 O(n),是一种稳定排序算法。

6. 快速排序

快速排序的核心思想是选择一个轴值 pivot,将数组分为小于轴值的部分和大于轴值的部分,然后递归地对这两部分进行排序。轴值的选择会影响算法的效率,一般选取数组的中间点作为轴值。为了设计方便,会将轴值置于数组末端,待排好序后再移动到合适位置:

//partition函数将数组的l+1~r-1划分为两个以轴值为分界的部分,并返回轴值的下标(实际上该位元素与最后一位交换后才是真正的轴值下标)
int partition(int A[],int l,int r,int& pivot){  //l,r为A[]的左右范围,pivot为轴值
    do{
        while(A[++l]<pivot);                    //跳过满足小于轴值的元素,终止在大于轴值的地方
        while((l<r)&&(A[--r]>pivot));
        int tmp=A[r];
        A[r]=A[l];
        A[l]=tmp;
    }while(l<r);
    return l;
}

void qsort(int A[],int i,int j){
    if(i>=j)    return;
    int pivotindex=(i+j)/2;                 //选取轴值
    int pivot=A[pivotindex];
    A[pivotindex]=A[j];						//将轴值与末尾元素互换
    A[j]=pivot;
    pivotindex=partition(A,i-1,j,A[j]);     //将l+1~r-1按轴值分部,并返回待与轴值交换元素下标
    pivot=A[j];                             //轴值还在末尾等待交换
    A[j]=A[pivotindex];
    A[pivotindex]=pivot;
    qsort(A,i,pivotindex-1);
    qsort(A,pivotindex+1,j);
}

快速排序是迄今为止所有内排序算法中在平均情况下最快的一种,当每个轴值都把数组分成相等的两个部分时,整个算法的时间代价是 Θ \Theta Θ(nlogn)。快速排序的平均时间复杂度为 O(nlogn),最好情况为 O(nlogn),最坏情况为 O(n2)。快速排序的空间复杂度为 O(logn),是一种不稳定排序算法。

7. 堆排序

堆排序是基于堆数据结构的排序算法,它利用最小堆的性质来实现对数组的排序:

class heap{
private:
    int* Heap;
    int maxsize;
    int n;
    void siftdown(int pos){
        while(!isLeaf(pos)){
            int j=leftchild(pos);       //下面j表示的是lc和rc中较小值的下标
            int rc=rightchild(pos);
            if((rc<n)&&(Heap[rc]<Heap[j])){
                j=rc;
            }
            if(Heap[pos]<Heap[j]){
                return;
            }
            int tmp=Heap[pos];
            Heap[pos]=Heap[j];
            Heap[j]=tmp;
            pos=j;
        }
    }
public:
    heap(int* h,int num,int max){
        Heap=h;
        n=num;
        maxsize=max;
        buildHeap();
    }
    int size(){
        return n;
    }
    bool isLeaf(int pos){
        return (pos>=n/2)&&(pos<n);
    }
    int leftchild(int pos){
        return 2*pos+1;
    }
    int rightchild(int pos){
        return 2*pos+2;
    }
    int parent(int pos){
        return (pos-1)/2;
    }
    void buildHeap(){
        for(int i=n/2-1;i>=0;i--){
            siftdown(i);
        }
    }
    void insert(const int& it){
        int curr=n++;
        Heap[curr]=it;
        while((curr!=0)&&(Heap[curr]<Heap[parent(curr)])){
            int tmp=Heap[curr];
            Heap[curr]=Heap[parent(curr)];
            Heap[parent(curr)]=tmp;
            curr=parent(curr);
        }
    }
    int removefirst(){
        n--;
        int tmp=Heap[0];
        Heap[0]=Heap[n];
        Heap[n]=tmp;
        if(n!=0){
            siftdown(0);
        }
        return Heap[n];
    }
    int remove(int pos){
        if(pos==(n-1)){
            n--;
        }
        else{
            n--;
            int tmp=Heap[pos];
            Heap[pos]=Heap[n];
            Heap[n]=tmp;
            while((pos!=0)&&(Heap[pos]<Heap[parent(pos)])){
                int tmp=Heap[pos];
                Heap[pos]=Heap[parent(pos)];
                Heap[parent(pos)]=tmp;
                pos=parent(pos);
            }
            if(n!=0){
                siftdown(pos);
            }
        }
        return Heap[n];
    }
};
void heapsort(int A[],int n){
    int minval;
    heap H(A,n,n);
    for(int i=0;i<n;i++){
        minval=H.removefirst();
    }
}

堆排序的平均时间复杂度为 O(nlogn),最好情况和最坏情况也都为 O(nlogn),但比快速排序要慢一个常数因子。堆排序在实际使用时适用于查找第 k 大小的元素,或者用于外排序。堆排序的空间复杂度为 O(1),是一种不稳定排序算法。

8. 桶排序

桶排序的核心思想是将数组按数值范围放入若干个桶,每个桶对应一定范围的数据,然后对每个桶中的数据使用其他排序算法或递归方式进行排序,最后将所有桶中的数据按顺序合并得到排序结果:

void inssort(int A[],int n){
    for(int i=1;i<n;i++){
        for(int j=i;j>0;j--){
            if(A[j]<A[j-1]){
            	int tmp=A[j];
            	A[j]=A[j-1];
            	A[j-1]=tmp;
            }
            else{
                break;
            }
        }
    }
}

void bucketSort(int A[],int n,int numBuckets){
    //创建桶
    int buckets[numBuckets][n]; 	//二维数组,每行表示一个桶
    int bucketSizes[numBuckets]; 	//每个桶中元素的数量
    //初始化桶的大小
    for(int i=0;i<numBuckets;i++){
        bucketSizes[i]=0;
    }
    //确定数组元素范围
	int maxA=-999,minA=999;
	for(int i=0;i<n;i++){
		if(A[i]>maxA)	maxA=A[i];
		if(A[i]<minA)	minA=A[i];
	} 
    //将元素放入桶中
    for(int i=0;i<n;i++){
    	int bucketGap=ceil((maxA-minA)/(float)numBuckets);
        int bucketIndex=(A[i]-minA)/bucketGap;
        buckets[bucketIndex][bucketSizes[bucketIndex]++]=A[i];
    }
    //对每个桶中的元素进行插入排序
    for(int i=0;i<numBuckets;i++){
        inssort(buckets[i],bucketSizes[i]);
    }
    // 合并桶中的元素
    int index=0;
    for(int i=0;i<numBuckets;i++){
        for(int j=0;j<bucketSizes[i];j++){
            A[index++]=buckets[i][j];
        }
    }
}

桶排序的平均时间复杂度为 O(n2),最好情况和最坏情况也都为 O(n2),但比插入排序要快一个常数因子。桶排序适用于在已知输入数据的范围内进行排序,若数据分布不均匀也会影响效率。桶排序的空间复杂度为 O(n2),是一种稳定排序算法。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
1、冒泡排序属于稳定排序,是一种借助“交换”进行排序的方法。首先要将第一个记录的关键字和第二个记录的关键字进行比较,若为逆序,则将两个记录交换之,然后比较第二个记录与第三个记录的关键字,以此类推,直至第n-1个记录与第n个记录的关键字进行比较为止,这一过程称为第一趟冒泡排序,其结果使得关键字最大的记录被安置在最后一个记录的位置上;然后进行第二趟冒泡排序,对前N-1个记录进行同样操作;以此类推,直到在一趟排序过程中没有进行过交换记录的操作为止。 2、直接插入排序属于稳定的排序,每次从无序表中取出第一个元素,把它插入到有序表的合适位置,使有序表仍然有序。第一趟将待比较的数值与它的前一个数值进行比较,当前一数值比待比较数值大的情况下继续循环比较,依次进行下去,进行了(n-1)趟扫描以后就完成了整个排序过程,结束该次循环。 3、快速排序属于不稳定排序,是对起泡排序的一种改进。它的基本思想是,通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。假设待排序的序列为{R.[s],R.[s+1],…….,R.[t]},首先任意选取一个记录,然后按下述原则从新排序记录:将关键字较他小的记录都安置在他的位置之前,将所有关键字较他大的记录都安置在他的位置后面。由此可以该“枢轴”记录最后所落的位置i作为分界线,将序列{R[s],R[s+1]…….R[t]}分割成两个子序列{R[s],R[s+1]…..R[i-1]}和{R[i+1]……R[t]},这个过程称作一趟快速排序。一趟快速排序的具体做法是:附设两个指针low和high,它们的初值分别指向数组第一个数据和最后一个数据,将枢轴记录暂存在R[0]的位置上排序过程中只作R[low]或R[high]的单向移动,直至一趟排序结束后再将枢轴记录移至正确位置上。 4、简单选择排序属于不稳定排序,基本思想是,每一趟在n-i+1(i=1,2,…n-1)个记录中选取关键字最小的记录作为有序序列中第i个记录。第i趟简单选择排序是指通过n-i次关键字的比较,从n-i+1个记录中选出关键字最小的记录,并和第i个记录进行交换。共需进行n-1趟比较,直到所有记录排序完成为止。例如:进行第i趟选择时,从当前候选记录中选出关键字最小的k号记录,并和第i个记录进行交换。 5、希尔排序属于不稳定排序,也是一种属插入排序类,它的基本思想是:先将整个待排记录序列分割称为若干个子序列分别进行直接插入排序,待整个序列中记录“基本有序”时,再对全体记录进行一次直接插入排序。希尔排序的一个特点是:子序列的构成不是简单的“逐段分割”,而是将相隔某个“增量”的记录组成一个子序列。 6、堆排序属于不稳定排序,它的基本思想是,先将初始文件R[1..n]建成一个大根堆,此堆为初始的无序区,再将关键字最大的记录R[1](即堆顶)和无序区的最后一个记录R[n]交换,由此得到新的无序区R[1..n-1]和有序区R[n],且满足R[1..n-1].keys≤R[n].key;由于交换后新的根R[1]可能违反堆性质,故应将当前无序区R[1..n-1]调整为堆,然后再次将R[1..n-1]中关键字最大的记录R[1]和该区间的最后一个记录R[n-1]交换,由此得到新的无序区R[1..n-2]和有序区R[n-1..n],且仍满足关系R[1..n- 2].keys≤R[n-1..n].keys,同样要将R[1..n-2]调整为堆。直到无序区只有一个元素为止。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值