排序算法总结

本文详细介绍了几种常见的排序算法,包括简单选择排序、堆排序、直接插入排序、希尔排序、冒泡排序和快速排序。其中,堆排序和快速排序的时间复杂度分别为O(nlogn)和O(nlogn),而选择排序、冒泡排序和直接插入排序的时间复杂度为O(n^2)。此外,还提到了排序算法的稳定性,如选择排序和堆排序是不稳定的,而插入排序和冒泡排序是稳定的。

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

1. 简单选择排序

   简单选择排序的思想:将数组看作两段,左边已经排好序,右边待排序,左边元素都小于等于右边元素,用sorted_index 指向排序元素的下一个位置,用unsorted_index指向sorted_index的下一个元素,向后移动unsorted_index找到待排序中最小的一个元素,与sorted_index的元素交换。直到sorted_index==n-1.

   

void simple_select_sort(Item a[],int n)
{
	for(int sorted_index = 0; sorted_index < n - 1; sorted_index++){
		int min_index = sorted_index;
		for(int unsorted_index = sorted_index + 1; unsorted_index < n; unsorted_index++) {
			if(a[unsorted_index] < a[min_index]) {
				min_index = unsorted_index;
			}
		}

		if(min_index != sorted_index) {
			Item tmp = a[sorted_index];
			a[sorted_index] = a[min_index];
			a[min_index] = tmp;
		}
	}
}

这个代码内层循环中为了找最小的元素,做了n-sorted_index-1 次比较,交换次数为1或0。外层循环遍历了n-1次。排序的复杂度最坏情况,平均情况,最好情况都是O(n^2)。

并且,可以看出简单选择排序是稳定的。

2.  堆排序

为什么叫堆排序,堆是用来实现优先队列的,优先队列具有这样的性质:当你从优先队列中取一个元素,总是取到具有最高优先级的。通常用最大堆来实现堆排序,堆数组中,第一个元素总是最大的。

堆排序不是一个稳定的排序,因为每次调整的时候,总是要将第一个元素和最后一个元素交换。

我们先来看看最大堆的重要性质,假设有n个元素。


1) 堆对应的二叉树是完全二叉树。

2) 第i个(i>=1)个结点的如果有左右子结点,则左右子结点为a[i*2]和a[i*2+1],并且a[i]>=a[i*2] && a[i] >= a[i*2]。

堆排序的步骤如下:

1 .对于一个数组,先调整为一个最大堆,第一个元素就是最大元素。

2. 我们交换堆顶(最大元素)与堆中最后一个元素,并且将交换后的最后一个元素从堆中剔除。

3. 剩下的元素重新调整为堆结构。

重复2&3,直至堆中元素个数为0。

其中第1步是从第floor(n/2)个元素起一直调整至根元素。已经证明这一步是线性时间。第三步复杂度为O(logn),因此总体复杂度为O(nlogn)。

现在开始解决每一步。首先根据一个数组构造堆,假如我们的数组为{5,1,5,4,2,3,0},起初:


显然不满足堆结构,我们从第floor(7/2)=3个元素开始调整,由于{5,3,0}满足堆结构,再到第3-1=2个元素,由于{1,4,2}不满组堆结构,调整成{4,1,2},再到第2-1=1个元素,由于{5,4,5}满足堆结构,不需要调整,至此,将原始数组调整成堆结束:

/

这一步的代码:

void build_heap(int a[],int n)   //数组a index是从1开始的
{
     for(int i = n/2; i >= 1; i--) {
           int child_bigger_index = 2*i;
           if(2*i +1 <= n && a[2*i + 1] < a[2*i]) {
                 child_bigger_index = 2*i+1;
            } 
           if(a[i] < a[child_bigger_index]) {
                 fixDown(a,i,n);
            }
     }
}

有了最大堆之后,我们进行第2步,和第3步。


先将第一个元素和第7个元素(最后一个元素)交换,并将交换后的最后一个元素从堆中剔除。现在剩下的元素不是堆了,我们需要用第3步进行调整成堆:


重复上面两步:



直到最终数组排好序。

这两步代码如下:

void fixDown(int a[],int k,int n){
     while(k <= n/2) {
        int child_bigger_index = 2*k;
        if(2*k+1<=n && a[2*k+1]>a[2*k]) {
	    child_bigger_index = 2*k+1;
        }
        if(a[child_bigger_index] > a[k]) {
           int tmp = a[k];
           a[k] = a[child_bigger_index];
           a[child_bigger_index] = tmp;
           k = child_bigger_index;
        }
        else {
           break;
        }
     }
}

void heapSort(int a[],int n) {
     buildHeap(a,n);
     for(int sorted_index = n; sorted_index > 1; sorted_index--) {
         int tmp = a[1];
         a[1] = a[sorted_index];
         a[sorted_index] = tmp;
         fixDown(a,1,sorted_index-1);
     }
}

link: 

Heap Sort


3. 直接插入排序

    插入排序背后的思想很简单:

     * 调整一下前2个元素,让他们处在相对排好序的位置

     * 将第3个元素插入到前2个元素中,使他们相对排好序

     * 将第4个元素插入到前3个元素中,使他们相对排好序

     * ......

    插入排序和选择排序一样,排序的时候都是分为两段,第一段为已经排好序的,第二段为没有排好序的。但是和选择排序不同的是,第一段中已经排好序的不是最终排序结果,只是相对排好序。在内循环中,通常要挪动已经排好序的元素位置。代码如下:

    

void insert_sort(int a[],int n) // index from 1
{
    if(n<=1) return;
    for(int unsorted_index = 2; unsorted_index <= n; unsorted_index++) {
          int tmp = a[unsorted_index]; 
          int sorted_index = unsorted_index - 1;
          while(sorted_index >= 1 && a[sorted_index]>tmp) {
               a[sorted_index+1] = a[sorted_index];
               sorted_index--;
           }
         a[sorted_index+1] = tmp;
     }
}

插入排序是稳定的,并且插入排序适合几乎排好序的数组,因为几乎排好序的数组移动次数较少,最好的情况是O(n),最坏的情况是O(n^2)


4. 希尔排序

 我们直到插入排序在数组几乎排序的情况下效率很高,如果一个小元素在很后面,需要挪动前面很多元素来空出位置来让希尔这个元素插入,这样效率不高。希尔排序是在插入排序的基础上做了改进,其基本思想是:

   将数组元素分成多个子序列,每个子序列index以某一个增量增加,假如当前增量为gap,则每个子序列元素个数为ceil(n/gap)。对于每个子序列,进行插入排序。然后递减gap,直到gap减为1,排序结果即为最终结果。

  

void shell_sort(int a[],int n)  // index from 1
{
   int gap;
   for(gap = n/2; gap>=1; gap/=2)
   {
      for(int i = 1; i <= gap; i++)
      {
          for(j = i + gap; j <= n; j += gap) {   //子序列,进行插入排序
              int tmp = a[j];
              int k = j - gap;
              while(k>=0 && a[k] > tmp){
                  a[k+gap] = a[k];
                  k-=gap; 
              }
              a[k+gap] = tmp;
          }
       }
   }
}
希尔排序不是稳定的,因为是跳跃的。

5. 冒泡排序

插入排序是基于“逐个记录插入”的,选择排序是基于“选择”的,冒泡排序是基于"交换"的,冒泡排序也是将数组看作两段,左边是待排序的,右边是已经排好序的,并且右边已经排好序的比左边元素都大,并且递增,冒泡排序是将大元素逐渐沉到数组底部。

void bubule_sort(int a[],int n) //index from 1
{
   for(int bottom = n; bottom > 1; bottom--)
   {
	//swap the max of the unsorted to bottom
        for(int i = 1; i < bottom; i++)
        {
            if(a[i] > a[i+1])
            {
                int tmp = a[i+1];
		a[i+1] = a[i];
		a[i] = tmp;
	    }
        }
   }
}

相对于简单选择排序,冒泡排序交换次数明显更多。它是通过不断地交换把最大的数冒出来。冒泡排序平均时间和最坏情况下(逆序)时间为o(n^2)。最佳情况下虽然不用交换,但比较的次数没有减少,时间复杂度仍为o(n^2)。此外冒泡排序是稳定的。


6.  快速排序

快速排序用的是归并的思想,为了排序一个数组,我们先将它分成两部分,分别排序这两部分,这样整个数组就排好序了,不过与归并排序不同的是,快排用元素来划分数组成两部分,将小于划分点的元素放在左边,大于等于划分点的元素放到右边,然后递归地对两部分进行排序。

比如一个数组有8个元素:


我们用第一个元素55来进行划分:


如果我们递归的对1到3的元素和5到8的元素进行排序,最终整个数组就排好序了。

假如我们以第一个元素作为划分点,划分的某一个过程为:


一趟划分完成。

代码如下:

int partition(int a[],int l,int u)
{
    int povit = a[l];
    int m = l; // less than povit index
    for(int i = l+1; i <= u; i++) {  //i is the index of undecided elements.
	if(a[i] < povit) {
           int tmp = a[i];
           a[i] = a[++m];
           a[m] = tmp;
        }
    }
    a[l] = a[m];
    a[m] = povit;
    return m;
}

void quick_sort(int a[],int l, int u)
{
    if(l<u){
	int p = partition(a,l,u);
        quick_sort(a,l,p-1);
        quick_sort(a,p+1,u);
    }
}

 算法平均复杂度为O(nlogn),但是当数组已经有序,算法的比较次数O(n^2),也就是最坏情况下为O(n^2),可以用随机选取划分点的方法来解决这个问题: 

int randomize_partition(int a[],int l,int u)
{
    int rand_index = l+rand()%(u-l);
    swap(a[rand_index],a[l]);
    return partition(a,l,u);
}

还可以对快排进行调优,当l与u之差很小,也就是递归排序的元素很少的时候,可以用插入排序进行排序

void quick_sort(int a[],int l, int u)
{
    if(l<u){
          if(u-l < cutoff) {
              insertion_sort(a,l,u);
          }
          int p = partition(a,l,u);
          quick_sort(a,l,p-1);
           quick_sort(a,p+1,u);
     }
}

 

7. 归并排序

归并排序思想很简单,将一个数组平均分成两个部分,分别对这两个部分进行排序,然后将排好序的两部分合并成一个数组,但是合并过程需要开辟新的空间,将排好序两部分元素按顺序加到新的空间中,然后再将新的空间直接复制到原来的数组中。

void insert_sort(Item a[],int l,int r)
{
	int i,j;
	for(i = l+1; i <= r; i++)
	{
		j = i;
		while(j >= 1 && a[j] < a[j-1]) {exch(a[j],a[j-1]);j--;}
	}
}

void merge(Item a[],Item t[],int l,int m,int r)
{
	int i,j,k;
	for(i = l,j = m+1,k=l;i <= m && j <= r;k++)
		if(less(a[i],a[j])) t[k] = a[i++];
		else t[k] = a[j++];
	while(i<=m) t[k++] = a[i++];
	while(j<=r) t[k++] = a[j++];
}

void mergesort(Item a[],Item t[],int l,int r)
{
	int m;
	if(r-l<=10) insert_sort(a,l,r);
	else{
		m = (l+r)/2;
		mergesort(t,a,l,m);
		mergesort(t,a,m+1,r);
		merge(t,a,l,m,r);
	}
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值