C指针编程之道(五)- 排序算法

KinglyJn      2013-03-20

冒泡排序

算法介绍:

算法的基本思想:
冒泡法排序是交换排序的一种,我们可以将待排序的数组 array[0...n-1] 理解为一个圆柱,将数组中的每一个元素都看成是重量为
array[i]的气泡,其中array[0]在最上面,array[n-1]在最下面。排序时根据轻气泡在上重气泡在下的原则,则上而下扫描数组,
遇到违反原则的气泡,就使其向下沉,直到所有的气泡都是轻气泡在上,重气泡在下为止。

算法的基本流程:
1. 初始时,array[0...n-1]无序,对n个元素进行n-1趟扫描
    82431
2. 第一趟扫描,自上而下依次比较数组R相邻的两个气泡的重量,若发现轻者在下、重者在上,则交换两个气泡的位置,经过第一趟比较,
   最重的气泡就被送到最下面,因此下一趟比较就至少可以减少一次;
    28431 24831 24381 24318
3. 第二趟扫面,扫描array[0...n-2],经过此次扫描,“次重”的气泡就被送到array[n-2]的位置上;
    24318 23418 23148
4. 按照以上的方法进行扫描,每次的扫描都比上一次的扫描少一次,直到进行n-1趟扫描,算法结束。
    23148 21348
    12348

算法实现:

//原始冒泡排序算法
void bubble_sort(int *array, int n) {
    int t;
    for (int i=n-1; i>0; i--) {
        for (int j=0; j<i; j++) {
            if (array[j] > array[j+1]) {
                t = array[j];
                array[j] = array[j+1];
                array[j+1] = t;
            }
        }
    }
}

/*
0   8 2 4 3 1
1   2 4 3 1 8
2   2 3 1 4 8
3   2 1 3 4 8
4   1 2 3 4 8

我们发现,每一趟比较结束之后,若发现从某个位置r开始,不再进行记录交换,就说明从array[r+1]到array[n-1]
已经排好序了,从而下一趟比较只要进行到位置r就可以了。若发现某一趟扫描中没有交换记录,则说明所有的元素都已
经有序,算法就可以结束了,而不是非要进行n-1趟扫描。基于这种思想,可以给出冒泡排序的改进算法。
*/

//改进冒泡排序算法
void bubble_sort(int *array, int n) {
    int m=0,bound=n-1;
    int t=0;
    while (bound!=0) { //bound记录m的值,m是每趟冒泡最后一次发生交换的位置,只要排序还没有排完,m的值就会从0发生改变
        m = 0;
        for (int i=0; i<bound;i++) {
            if (array[i] > array[i+1]) {
                t = array[i];
                array[i] = array[i+1];
                array[i+1] = t;
                m = i;
            }
        }
        bound = m;
    }
}

/*
上述冒泡排序只考虑了重气泡下沉的过程,是“单向起泡”,即使得重气泡下沉,经过一次冒泡至少可以把关键词最大的记录送到最后的位置。
其实我们可以在重气泡下沉的同时,也使轻气泡上浮,这样经过一次扫描就可以至少把关键词最大的记录和关键词最小的记录放到最终的位
置上,这就是“双向起泡”,基于这种思想,我们给出双向起泡排序算法的实现如下。
*/

//双向起泡排序算法(最终版)
void bubble_sort(int *array, int n) {
    int mmin,boundmin=0;
    int mmax,boundmax=n-1;
    int t;
    
    while (boundmin<boundmax) {
        mmin = 0;
        mmax = 0;
        
        //重气泡下沉
        for (int i=boundmin; i<boundmax; i++) {
            if (array[i] > array[i+1]) {
                t = array[i];
                array[i] = array[i+1];
                array[i+1] = t;
                mmax = i;
            }
        }
        if (mmax==0) break; //本次没有记录交换,扫描结束
        boundmax = mmax;
        
        //轻气泡上浮
        for (int i=boundmax; i>boundmin; i--) {
            if (array[i] < array[i-1]) {
                t = array[i];
                array[i] = array[i-1];
                array[i-1] = t;
                mmin = i;
            }
        }
        if (mmin==0) break; //本次没有记录交换,扫描结束
        boundmin = mmin;
    }
}

冒泡排序性能评价:

  • 时间复杂度:最好时间为O(n),最坏时间为O(n^2),因此平均时间复杂度为O(n^2);
  • 空间复杂度:冒泡排序是就地排序,空间复杂度为O(1);
  • 稳定性:有算法的流程可知,冒泡排序是稳定的;



快速排序法

算法介绍:

算法的基本思想:
快速排序也叫分划交换排序,是交换排序的一种。它采用叫做分治的策略,将问题分解为若干规模更小但与原问题相似的子问题,递归地求解
这些子问题,最终将子问题的解组合为原问题的解。快速排序正是结合了分治法这种“分而治之”的理念来实现的,其基本思想如下:

1. 分划:在array[low..high]中选择一个记录array[i]作为基准,记录array[i]将当前序列划分为左右两个子区间array[low..i-1]
        和array[i+1..high],且满足左子区间中所有记录的值都小于array[i]的值,右子区间中所有记录的值都大于array[i]的值,
        而array[i]就在它最终的位置上,无需参加之后的排序;
2. 求解:array[i]将当前序列划分为左右两个子区间array[low..i-1]和array[i+1..high],这两个子区间都可以进行单独的排序,所
        以就可以递归调用快速排序方法对左右两个子区间进行排序;
3. 组合:当两个子区间排序完成后,整个序列就完成了排序任务,这里的组合实际上不需要做任何操作。

快速排序主要是利用基准记录对序列进行分划,可见如何寻找基准记录是算法的关键。

示例:(49 38 65 97 76 13 27)

  1. 选取49为基准,从右到左找出第一个比49小的数与49位置调换,从左到右找出
    第一个比49大的数与49位置调换,i和j坐标不断向中间靠拢,直到i==j为止。
    27 38 65 97 76 13 49
    27 38 49 97 76 13 65
    27 38 13 97 76 49 65
    27 38 13 49 76 97 65

  2. 经过第一个排序循环,49被排在了它最终应有的位置上(左边的数都比49小,右边的数都比49大)。
    再次运用递归的方法,求出49两边的排序。

算法实现:

//分划算法(以最左端值array[i]为基准,不断左右比较后,求出它在最终序列上的下标位置)
int part(int *array, int i, int j) {
    int t=array[i];
    
    while (i<j) { //两端交替向中间扫描,直到i==j为止
        //从右向左扫描,找出第一个小于t的数
        while (i<j && array[j]>=t) {
            j--;
        }
        if (i<j) { //表示找到了这个数array[j]
            array[i] = array[j]; //这里”相当于“array[i]和array[j]交换位置
            i++;
        }
        
        //从左到右扫描,找出第一个大于t的数
        while (i<j && array[i]<=t) {
            i++;
        }
        if (i<j) { //表示找到了这个数array[i]
            array[j] = array[i];//这里”相当于“array[i]和array[j]交换位置
            j--;
        }
    }
    array[i] = t; //最后定位基准位置
    
    return i;
}

//快速排序法
void quick_sort(int * array, int low, int high) {
    if (low < high) {
        int mid = part(array, low, high);
        quick_sort(array, low, mid-1);
        quick_sort(array, mid+1, high);
    }
}

快速排序性能评价:

  • 时间复杂度:最好时间为O(nlog2Q),最坏时间为O(n^2),因此平均时间复杂度为O(nlog2n) [以2为底的对数];
  • 空间复杂度:最坏复杂度为O(n), 平均空间复杂度为O(nlog2n);
  • 稳定性:有算法的流程可知,冒泡排序是不稳定的,即原序列经排序后相同记录的相对位置可能发生改变;


直接选择排序

算法介绍:

算法的基本思想:
直接选择排序是选择排序的一种。算法对待排序的记录进行n-1次选择,第i次操作选择第i大的记录放在第i个位置上,即
每一次都将一个记录放到它的最终的位置上,这就是所谓的“各回各家”。

算法的基本流程:
1. array[0..n-1]无序;
2. 第一趟排序:在array[0..n-1]中找出最小的记录array[t],将它与array[0]交换,此时array[0..0]有序,array[1..n-1]无序;
3. 重复2,...
4. 如此,n个记录经过n-1趟直接选择排序就可以得到有序序列。

算法实现:

void select_sort(int *array, int n) {
    int k; //记录每趟扫描的最小值
    int t;
    
    for (int i=0; i<n-1; i++) {
        k = i;
        for (int j=i+1; j<n; j++) {
            if (array[j] < array[k]) {
                k = j; //记录当前最小值的位置
            }
        }
        if (k!=i) {
            t = array[k];
            array[k] = array[i];
            array[i] = t;
        }
    }
}

选择排序性能评价:

  • 时间复杂度:选择排序的最大优点就是赋值次数少,关键字比较的次数均是n(n-1)/2,因此时间复杂度为O(n^2);
  • 空间复杂度:直接选择排序为就地排序,空间复杂度为O(1);
  • 稳定性:直接选择排序是不稳定的。


直接插入排序

算法基本思想:
直接插入排序是插入排序的一种,记录array[0..i-1]已经排好顺序,即array[0]<array[1]<...<array[i-1],
将待排序记录array[i]插入到上述序列中的适当位置,使得新的序列也有顺序。

算法基本流程:
1. 待排序数组array[0..n-1],初始时,array[0]自成有序区,array[1..n-1]无序;
2. 第一趟直接插入排序:将array[1]与array[0]比较,若array[1]<array[0],则将array[0]向后移动一个位置,
   腾出位置插入array[1],否则将array[i]插入到array[0]的后面;
3. 重复2,...
4. 按如上方法插入,直到将array[n-1]插入到有序序列array[0..n-2]中。

算法实现:

void insert_sort(int *array, int n) {
    int temp;
    int j;
    
    for (int i=1; i<n; i++) {
        if (array[i] < array[i-1]) {
            temp = array[i];
            
            for (j=i-1; temp<array[j]; j--) {
                array[j+1] = array[j];
            }
            
            array[j+1] = temp;
        }
    }
}

直接插入排序性能评价:
直接插入法优点是算法执行过程比较清晰,容易实现,当待排序记录的数量很小时,宜使用该方法,但当记录很大时,不易使用该方法。

  • 时间复杂度:时间复杂度为O(n^2);
  • 空间复杂度:空间复杂度为O(1);
  • 稳定性:稳定。


希尔排序法

算法的基本思想:
希尔排序是对直接插入排序的一种改进方法,其基本思想是,把记录按下表的一定增量进行分组,对每一组使用直接插入排序法,随着增量的逐渐
减少,组内的记录越来越多,直至增量减至1时,整个序列正好被分成一个组,算法结束。

算法的基本实现:
1. 序列array[0..n-1]无序,取一个小于n的整数d1为第一个增量,将距离d1的倍数的记录放到同一个组中,在组内进行直接插入排序;
2. 取第二个增量d2<d1,重复上面的分组和排序,直到取到的增量dm=1(其中d1>d2>..dm),即所有的记录都放在同一组中进行直接插入排序为止。

算法的实现:

void shell_insert(int *array, int step, int n) {
    int j;
    int temp;
    
    for (int i=step; i<n; i++) {
        if (array[i] < array[i-step]) {
            temp = array[i];
            
            for (j=i-step; j>=0 && temp<array[j]; j-=step) {
                array[j+step] = array[j];
            }
            array[j+step] = temp;
        }
    }
}

void shell_sort(int *array, int n) {
    int addition = n;//开始的增量
    while (addition > 1) {
        addition = addition/3+1; //求下一个增量
        shell_insert(array, addition, n);
    }
}

直接插入排序性能评价:
直接插入法优点是算法执行过程比较清晰,容易实现,当待排序记录的数量很小时,宜使用该方法,但当记录很大时,不易使用该方法。

  • 时间复杂度:对希尔排序的时间复杂性进行分析是个复杂的问题,因为它的时间是所取增量序列的函数,希尔排序渐进增量可选如下任意的 自然序列:n>dm>dm-1>…d2>d1=1,渐进增量的取法可以有很多种,但需要注意的是,应使渐减增量序列中没有除了1以外的公因子,并且 最后一个增量值必须为1,当n不太大时,使用直接插入排序比较好;
  • 空间复杂度:空间复杂度为O(1);
  • 稳定性:不稳定。


堆排序算法:

归并排序算法:


排序方法小结

  • 当待排序的序列基本有序时,用直接排序最快,冒泡排序也较快,快速排序明显变慢。此外,直接插入排序最简单,当序列中的记录数量较小时, 他也是最佳的排序方法,因此也经常和其他算法一起使用,如快速排序、归并排序等;
  • 当待排序的记录数n较小时,可使用插入排序或选择排序,由于直接插入排序所需移动记录的操作比较多,因此当记录本身信息量较大时用简单 选择排序方法比较好;
  • 当n较大时,应采用时间复杂度为O(nlog2n)的快速排序、堆排序或者归并排序方法;
  • 改进的冒泡排序对输入是敏感的,类似于直接插入排序;快速排序对输入非常敏感;选择排序对输入不敏感;归并排序对输入不敏感;

Tags:


Share: