数据结构基础 排序算法(一) 概念篇

       本辑将会对笔试面试最常涉及到的12种排序算法(包括插入排序、二分插入排序、希尔排序、选择排序、冒泡排序、鸡尾酒排序、快速排序、堆排序、归并排序、桶排序、计数排序和基数排序)进行详解。每一种算法都有基本介绍、算法原理分析、图解演示、算法代码、笔试面试重点分析、笔试面试题等板块。


一、直接插入排序
1)算法简介

       插入排序(Insertion Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
2)算法描述和分析
一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:

1、从第一个元素开始,该元素可以认为已经被排序

2、取出下一个元素,在已经排序的元素序列中从后向前扫描

3、如果该元素(已排序)大于新元素,将该元素移到下一位置

4、重复步骤3,直到找到已排序的元素小于或者等于新元素的位置

5、将新元素插入到该位置后

6、重复步骤2~5

       如果目标是把n个元素的序列升序排列,那么采用插入排序存在最好情况和最坏情况。

       最好情况就是,序列已经是升序排列了,在这种情况下,需要进行的比较操作需(n-1)次即可。

       最坏情况就是,序列是降序排列,那么此时需要进行的比较共有n(n-1)/2次。插入排序的赋值操作是比较操作的次数减去(n-1)次。

       平均来说插入排序算法复杂度为O(n^2)。

因而,插入排序不适合对于数据量比较大的排序应用。但是,如果需要排序的数据量很小,例如,量级小于千,那么插入排序还是一个不错的选择。 插入排序在工业级库中也有着广泛的应用,在STL的sort算法和stdlib的qsort算法中,都将插入排序作为快速排序的补充,用于少量元素的排序(通常为8个或以下)。

3)算法图解

数据结构基础 排序算法(一) 概念篇_第1张图片

4)算法代码

// 插入排序(直接)
// 依次取出遍历每一项 将每一项temp=a[j]和其之前的数进行比较,如果发现了比自己大的数 就将其放在当前j的位置 同时j--,继续用temp和之前的数进行比较,直到没有比自己大的数时结束。
// 直接插入排序  
void insertSort(int *a,int size)  
{  
    if(size<2)  
        return;  
    int temp;  
    int j;  
    for(int i=1;i<size;i++)  
    {  
        temp=a[i];
        for( j=i;j>0&&temp<a[j-1];j--)
        {  
              a[j]=a[j-1];  
        }  
        a[j]=temp;  
    }  
    print(a,size);  
} 
 
 

5)考察点,重点和频度分析

        把插入排序放在第一个的原因是因为其出现的频度不高,尤其是这里提到的直接排序算法,基本在笔试的选择填空问时间空间复杂度的时候才可能出现。毕竟排序速度比较慢,因此算法大题中考察的次数比较比较少

6)笔试面试例题

例题1

请写出链表的插入排序程序

详见:数据结构基础 链表的插入排序程序

详址:http://blog.csdn.net/u013630349/article/details/48242949

template<typename T>  
struct list_node  
{  
    struct list_node<T> *next;  
    T value;  
};  
template<typename T>  
struct _list  
{  
    struct list_node<T> *head;  
    int size;  
};  
template<typename T>  
void SortLink(struct _list<T> * link) {  
    struct list_node<T> *pHead,*pRear,*p,*tp; 
    if (!link) return;  
    for (pHead=link->head,pRear=0;pHead;pHead=pHead->next) {  
        for (tp=pHead,p=pHead->next;p;tp=p,p=p->next)  
            if (pHead->value>=p->value)  
                tp->next=p->next,p->next=pHead,pHead=p,p=tp;  
        if (!pRear) link->head=pHead;  
        else pRear->next=pHead;  
        pRear=pHead;  
    }  
}  

例题2
下列排序算法中最坏复杂度不是 n(n-1)/2 的是
A.快速排序     B.冒泡排序   C.直接插入排序   D.堆排序
 D


二、二分插入排序
1)算法简介
        二分(折半)插入(Binary insert sort)排序是一种在直接插入排序算法上进行小改动的排序算法。其与直接排序算法最大的区别在于查找插入位置时使用的是二分查找的方式,在速度上有一定提升。
2)算法描述和分析
一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:
        1、从第一个元素开始,该元素可以认为已经被排序;
        2、取出下一个元素,在已经排序的元素序列中二分查找到第一个比它大的数的位置;
        3、将新元素插入到该位置后;
        4、重复上述两步。

算法性能
        1)稳定
        2)空间代价:O(1)
        3)时间代价:插入每个记录需要O(log i)比较,最多移动i+1次,最少2次。最佳情况O(n log n),最差和平均情况O(n^2)。
        二分插入排序是一种稳定的排序。当n较大时,总排序码比较次数比直接插入排序的最差情况好得多,但比最好情况要差,当元素初始序列已经按排序码接近有序时直接插入排序比二分插入排序比较次数少。二分插入排序元素移动次数与直接插入排序相同,依赖于元素初始序列。
3)算法图解

数据结构基础 排序算法(一) 概念篇_第2张图片


4)算法代码
// 折半插入排序(binary insertion sort)是对插入排序算法的一种改进,由于排序算法过程中,就是不断的依次将元素插入前面已排好序的序列中。由于前半部分为已排好序的数列,这样我们不用按顺序依次寻找插入点,可以采用折半查找的方法来加快寻找插入点的速度。
// 折半插入排序算法的具体操作为:在将一个新元素插入已排好序的数组的过程中,寻找插入点时,将待插入区域的首元素设置为a[low],末元素设置为a[high],每轮比较时将待插入元素与a[m],其中m=(low+high)/2相比较,如果比参考元素小,则选择a[low]到a[m-1]为新的插入区域(即high=m-1),否则选择a[m+1]到a[high]为新的插入区域(即low=m+1),如此直至low<=high不成立,即将此位置之后所有元素后移一位,并将新元素插入a[high+1]。
 
   
void binaryInsertSort(int *a,int size)  
{  
    int low,high,m,temp,j;  
    for (int i=1;i<size;++i){  
       temp=a[i];  
       low=0;  
       high=i-1;  
      /* if(a[i]>a[i-1])   //这句话可以稍微优化一下下速度 不影响结果 
           continue;*/  
       while (low<=high){  
         m=(low+high)/2;  
         if (temp<=a[m])  
             high=m-1;   //当程序跳出时 high指向的其实并不是 >=temp的值的位置 而是>=temp的值的左边的位置 所以....  
         else    
             low=m+1;  
       }  
       for( j=i;j>high+1;--j)        //所以 这里的high要加 1   
           a[j]=a[j-1];  
       a[j]=temp;  
     }  
    print(a,size);  
}  
 
     
   5)考察点,重点和频度分析 
  
        这个排序算法在笔试面试中出现的频度也不高,但毕竟是直接排序算法的一个小改进算法,同时二分查找又是很好的思想,有可能会在面试的时候提到,算法不难,留心一下就会了。 
6)笔试面试例题 
例题1下面的排序算法中,初始数据集的排列顺序对算法的性能无影响的是(B) 
A、二分插入排序         B、堆排序         C、冒泡排序            D、快速排序 
例题2写出下列算法的时间复杂度。 
(1)冒泡排序;(2)选择排序;(3)插入排序;(4)二分插入排序;(5)快速排序;(6)堆排序;(7)归并排序;
三、希尔排序 
1)算法简介 
希尔排序,也称递减增量排序算法,因DL.Shell于1959年提出而得名,是插入排序的一种高速而稳定的改进版本。
插入排序(Insertion Sort)的一个重要的特点是,如果原始数据的大部分元素已经排序,那么插入排序的速度很快(因为需要移动的元素很少)。从这个事实我们可以想到,如果原始数据只有很少元素,那么排序的速度也很快。--希尔排序就是基于这两点对插入排序作出了改进。

例如,有100个整数需要排序。

  1. 第一趟排序先把它分成50组,每组2个整数,分别排序。
  2. 第二趟排序再把经过第一趟排序后的100个整数分成25组,每组4个整数,分别排序。
  3. 第三趟排序再把前一次排序后的数分成12组,每组不少于8个整数,分别排序。
  4. 照这样子分下去,最后一趟分成100组,每组一个整数,这就相当于一次插入排序。
由于开始时每组只有很少整数,所以排序很快。之后每组含有的整数越来越多,但是由于这些数也越来越有序,所以排序速度也很快。 
2)算法描述和分析 
1、先取一个小于n的整数d1作为第一个增量,把文件的全部记录分成d1个组。
2、所有距离为d1的倍数的记录放在同一个组中,在各组内进行直接插入排序。
3、取第二个增量d2<d1重复上述的分组和排序。
4、直至所取的增量dt=1(dt<dt-l<…<d2<d1),即所有记录放在同一组中进行直接插入排序为止。        
希尔排序的时间复杂度与增量序列的选取有关。
        例如希尔增量时间复杂度为O(n^2),而Hibbard增量的希尔排序的时间复杂度为O(N^(5/4)),但是现今仍然没有人能找出希尔排序的精确下界。
由于多次插入排序,我们知道一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以shell排序是不稳定的。
3)算法图解

数据结构基础 排序算法(一) 概念篇_第3张图片

4)算法代码
#include <stdio.h>
#include <stdlib.h>

#define MAX 8
#define T_Num 2
void shellsort(int A[], int N);
void printarray(int A[]);

int main()
{
  int i, s[MAX];

  for (i = 0; i < MAX; i++)
    s[i] = 1+(int) (100.0*rand()/(RAND_MAX+1.0));

  printf("before   :");		// 打印排序前的数据
  printarray(s);
  shellsort(s, MAX);

  return 0;
}

void shellsort(int V[], int n)
{
	int gap, i, j, temp;
	gap = n/T_Num;
	bool flag = false;
	while(gap > 0) 
	{
		printf("\ngap = %d\t\tV[j] - V[j+gap]\n", gap);	//打印gap的值
		// 外层循环 往 后面走 步长为 1
		for (i = gap; i < n; i++) {
		  printf("i = %d\t\t", i);				//打印 i 的值
		  // 内层循环 往 前面走 步长为 gap
		  for (j = i-gap; j>=0; j -= gap) {
		if (V[j] > V[j+gap]) {
		  temp = V[j];
		  V[j] = V[j+gap];
		  V[j+gap] = temp;
		}
		printf("[%2d]-[%2d]  ", j, j+gap);	//打印每次进行比较的 j 和 j+gap
		  }
		  printf("\n");
		}
		printf("after gap(%d):", gap);		//打印每趟排序后的结果
		printarray(V);
		gap /= T_Num;
		if(!flag&&(gap<=1))
		{
			gap = 1;
			flag = !flag;
		}
	}
}

void printarray(int a[])
{
  int i;
  for (i = 0; i < MAX; i++)
    printf(" %d", a[i]);
  printf("\n");
}
 
   5)考察点,重点和频度分析 
  
        事实上希尔排序算法在笔试面试中出现的频度也不比直接插入排序高,但它的时间复杂度并不是一个定值,所以偶尔会被面试官问到选择的步长和时间复杂度的关系,要稍微有点了解吧。算法大题中使用该方法或者其思想的题也不多。 
6)笔试面试例题
例题1写出希尔排序算法程序,并说明最坏的情况下需要进行多少次的比较和交换。
     程序略,需要O(n^2)次的比较 
例题2设要将序列(Q, H, C, Y, P, A, M, S, R, D, F, X)中的关键码按字母序的升序重新排列, 则: 
冒泡排序一趟扫描的结果是       H, C, Q, P, A, M, S, R, D, F, X ,Y      ; 
初始步长为4的希尔(shell)排序一趟的结果是   P, A, C, S, Q, D, F, X , R, H,M, Y     ; 
二路归并排序一趟扫描的结果是   H, Q, C, Y,A, P, M, S, D, R, F, X   ; 
快速排序一趟扫描的结果是     F, H, C, D, P, A, M, Q, R, S, Y,X     ; 
堆排序初始建堆的结果是   A, D, C, R, F, Q, M, S, Y,P, H, X   。
四、选择排序 
1)算法简介
        选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理如下。首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。 
2)算法描述和分析
        n个记录的文件的直接选择排序可经过n-1趟直接选择排序得到有序结果:
        1、初始状态:无序区为R[1..n],有序区为空。
        2、第i趟排序(i=1,2,3...n-1)第i趟排序开始时,当前有序区和无序区分别为R[1..i-1]和R(i..n)。该趟排序从当前无序区中选出关键字最小的记录 R[k],将它与无序区的第1个记录R交换,使R[1..i]和R分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区。
        3、前n-1趟结束,数组有序化了        选择排序的交换操作介于0和(n-1)次之间。
        选择排序的比较操作为n(n-1)/2次之间。
        选择排序的赋值操作介于0和3(n-1)次之间。
        比较次数O(n^2)比较次数与关键字的初始状态无关,总的比较次数N=(n-1)+(n-2)+...+1=n*(n-1)/2。 
        交换次数O(n),最好情况是,已经有序,交换0次;最坏情况是,逆序,交换n-1次。 
        交换次数比冒泡排序少多了,由于交换所需CPU时间比比较所需的CPU时间多,n值较小时,选择排序比冒泡排序快。
最差时间复杂度 О(n^2)
最优时间复杂度 О(n^2)
平均时间复杂度 О(n^2)
最差空间复杂度 О(n) total, O(1)
3)算法图解
数据结构基础 排序算法(一) 概念篇_第4张图片
4)算法代码
void selection_sort(int *a, int len)  
{  
    register int i, j, min, t;  
    for(i = 0; i < len - 1; i ++)  
    {  
        min = i;  
        //查找最小值  
        for(j = i + 1; j < len; j ++)
            if(a[min] > a[j])  
                min = j;  
        //交换  
        if(min != i)  
        {  
            t = a[min];  
            a[min] = a[i];  
            a[i] = t;  
        }  
    }  
}  
5)考察点,重点和频度分析
        就博主看过的笔试面试题而言,选择算法也大多出现在选择填空中,要熟悉其时间和空间复杂度,最好最坏的情况分别是什么,以及在那种情况下,每一轮的比较次数等。
6)笔试面试例题

例题1
        在插入和选择排序中,若初始数据基本正序,则选用 插入排序(到尾部)   ;若初始数据基本反序,则选用   选择排序。
例题2
 下述几种排序方法中,平均查找长度(ASL)最小的是
A. 插入排序      B.快速排序        C. 归并排序       D. 选择排序

五、冒泡排序
1)算法简介

        冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
2)算法描述
        1、比较相邻的元素。如果第一个比第二个大,就交换他们两个。
        2、对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
        3、针对所有的元素重复以上的步骤,除了最后一个。
        4、持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
        冒泡排序是与插入排序拥有相等的执行时间,但是两种法在需要的交换次数却很大地不同。在最坏的情况,冒泡排序需要O(n^2)次交换,而插入排序只要最多O(n)交换。冒泡排序的实现(类似下面)通常会对已经排序好的数列拙劣地执行(O(n^2)),而插入排序在这个例子只需要O(n)个运算。因此很多现代的算法教科书避免使用冒泡排序,而用插入排序取代之。冒泡排序如果能在内部循环第一次执行时,使用一个旗标来表示有无需要交换的可能,也有可能把最好的复杂度降低到O(n)。在这个情况,在已经排序好的数列就无交换的需要。若在每次走访数列时,把走访顺序和比较大小反过来,也可以稍微地改进效率。有时候称为往返排序,因为算法会从数列的一端到另一端之间穿梭往返。
最差时间复杂度 O(n^2)
最优时间复杂度 O(n)
平均时间复杂度 O(n^2)
最差空间复杂度 总共O(n),需要辅助空间O(1)
3)算法图解
数据结构基础 排序算法(一) 概念篇_第5张图片

4)算法代码
#include <stdio.h> 
void bubbleSort(int arr[], int count) 
{ 
int i = count, j; 
int temp; 

while(i > 0) 
{ 
for(j = 0; j < i - 1; j++) 
{ 
if(arr[j] > arr[j + 1]) 
{ temp = arr[j]; 
arr[j] = arr[j + 1]; 
arr[j + 1] = temp; 
} 
} 
i--; 
} 

} 

int main() 
{ 
//测试数据 
int arr[] = {5, 4, 1, 3, 6}; 
//冒泡排序 
bubbleSort(arr, 5); 
//打印排序结果 
int i; 
for(i = 0; i < 5; i++) 
printf("%4d", arr[i]); 
} 
5)考察点,重点和频度分析
一般我们学到的第一个排序算法就是冒泡排序,不得不说,这个还真是一个很常见的考点,平均时间空间复杂度,最好最坏情况下的时间空间复杂度,在不同情况下每一趟的比较次数,以及加标志位减少比较次数等,都是需要注意的地方。
6)笔试面试例题
例题1
对于整数序列100,99,98,…3,2,1,如果将它完全倒过来,分别用冒泡排序,它们的比较次数和交换次数各是多少?
答:冒泡排序的比较和交换次数将最大,都是1+2+…+n-1=n(n-1)/2=50×99=4545次。
例题2
把一个字符串的大写字母放到字符串的后面,各个字符的相对位置不变,不能申请额外的空间。
事实上,这道题放到冒泡排序这里不知道是不是特别合适,只是有一种解法是类似冒泡的思想,如下解法一
解法一
每次遇到大写字母就往后冒,最后结果即为所求

#include <stdio.h> 
#include <string.h> 
//题目以及要求:把一个字符串的大写字母放到字符串的后面, 
//各个字符的相对位置不变,不能申请额外的空间。 
//判断是不是大写字母 
int isUpperAlpha(char c){ 
if(c >= 'A' && c <= 'Z'){ 
return 1; 
} 
return 0; 
} 
//交换两个字母 
void swap(char *a, char *b){ 
char temp = *a; 
*a = *b; 
*b = temp; 
} 
char * mySort(char *arr, int len){ 
if(arr == NULL || len <= 0){ 
return NULL; 
} 
int i = 0, j = 0, k = 0; 
for(i = 0; i < len; i++){ 
for(j = len - 1 - i; j >= 0; j--){ 
if(isUpperAlpha(arr[j])){ 
for(k = j; k < len - i - 1; k++){ 
swap(&arr[k], &arr[k + 1]); 
} 
break; 
} 
//遍历完了字符数组,但是没发现大写字母,所以没必要再遍历下去 
if(j == 0 && !isUpperAlpha(arr[j])){ 
//结束; 
return arr; 
} 
} 
} 
//over: 
return arr; 
} 
int main(){ 
char arr[] = "aaaaaaaaaaaaaaaaaaaaaaaAbcAdeBbDc"; 
printf("%s\n", mySort(arr, strlen(arr))); 
return 0; 
} 
解法二
步骤如下
1、两个指针p1和p2,从后往前扫描
2、p1遇到一个小写字母时停下, p2遇到大写字母时停下,两者所指向的char交换
3、p1, p2同时往前一格
代码如下
#include <stdio.h> 
#include <string.h> 
//判断是不是大写字母 
int isUpperAlpha(char c){ 
if(c >= 'A' && c <= 'Z'){ 
return 1; 
} 
return 0; 
} 
//交换两个字母 
void swap(char *a, char *b){ 
char temp = *a; 
*a = *b; 
*b = temp; 
} 
char * Reorder(char *arr, int len){ 
if(arr == NULL || len <= 0){ 
return NULL; 
} 
int *p1 = arr; 
int *p2 = arr; 
While(p1<arr+len && p2<arr+len){ 
While( isUpperAlpha(*p1) ){ 
P1++; 
} 
While( !isUpperAlpha(*p2) ){ 
P2++; 
} 
swap(p1, p2) 
} 
//结束 
return arr; 
} 
int main(){ 
char arr[] = "aaaaaaaaaaaaaaaaaaaaaaaAbcAdeBbDc"; 
printf("%s\n", Reorder(arr, strlen(arr))); 
return 0; 
} 

你可能感兴趣的:(冒泡排序,选择排序,希尔排序,插入排序,二分插入排序)