各种排序算法总结和比较

一.介绍

 排序算法可以说是一项基本功,解决实际问题中经常遇到,针对实际数据的特点选择合适的排序算法可以使程序获得更高的效率,有时候排序的稳定性还是实际问题中必须考虑的,这篇博客对常见的排序算法进行整理,包括:插入排序、选择排序、冒泡排序、快速排序、堆排序、归并排序、希尔排序、二叉树排序、计数排序、桶排序、基数排序。

1.比较排序和非比较排

常见的排序算法都是比较排序,非比较排序包括计数排序、桶排序和基数排序,非比较排序对数据有要求,因为数据本身包含了定位特征,所有才能不通过比较来确定元素的位置。

      比较排序的时间复杂度通常为O(n2)或者O(nlogn),比较排序的时间复杂度下界就是O(nlogn),而非比较排序的时间复杂度可以达到O(n),但是都需要额外的空间开销。

      比较排序时间复杂度为O(nlogn)的证明:

      a1,a2,a3……an序列的所有排序有n!种,所以满足要求的排序a1',a2',a3'……an'(其中a1'<=a2'<=a3'……<=an')的概率为1/n!。基于输入元素的比较排序,每一次比较的返回不是0就是1,这恰好可以作为决策树的一个决策将一个事件分成两个分支。比如冒泡排序时通过比较a1和a2两个数的大小可以把序列分成a1,a2……an与a2,a1……an(气泡a2上升一个身位)两种不同的结果,因此比较排序也可以构造决策树。根节点代表原始序列a1,a2,a3……an,所有叶子节点都是这个序列的重排(共有n!个,其中有一个就是我们排序的结果a1',a2',a3'……an')。如果每次比较的结果都是等概率的话(恰好划分为概率空间相等的两个事件),那么二叉树就是高度平衡的,深度至少是log(n!)。

      又因为 1. n! < nn ,两边取对数就得到log(n!)<nlog(n),所以log(n!) = O(nlogn).

                2. n!=n(n-1)(n-2)(n-3)…1 > (n/2)^(n/2) 两边取对数得到 log(n!) > (n/2)log(n/2) = Ω(nlogn),所以 log(n!) = Ω(nlogn)。

      因此log(n!)的增长速度与 nlogn 相同,即 log(n!)=Θ(nlogn),这就是通用排序算法的最低时间复杂度O(nlogn)的依据。

2.排序的稳定性和复杂度

不稳定:

选择排序(selection sort)— O(n2)

      快速排序(quicksort)— O(nlogn) 平均时间, O(n2) 最坏情况; 对于大的、乱序串列一般认为是最快的已知排序

      堆排序 (heapsort)— O(nlogn)

      希尔排序 (shell sort)— O(nlogn)

      基数排序(radix sort)— O(n·k); 需要 O(n) 额外存储空间 (K为特征个数)

稳定:

      插入排序(insertion sort)— O(n2)

      冒泡排序(bubble sort) — O(n2)

      归并排序 (merge sort)— O(n log n); 需要 O(n) 额外存储空间

      二叉树排序(Binary tree sort) — O(nlogn); 需要 O(n) 额外存储空间

      计数排序  (counting sort) — O(n+k); 需要 O(n+k) 额外存储空间,k为序列中Max-Min+1

      桶排序 (bucket sort)— O(n); 需要 O(k) 额外存储空间

二.每种排序的原理和实现

1.插入排序

遍历数组,遍历到i时,a0,a1...ai-1是已经排好序的,取出ai,从ai-1开始向前和每个比较大小,如果小于,则将此位置元素向后移动,继续先前比较,如果不小于,则放到正在比较的元素之后。可见相等元素比较是,原来靠后的还是拍在后边,所以插入排序是稳定的。

      当待排序的数据基本有序时,插入排序的效率比较高,只需要进行很少的数据移动。
void insert_sort (int a[], int n) {
    int i,j,v;
    for (i=1; i<n; i++) {
      //如果第i个元素小于第j个,则第j个向后移动
        for (v=a[i], j=i-1; j>=0&&v<a[j]; j--)
            a[j+1]=a[j];
        a[j+1]=v;
    }
}

2.折半插入排序

基本思想既然每个要插入记录之前的纪录已经按关键字有序排列,在查找插入位置时就没有必要逐个关键字比较,可以使用折半查找来实现。由此进行的插入排序称之为折半插入排序。
void BInsertSort(int a[],int n)
{
    int i,j,v,low,high,mid;
    for(i = 1;i<n;i++)
    {
        low = 0;high = i-1;//查找范围由0到i-1
        v=a[i];
        while(low<=high){
            mid = (low+high)/2;
            if(v<a[mid]) high = mid -1;
            else low =mid+1;
        }//折半查找
        for(j=i-1;j>=high+1;j--)
        { 
             a[j+1]=a[j];
        }
        a[j+1] = v;
    }
}

3.2-路插入排序

通过一个辅助的循环数组, 如果大于最大的元素, 则插入至尾部, 如果小于最小的元素, 则插入至头部, 

如果在两者之间, 采用折半查找的方式,移动一部分的元素;

设计到循环数组的中间值的查找和数据移动的问题.

由于折半查找可以减少比较次数, 

首尾插入又不需要移动元素, 即移动次数约为[(n^2)/8], 有1/2的元素不需要移动, 每次移动(1/2*1/2), 所以移动次数为[(n^2)/8];

但是需要n个辅助空间, 时间复杂度扔是O(n^2);
//#include<deque>  双向队列 
void TwoInsertSort (deque<int>& L){
    int first = 0,final = 0;
    int n = L.size();
    deque<int> D(n);
    D[0] = L[0];
    for (int i = 1; i < n;i++){
        if(L[i]<D[first]){
            first = (first -1 +n)%n;
        D[first] = L[i];
        }
        else if(L[i] > D[final]) { //大于最大元素
			final = (final+1+n)%n;
			D[final] = L[i];
        }
        else {
            //折半查找 
            int low = 0, high = (final - first)%n;
            int temp = first,end = high;
            while(low<=high){
				int m = (low + high)/2;
				if (L[i] < D[(m+temp)%n])
					high = m-1;
				else
					low = m+1;
			}
			for (int j=end; j>=high+1; --j) {
				D[(j+temp+1)%n] = D[(j+temp)%n];
			}
			D[(high+temp+1)%n] = L[i];
			final = (final+1+n)%n;
            }
        }
        //复制数组
        for(int i = 0;i<n;i++){
            L[i]=D[(first+i)%n];
        }
    }

4.希尔排序

希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序,同时该算法是冲破O(n2)的第一批算法之一。
老师说这个复杂度低,是每次处理后,下轮排序需要涉及的数据范围就减少一半(另一半不影响这一半的排序)
充分利用了插入排序的优点
//代码1
void shellSort(int a[],int n)
{
    int gap,i,j,t,k; 
    for(int gap = n/2;gap>0;gap /= 2){
       for (i= 0;i<gap;i++){
            for(j=i+gap;j<n;j+=gap){
                if(a[j]<a[j-gap])
				{
					for(t = a[j],k=j-gap;k>=0&&t<a[k];k-=gap)
                    {
                        a[k+gap]=a[k];
                    }
                    a[k+gap]=t;
				}
            }
    }
    }
}
//代码2 跟上面的差不多
void shell_sort(int a[], int n)
{
    int d, i, j, temp; //d为增量
    for(d = n/2;d >= 1;d = d/2) //增量递减到1使完成排序
    {
        for(i = d; i < n;i++)   //插入排序的一轮
        {
            temp = a[i];
            for(j = i - d;(j >= 0) && (a[j] > temp);j = j-d)
            {
                a[j + d] = a[j];
            }
        a[j + d] = temp;
        }
    }
}

5.奇偶交换排序

奇偶交换排序是另一种交换排序。它的第一趟对序列中的所有奇数项i扫描,第二趟对序列中的所有偶数项i扫描。
若A[i] > A[i+1],则交换它们。第三趟有对所有的奇数项,第四趟对所有的偶数项,…,如此反复,直到整个序列全部排好序为止。
奇偶排序过程如图

代码
void swapSort(int array[],int n){
   int i,j;
   bool flag = true;
       while(flag) {
			flag = false ;
			for(int i=1;i<n;i+=2) {
				if(array[i] > array[i+1]) {
					swap(array[i],array[i+1]) ;
					flag = true ;
				}
			}
			for(int i=0;i<n;i+=2) {
				if( array[i] > array[i+1]) {
					swap(array[i],array[i+1]) ;
					flag = true ;
				}
			}		
   }
   cout << "swapSort: ";
   print(array,n) ;
}

6.表插入排序

7.冒泡排序

  冒泡排序的名字很形象,实际实现是相邻两节点进行比较,大的向后移一个,经过第一轮两两比较和移动,最大的元素移动到了最后,第二轮次大的位于倒数第二个,依次进行。这是最基本的冒泡排序,还可以进行一些优化。

      优化一:如果某一轮两两比较中没有任何元素交换,这说明已经都排好序了,算法结束,可以使用一个Flag做标记,默认为false,如果发生交互则置为true,每轮结束时检测Flag,如果为true则继续,如果为false则返回。

      优化二:某一轮结束位置为j,但是这一轮的最后一次交换发生在lastSwap的位置,则lastSwap到j之间是排好序的,下一轮的结束点就不必是j--了,而直接到lastSwap即可,代码如下:
int bubble_sort(int a[],int n){
    int exchange = 1;
    int i=1,j;
    while(i<n&&exchange){
        exchange = 0;
        for(j = n -1;j>=i;j--){
            if(a[j-1]>a[j]){
                swap(a[j-1],a[j]);
                exchange = 1;
            }
            
        }
        i++;
    }
}
//[拓展]双向冒泡排序
void bubble_sort3(int a[],int n){
   int low = 0,high = n - 1;
   int i, j;
   bool flag = true;
   while(low<high && flag){
       flag = false;
       for (i=low;i< high;i++)
       {
           if(a[i]>a[i+1]){
               swap(a[i],a[i+1]);
               flag = true;
           }
       }
       high--;
       for(j = high;j>low;j--){
           if(a[j-1]>a[j]){
               swap(a[j-1],a[j]);
               flag = true;
           }
       }
        low++;
   } 
//优化二
void bubble_sort2(int a[],int n)
{
    int i, j, lastSwap, tmp;
    bool flag = 1;
    for (j=n-1; j>0&&flag; j=lastSwap) {
        flag = 0;
        lastSwap=0;     //每一轮要初始化为0,防止某一轮未发生交换,lastSwap保留上一轮的值进入死循环
        for (i=0; i<j; i++) {
            if (a[i] > a[i+1]) {
                flag = 1;
                swap(a[i],a[i+1]);
                lastSwap = i; //最后一次交换位置的坐标
            }
        }
    }
}

9.选择排序

遍历数组,遍历到i时,a0,a1...ai-1是已经排好序的,然后从i到n选择出最小的,记录下位置,如果不是第i个,则和第i个元素交换。此时第i个元素可能会排到相等元素之后,造成排序的不稳定。

void selection_sort (int a[], int n) {
    int i,j,pos,tmp;
    for (i=0; i<n-1; i++) {
      //寻找最小值的下标
        for (pos=i, j=i+1; j<n; j++)
            if (a[pos]>a[j])
                pos=j;
        if (pos != i) {
            tmp=a[i];
            a[i]=a[pos];
            a[pos]=tmp;
        }
    }
}

树形选择排序(锦标赛排序)

基本思想是:如果a>b,b>c则a>c即关系的可传递性,来减少关键字比较的次数。
树形选择排序的缺点是:需要辅助存储空间较多,和最大值作多余的比较等等。

4.快速排序

//对冒泡排序的一种改进
  快速排序首先找到一个基准,下面程序以第一个元素作为基准(pivot),然后先从右向左搜索,如果发现比pivot小,则和pivot交换,然后从左向右搜索,如果发现比pivot大,则和pivot交换,一直到左边大于右边,此时pivot左边的都比它小,而右边的都比它大,此时pivot的位置就是排好序后应该在的位置,此时pivot将数组划分为左右两部分,可以递归采用该方法进行(快速排序的趟数取决于递归树的高度)。快排的交换使排序成为不稳定的。
int mpartition(int a[], int low, int high) {
    int pivot = a[low];

    while (low<high) {
        while (low<high && pivot<=a[high]) high--;
        if (low<high) a[low++]=a[high];
        while (low<high && pivot>a[low]) low++;
        if (low<high) a[high--]=a[low];
    }
    a[low]=pivot;
    return low;
}
void quick_sort (int a[], int low, int high) {

    if (low < high) {
        int q = mpartition(a, low, high);
        quick_sort(a, low, q-1);
        quick_sort(a, q+1, high);
    }
}
代码2:
void quicksort(int left, int right) {
	int i, j, t, temp;
	if(left > right)
		return;
    temp = a[left]; //temp中存的就是基准数
    i = left;
    j = right;
    while(i != j) { //顺序很重要,要先从右边开始找
    	while(a[j] >= temp && i < j)
    		j--;
    	while(a[i] <= temp && i < j)//再找右边的
    		i++;       
    	if(i < j)//交换两个数在数组中的位置
    	{
    		t = a[i];
    		a[i] = a[j];
    		a[j] = t;
    	}
    }
    //最终将基准数归位
    a[left] = a[i];
    a[i] = temp;
    quicksort(left, i-1);//继续处理左边的,这里是一个递归的过程
    quicksort(i+1, right);//继续处理右边的 ,这里是一个递归的过程
}

5.堆排序 重点

       堆排序是把数组看作堆,第i个结点的孩子结点为第2*i+1和2*i+2个结点(不超出数组长度前提下),堆排序的第一步是建堆,然后是取堆顶元素然后调整堆。建堆的过程是自底向上不断调整达成的,这样当调整某个结点时,其左节点和右结点已经是满足条件的,此时如果两个子结点不需要动,则整个子树不需要动,如果调整,则父结点交换到子结点位置,再以此结点继续调整。

      下述代码使用的大顶堆,建立好堆后堆顶元素为最大值,此时取堆顶元素即使堆顶元素和最后一个元素交换,最大的元素处于数组最后,此时调整小了一个长度的堆,然后再取堆顶和倒数第二个元素交换,依次类推,完成数据的非递减排序。

      堆排序的主要时间花在初始建堆期间,建好堆后,堆这种数据结构以及它奇妙的特征,使得找到数列中最大的数字这样的操作只需要O(1)的时间复杂度,维护需要logn的时间复杂度。堆排序不适宜于记录数较少的文件

排序动画过程解释
首先,将所有的数字存储在堆中
按大顶堆构建堆,其中大顶堆的一个特性是数据将被从大到小取出,将取出的数字按照相反的顺序进行排列,数字就完成了排序
在这里数字 5 先入堆
数字 2 入堆
数字 7 入堆, 7 此时是最后一个节点,与最后一个非叶子节点(也就是数字 5 )进行比较,由于 7 大于 5 ,所以 7 和 5 交互
按照上述的操作将所有数字入堆,然后从左到右,从上到下进行调整,构造出大顶堆
入堆完成之后,将堆顶元素取出,将末尾元素置于堆顶,重新调整结构,使其满足堆定义
堆顶元素数字 7 取出,末尾元素数字 4 置于堆顶,为了维护好大顶堆的定义,最后一个非叶子节点数字 5 与 4 比较,而后交换两个数字的位置
反复执行调整+交换步骤,直到整个序列有序


/* 
 * (最大)堆的向下调整算法
 *
 * 注:数组实现的堆中,第N个节点的左孩子的索引值是(2N+1),右孩子的索引是(2N+2)。
 *     其中,N为数组下标索引值,如数组中第1个数对应的N为0。
 *
 * 参数说明:
 *     a -- 待排序的数组
 *     start -- 被下调节点的起始位置(一般为0,表示从第1个开始)
 *     end   -- 截至范围(一般为数组中最后一个元素的索引)
 */
void maxHeapDown(int* a, int start, int end)
{
    int c = start;            // 当前(current)节点的位置
    int l = 2*c + 1;        // 左(left)孩子的位置
    int tmp = a[c];            // 当前(current)节点的大小
    for (; l <= end; c=l,l=2*l+1)
    {
        // "l"是左孩子,"l+1"是右孩子
        if ( l < end && a[l] < a[l+1])
            l++;        // 左右两孩子中选择较大者,即m_heap[l+1]
        if (tmp >= a[l])
            break;        // 调整结束
        else            // 交换值
        {
            a[c] = a[l];
            a[l]= tmp;
        }
    }
}

/*
 * 堆排序(从小到大)
 *
 * 参数说明:
 *     a -- 待排序的数组
 *     n -- 数组的长度
 */
void heapSortAsc(int* a, int n)
{
    int i,tmp;

    // 从(n/2-1) --> 0逐次遍历。遍历之后,得到的数组实际上是一个(最大)二叉堆。
    for (i = n / 2 - 1; i >= 0; i--)
        maxHeapDown(a, i, n-1);

    // 从最后一个元素开始对序列进行调整,不断的缩小调整的范围直到第一个元素
    for (i = n - 1; i > 0; i--)
    {
        // 交换a[0]和a[i]。交换后,a[i]是a[0...i]中最大的。
        tmp = a[0];
        a[0] = a[i];
        a[i] = tmp;
        // 调整a[0...i-1],使得a[0...i-1]仍然是一个最大堆。
        // 即,保证a[i-1]是a[0...i-1]中的最大值。
        maxHeapDown(a, 0, i-1);
    }
}

/* 
 * (最小)堆的向下调整算法
 *
 * 注:数组实现的堆中,第N个节点的左孩子的索引值是(2N+1),右孩子的索引是(2N+2)。
 *     其中,N为数组下标索引值,如数组中第1个数对应的N为0。
 *
 * 参数说明:
 *     a -- 待排序的数组
 *     start -- 被下调节点的起始位置(一般为0,表示从第1个开始)
 *     end   -- 截至范围(一般为数组中最后一个元素的索引)
 */
void minHeapDown(int* a, int start, int end)
{
    int c = start;            // 当前(current)节点的位置
    int l = 2*c + 1;        // 左(left)孩子的位置
    int tmp = a[c];            // 当前(current)节点的大小
    for (; l <= end; c=l,l=2*l+1)
    {
        // "l"是左孩子,"l+1"是右孩子
        if ( l < end && a[l] > a[l+1])
            l++;        // 左右两孩子中选择较小者
        if (tmp <= a[l])
            break;        // 调整结束
        else            // 交换值
        {
            a[c] = a[l];
            a[l]= tmp;
        }
    }
}

/*
 * 堆排序(从大到小)
 *
 * 参数说明:
 *     a -- 待排序的数组
 *     n -- 数组的长度
 */
void heapSortDesc(int* a, int n)
{
    int i,tmp;

    // 从(n/2-1) --> 0逐次遍历每。遍历之后,得到的数组实际上是一个最小堆。
    for (i = n / 2 - 1; i >= 0; i--)
        minHeapDown(a, i, n-1);

    // 从最后一个元素开始对序列进行调整,不断的缩小调整的范围直到第一个元素
    for (i = n - 1; i > 0; i--)
    {
        // 交换a[0]和a[i]。交换后,a[i]是a[0...i]中最小的。
        tmp = a[0];
        a[0] = a[i];
        a[i] = tmp;
        // 调整a[0...i-1],使得a[0...i-1]仍然是一个最小堆。
        // 即,保证a[i-1]是a[0...i-1]中的最小值。
        minHeapDown(a, 0, i-1);
    }
}
题解2:
//假定对某一个节点i其左,右子树都是都是最大堆,但是对于节点i和它的左右子节点则可能破坏最大堆的性质,我们来写一个函数对这
//情况下的堆来进行维护使整体的堆满足最大堆性质

#define left(x) 2*x+1;//获得左节点在数组中的下标
#define right(x) 2*(x+1);//获得右节点在数组中的下标
void MaxHeapify(int* a,int i,int low,int high)//输入为要被排序的数组和根节点,数组a当中被维护的那一部分的下标low,high
{
    int l = left(i);//计算下标为i的节点的左子节点
    int r = right(i);//计算下标为i的节点的右子节点
    int largest;//保存i,l,r(即i和它的左右子节点)之间的最大数的下标
    int temp;//交互数组中的数所使用的临时变量
    //找到三个数当中最大的那个数,将最大的那个数和i进行互换
    if (l<=high && a[l]>a[i])
    {
        largest = l;
    }
    else{
        largest = i;
    }

    if (r<=high && a[r]>a[largest])
    {
        largest = r;
    }
    if (largest != i)
    {
        temp = a[i];
        a[i] = a[largest];
        a[largest] = temp;
        MaxHeapify(a, largest,low,high);//交换有可能破坏子树的最大堆性质,所以对所交换的那个子节点进行一次维护,而未交换的那个子节点,根据我们的假设,是保持着最大堆性质的。
    }
}
//将数组建立为一个最大堆
//调整数组当中数的位置将其处理为一个最大堆
void BuildMaxHeap(int* a,int length)
{
    for (int i = length / 2-1; i >= 0; i--)
    {
        MaxHeapify(a, i, 0, length - 1);
    }
}
//堆排序函数
void HeapSort(int a[],int length)
{
    int temp;
    BuildMaxHeap(a,length);
    for (int i = length - 1; i >= 1; i--)
    {
        //交换根节点和数组的最后一个节点
        temp = a[i];
        a[i] = a[0];
        a[0] = temp;
        MaxHeapify(a, 0, 0, i-1);//维护从下标为i-1到0的子数组
    }
}
备注 宏参数

6.归并排序

基本思想:分而治之
 归并排序是稳定排序,它也是一种十分高效的排序,能利用完全二叉树特性的排序一般性能都不会太差。

void mergearray(int a[], int first, int mid, int last, int temp[])
{
    int i = first, j = mid + 1;
    int m = mid,   n = last;
    int k = 0;

    while (i <= m && j <= n)
    {
        if (a[i] <= a[j])
            temp[k++] = a[i++];
        else
            temp[k++] = a[j++];
    }

    while (i <= m)
        temp[k++] = a[i++];

    while (j <= n)
        temp[k++] = a[j++];

    for (i = 0; i < k; i++)
        a[first + i] = temp[i];
}
void merge_sort(int a[], int first, int last, int temp[])
{
    if (first < last)
    {
        int mid = (first + last) / 2;
        merge_sort(a, first, mid, temp);    //左边有序
        merge_sort(a, mid + 1, last, temp); //右边有序
        mergearray(a, first, mid, last, temp); //再将二个有序数列合并
    }
}

7.希尔排序

    希尔排序是对插入排序的优化,基于以下两个认识:1. 数据量较小时插入排序速度较快,因为n和n2差距很小;2. 数据基本有序时插入排序效率很高,因为比较和移动的数据量少。

      因此,希尔排序的基本思想是将需要排序的序列划分成为若干个较小的子序列,对子序列进行插入排序,通过则插入排序能够使得原来序列成为基本有序。这样通过对较小的序列进行插入排序,然后对基本有序的数列进行插入排序,能够提高插入排序算法的效率。

      希尔排序的划分子序列不是像归并排序那种的二分,而是采用的叫做增量的技术,例如有十个元素的数组进行希尔排序,首先选择增量为10/2=5,此时第1个元素和第(1+5)个元素配对成子序列使用插入排序进行排序,第2和(2+5)个元素组成子序列,完成后增量继续减半为2,此时第1个元素、第(1+2)、第(1+4)、第(1+6)、第(1+8)个元素组成子序列进行插入排序。这种增量选择方法的好处是可以使数组整体均匀有序,尽可能的减少比较和移动的次数,二分法中即使前一半数据有序,后一半中如果有比较小的数据,还是会造成大量的比较和移动,因此这种增量的方法和插入排序的配合更佳。

      希尔排序的时间复杂度和增量的选择策略有关,上述增量方法造成希尔排序的不稳定性。
void shell_sort(int a[], int n)
{
    int d, i, j, temp; //d为增量
    for(d = n/2;d >= 1;d = d/2) //增量递减到1使完成排序
    {
        for(i = d; i < n;i++)   //插入排序的一轮
        {
            temp = a[i];
            for(j = i - d;(j >= 0) && (a[j] > temp);j = j-d)
            {
                a[j + d] = a[j];
            }
        a[j + d] = temp;
        }
    }
}

8.二叉树排序

    二叉树排序法借助了数据结构二叉排序树,二叉排序数满足三个条件:(1)若左子树不空,则左子树上所有结点的值均小于它的根结点的值; (2)若右子树不空,则右子树上所有结点的值均大于它的根结点的值; (3)左、右子树也分别为二叉排序树。根据这三个特点,用中序遍历二叉树得到的结果就是排序的结果。

      二叉树排序法需要首先根据数据构建二叉排序树,然后中序遍历,排序时间复杂度为O(nlogn),构建二叉树需要额外的O(n)的存储空间,有相同的元素是可以设置排在后边的放在右子树,在中序变量的时候也会在后边,所以二叉树排序是稳定的。

      在实现此算法的时候遇到不小的困难,指针参数在函数中无法通过new赋值,后来采用取指针地址,然后函数设置BST** tree的方式解决。
int arr[] = {7, 8, 8, 9, 5, 16, 5, 3,56,21,34,15,42};

struct BST{
    int number; //保存数组元素的值
    struct BST* left;
    struct BST* right;
};

void insertBST(BST** tree, int v) {
    if (*tree == NULL) {
        *tree = new BST;
        (*tree)->left=(*tree)->right=NULL;
        (*tree)->number=v;
        return;
    }
    if (v < (*tree)->number)
        insertBST(&((*tree)->left), v);
    else
        insertBST(&((*tree)->right), v);
}

void printResult(BST* tree) {
    if (tree == NULL)
        return;
    if (tree->left != NULL)
        printResult(tree->left);
    cout << tree->number << "  ";
    if (tree->right != NULL)
        printResult(tree->right);
}

void createBST(BST** tree, int a[], int n) {
    *tree = NULL;
    for (int i=0; i<n; i++)
        insertBST(tree, a[i]);
}

int main()
{
    int n = sizeof(arr)/sizeof(int);

    BST* root;
    createBST(&root, arr, n);
    printResult(root);

}

8.计数排序

  如果通过比较进行排序,那么复杂度的下界是O(nlogn),但是如果数据本身有可以利用的特征,可以不通过比较进行排序,就能使时间复杂度降低到O(n)。

      计数排序要求待排序的数组元素都是 整数,有很多地方都要去是0-K的正整数,其实负整数也可以通过都加一个偏移量解决的。

      计数排序的思想是,考虑待排序数组中的某一个元素a,如果数组中比a小的元素有s个,那么a在最终排好序的数组中的位置将会是s+1,如何知道比a小的元素有多少个,肯定不是通过比较去觉得,而是通过数字本身的属性,即累加数组中最小值到a之间的每个数字出现的次数(未出现则为0),而每个数字出现的次数可以通过扫描一遍数组获得。

      计数排序的步骤:

找出待排序的数组中最大和最小的元素(计数数组C的长度为max-min+1,其中位置0存放min,依次填充到最后一个位置存放max)
统计数组中每个值为i的元素出现的次数,存入数组C的第i项
对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加)
反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1(反向填充是为了保证稳定性)
      以下代码中寻找最大和最小元素参考编程之美,比较次数为1.5n次。

      计数排序适合数据分布集中的排序,如果数据太分散,会造成空间的大量浪费,假设数据为(1,2,3,1000000),这就需要1000000的额外空间,并且有大量的空间浪费和时间浪费。
int getNumInPos(int num,int pos) //获得某个数字的第pos位的值
{
    int temp = 1;
    for (int i = 0; i < pos - 1; i++)
        temp *= 10;

    return (num / temp) % 10;
}

#define RADIX_10 10    //十个桶,表示每一位的十个数字
#define KEYNUM 5     //整数位数
void radix_sort(int* pDataArray, int iDataNum)
{
    int *radixArrays[RADIX_10];    //分别为0~9的序列空间
    for (int i = 0; i < RADIX_10; i++)
    {
        radixArrays[i] = new int[iDataNum];
        radixArrays[i][0] = 0;    //index为0处记录这组数据的个数
    }

    for (int pos = 1; pos <= KEYNUM; pos++)    //从个位开始到31位
    {
        for (int i = 0; i < iDataNum; i++)    //分配过程
        {
            int num = getNumInPos(pDataArray[i], pos);
            int index = ++radixArrays[num][0];
            radixArrays[num][index] = pDataArray[i];
        }

        for (int i = 0, j =0; i < RADIX_10; i++) //写回到原数组中,复位radixArrays
        {
            for (int k = 1; k <= radixArrays[i][0]; k++)
                pDataArray[j++] = radixArrays[i][k];
            radixArrays[i][0] = 0;
        }
    }
}

 posted on 2019-10-25 09:11  Jason1999  阅读(213)  评论(0编辑  收藏  举报