内部排序算法小结

   


                 【排序算法笔记】

 


// 排序算法的分类
// 1. 按照排序的原理:基于比较的排序;基于分配收集的排序
// 2. 按照平均时间复杂度:
    //(1)平方阶(O(n^2))排序
          一般称为简单排序,例如直接插入、直接选择和冒泡排序;

    //(2)线性对数阶(O(nlogn))排序
          如快速、堆和归并排序;

    //(3)O(n^1+£)阶排序
          £是介于0和1之间的常数,即0<£<1,如希尔排序;

    //(4)线性阶(O(n))排序(基于分配+收集操作的排序)
          如桶、计数和基数排序。


// 各种排序方法比较
// 简单排序中直接插入最好,快速排序最快,当文件为正序时,直接插入和冒泡均最佳。

// 影响排序效果的因素
// 因为不同的排序方法适应不同的应用环境和要求,所以选择合适的排序方法应综合考虑下列因素:
// ① 待排序的元素数目N(规模);
// ② 元素本身的复杂性(元素移动/交换的代价);
// ③ 关键字的结构及其初始状态;
// ④ 对稳定性的要求;
// ⑤ 语言工具的条件;
// ⑥ 存储结构(顺序存储/链式存储);
// ⑦ 时间和辅助空间复杂度等。

// 不同条件下,排序方法的选择

(1)若记录规模较小时(N较小),则可采用直接插入或直接选择排序。
   通常而言,直接选择排序移动的元素数要少于直接插人排序,因此,若移动元素代价较大时,直接选择排序更佳。

(2)若文件初始状态基本有序(指正序),则应选用直接插人、冒泡或随机的快速排序为宜;

(3)若记录规模较大(N很大),则应采用时间复杂度为O(nlogn)的排序方法:快速排序、堆排序或归并排序。
   快速排序是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短;
   堆排序所需的辅助空间少于快速排序,并且不会出现快速排序可能出现的最坏情况。这两种排序都是不稳定的。
   归并排序是稳定的,但是从单个元素起进行两两归并则不值得提倡,通常可以将它和直接插入排序结合在一起使用。
   先利用直接插入排序求得较长的有序子序列,然后再两两归并之。因为直接插入排序是稳定的,所以改进后的归并排序仍是稳定的。

(4)任何基于比较的排序算法,至少需要O(nlogn)的时间,
   任何基于分配的排序算法,例如计数排序,桶排序和基数排序则可能在线性O(n)时间内完成对n个记录的排序。
   但是,计数排序,桶排序和基数排序只适用于像字符串和整数这类有明显结构特征的关键字,
   当关键字的取值范围属于某个无穷集合时,无法使用计数排序,桶排序和基数排序。
   若n很大,记录的关键字位数较少且可以分解时,采用基数排序较好。

(5)有的语言(如Fortran,Cobol或Basic等)没有提供指针及递归,
   导致实现归并、快速(它们用递归实现较简单)和基数(使用了指针)等排序算法变得复杂。此时可考虑用其它排序。

(6)对排序算法而言,输人数据通常是存储在一个顺序存储的向量中。
   当元素属于重型对象时,为避免耗费大量的时间去移动元素,可以用链表作为存储结构。
   譬如插入排序、归并排序、基数排序都易于在链表上实现,使之减少记录的移动次数。
   但是快速排序和堆排序(它们要求随机访问元素),在链表上却难于实现,
   在这种情况下,可以提取关键字/索引建立间接索引表,然后再对间接索引表进行排序,
   此时交换的仅是索引/指针/关键字,而不再是元素本身,从而有效避免了重型元素的移动/交换开销。
  


"++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++"

 

内部排序算法的分析比较

//1.时间复杂度

  ① 直接插入、直接选择、冒泡排序算法的时间复杂度为(n^2);
  ② 快速、归并、堆排序算法的时间复杂度为O(nlog2n);
  ③ 希尔排序算法的时间复杂度很难计算,有几种较接近的答案:O(nlog2n)或O(n^1.25);
  ④ 基数排序算法的时间复杂度为O(d*(rd+n)),其中rd是基数,d是关键字的位数,n是元素个数。

//2.稳定性

  ① 直接插入、冒泡、归并和基数排序算法是稳定的;
  ② 直接选择、希尔、快速和堆排序算法是不稳定的。

//3.辅助空间(空间复杂度)

  ① 直接插入、直接选择、冒泡、希尔和堆排序算法需要辅助空间为O(1);
  ② 快速排序算法需要辅助空间为O(lgn);
  ③ 归并排序算法需要辅助空间为O(n);
  ④ 基数排序算法需要辅助空间为O(n+rd)。

//4.选取排序方法时需要考虑的主要因素有:

  ① 待排序的记录个数;
  ② 记录本身的大小和存储结构;
  ③ 关键字的分布情况;
  ④ 对排序稳定性的要求;
  ⑤ 时间和空间复杂度等。

//5.排序方法的选取

  ① 若待排序的一组记录数目n较小(如n≤50)时,可采用插入排序或选择排序。
  ② 若n较大时,则应采用快速排序、堆排序或归并排序。
  ③ 若待排序记录按关键字基本有序(正序或叫升序),则适宜选用直接插入排序、冒泡排序或快速排序。
  ④ 当n很大,而且关键字位数教少时,采用链式基数排序较好。
  ⑤ 关键字比较次数与记录的初始排列顺序无关的排序方法是选择排序。

//6.排序方法对记录存储方式的要求:

  ① 当记录本身信息量较大时,插入排序、归并排序、基数排序易于在链表上实现;
  ② 快速排序、堆排序更适合在索引结构上排序;
    ③ 一般的排序方法都是在顺序结构(一维数组)上实现。


"++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++"

 

 

template<typename T>
inline void myswap(T& a, T& b)
{
    if (&a != &b)
    {
        T temp = a;
        a = b;
        b = temp;

    //  a = a ^ b;
    //  b = a ^ b;
    //  a = a ^ b;

    //  a = a + b;
    //  b = a - b;
    //  a = a - b;

    }
}



/*
** 冒泡排序简介:
**
** 冒泡排序,是指计算机的一种排序方法,它的时间复杂度为O(n^2),
** 虽然不及堆排序、快速排序的O(nlogn,底数为2),但是有两个优点:
**
** 1.编程复杂度很低,很容易写出代码;
** 2.具有稳定性,这里的稳定性是指原序列中相同元素的相对顺序仍然保持到排序后的序列,而堆排序、快速排序均不具有稳定性。
**
** 不过,一路、二路归并排序、不平衡二叉树排序的速度均比冒泡排序快,且具有稳定性,但速度不及堆排序、快速排序。
** 冒泡排序是经过n-1趟子排序完成的,第i趟子排序从第1个数至第n-i个数,若第i个数比后一个数大(则升序,小则降序)则交换两数
** 由于在排序过程中总是小数往前放,大数往后放,相当于气泡往上升,所以称作冒泡排序。
**
** 冒泡排序法存在的不足及改进方法:
** 在排序过程中,执行完最后的排序后,虽然数据已全部排序完备,但程序无法判断是否完成排序,
** 为了解决这一不足,可设置一个标志单元flag,将其设置为OFF,表示被排序的表示是一个无序的表。
** 在每一排序开始时,检查此标志,若此标志为0,则结束排序;否则进行排序;
**
** 局部冒泡排序算法对冒泡排序的改进:
** 在冒泡排序中,一趟扫描有可能无数据交换,也有可能有一次或多次数据交换,
** 在传统的冒泡排序算法及近年来的一些改进的算法中,只记录一趟扫描有无数据交换的信息,
** 对数据交换发生的位置信息则不予处理。为了充分利用这一信息,可以在一趟全局扫描中,
** 对每一反序数据对进行局部冒泡排序处理,称之为局部冒泡排序。
** 局部冒泡排序与冒泡排序算法具有相同的时间复杂度,
** 并且在正序和逆序的情况下,所需的关键字的比较次数和移动次数完全相同。
** 由于局部冒泡排序和冒泡排序的数据移动次数总是相同的,
** 而局部冒泡排序所需关键字的比较次数常少于冒泡排序,
** 这意味着局部冒泡排序很可能在平均比较次数上对冒泡排序有所改进,
** 当比较次数较少的优点不足以抵消其程序复杂度所带来的额外开销,
** 而当数据量较大时,局部冒泡排序的时间性能则明显优于冒泡排序。
*/


//【冒泡排序】


template<typename T>
bool once_bubble(T arr[], int size)
{
    bool bSorted = true;

    for (int i=1; i<size; i++) //正向遍历
    {
        if (arr[i-1] > arr[i]) //严格反序才交换(使用>而不是>=),稳定性由此体现
        {
            bSorted = false;
            myswap(arr[i-1], arr[i]);
        }
    }

    return bSorted;
}


template<typename T>
void bubble_sort(T arr[], int size)
{
    for (int i=size; i>1; i--) //反向遍历
    {
        if (once_bubble(arr,i))
        {
            break;
        }
    }
}


/* 
** 鸡尾酒排序简介:
** 
** 鸡尾酒排序,也就是定向冒泡排序, 鸡尾酒搅拌排序, 搅拌排序, 
** 涟漪排序, 来回排序 or 快乐小时排序, 是冒泡排序的一种变形。
** 此算法与冒泡排序的不同处在于排序时是以双向在序列中进行排序。
** 
** 鸡尾酒排序等于是冒泡排序的轻微变形。
** 不同的地方在于从低到高然后从高到低,而冒泡排序则仅从低到高去比较序列里的每个元素。
** 他可以得到比冒泡排序稍微好一点的效能,原因是冒泡排序只从一个方向进行比对(由低到高),每次循环只移动一个项目。
** 以序列(2,3,4,5,1)为例,鸡尾酒排序只需要访问一次序列就可以完成排序,但如果使用冒泡排序则需要四次。
** 
** 鸡尾酒排序最糟或是平均所花费的次数都是O(n^2),但如果序列在一开始已经大部分排序过的话,会接近O(n)。
** 
*/ 

//【鸡尾酒排序】冒泡排序变形版

template<typename T>
void cocktail_sort(T arr[], int size)
{
    int bottom = 0, top = size - 1;

    while (true)
    {   
        bool bSorted = true;

        for (int i=bottom; i<top; i++) //正向冒泡
        {   
            if (arr[i] > arr[i+1])
            {   
                bSorted = false;
                myswap(arr[i], arr[i+1]);
            }
        }
        if (bSorted) break;

        top--; //正向冒泡令尾部一个元素就位

        bSorted = true;
        for (int i=top; i>bottom; i--) //逆向冒泡
        {   
            if (arr[i-1] > arr[i])
            {   
                bSorted = false;
                myswap(arr[i-1], arr[i]);
            }
        }

        if (bSorted) break;
        
        bottom++; //逆向冒泡令头部一个元素就位
    }
}



/*
** 选择排序简介:
**
** 每一趟从待排序的数据元素中选出最小(或最大)的一个元素,
** 顺序放在已排好序的数列的最后,直到全部待排序的数据元素排完。 
** 选择排序是否稳定跟冒泡排序一样,受实现影响。
*/


//选择排序


template<typename T>
bool once_select(T arr[], int size)
{
    bool bSorted = true;
    int max = 0;

    for (int i=1; i<size; i++) //正向遍历迭代出最大的值
    {
        if (arr[i] >= arr[max]) max = i;  // 稳定性由此体现

        else bSorted = false;
    }

    //将最大值放置在最后
    myswap(arr[max], arr[size-1]);

    return bSorted;
}


template<typename T>
void selection_sort(T arr[], int size)
{
    for (int i=size; i>1; i--) //反向遍历
    {
        if (once_select(arr,i))
        {
            break;
        }
    }
}

//冒泡排序和选择排序的前端是一样的,所不同的是两者使一个元素就绪的方式不一样。


/* 
** 直接插入排序简介:
** 
** 插入排序类似玩牌时整理手中纸牌的过程。
** 每步将一个待排序的记录按其关键字的大小插到前面已经排序的序列中的适当位置,
** 使有序表仍然有序,直到全部记录插入完毕为止。
** 
** 第一趟比较前两个数,然后把第二个数按大小插入到有序表中; 
** 第二趟把第三个数据与前两个数从前向后扫描,把第三个数按大小插入到有序表中;
** 依次进行下去,进行了(n-1)趟扫描以后就完成了整个排序过程。
** 
** 直接插入排序属于稳定的排序,时间复杂性为o(n^2),空间复杂度为O(1)。
** 
** 值得注意的是,我们必需用一个存储空间来保存当前待比较的数值,
** 因为当一趟比较完成时,我们要将待比较数值置入比它小的数值的后一位。
*/ 


//(直接)插入排序


template<typename T>
void once_insert(T arr[], int size, T key) //第三个参数最好使用值传递
{
    for (int i=size-1; i>=0&&arr[i]>key; i--) //稳定性体现
    {
        arr[i+1] = arr[i]; //向后腾挪一个位置
    }

    arr[i+1] = key; //目标位置必须是i+1
}

template<typename T>
void insertion_sort(T arr[], int size)
{
    for (int i=1; i<size; i++)
    {
        once_insert(arr,i,arr[i]);
    }
}


/* 
** 希尔排序(Shell sort)简介:
** 
** 希尔排序是不稳定的
** 
** 希尔排序通过将比较的全部元素分为几个区域来提升插入排序的性能。
** 这样可以让一个元素可以一次性地朝最终位置前进一大步。
** 然后算法再取越来越小的步长进行排序,算法的最后一步就是普通的插入排序,
** 但是到了这步,需排序的数据几乎是已排好的了(此时插入排序较快)。
** 
** 步长的选择是希尔排序的重要部分。只要最终步长为1任何步长序列都可以工作。
** 算法最开始以一定的步长进行排序。然后会继续以一定步长进行排序,最终算法以步长为1进行排序。
** 当步长为1时,算法变为插入排序,这就保证了数据一定会被排序。
** Donald Shell 最初建议步长选择为 n/2 并且对步长取半直到步长达到 1,
** 但这样依然不是最优的,仍然存在减少平均时间和最差时间的余地。 
** 
** 已知的最好步长序列由Marcin Ciura设计(1,4,10,23,57,132,301,701,1750,…) 
** 这项研究也表明:比较在希尔排序中是最主要的操作,而不是交换。
** 用这样步长序列的希尔排序比插入排序和堆排序都要快,甚至在小数组中比快速排序还快,
** 但是在涉及大量数据时希尔排序还是比快速排序慢。
*/ 

//希尔排序(分组插入排序)
//step参数含义:1.逻辑子序列的个数;2.逻辑子序列内相邻元素的间距。

template<typename T>
void inner_hier(T arr[], int size, int step) //子序列穿插排序
{
    for (int i = step; i < size; i++) //交叉扫描所有逻辑子序列的非首元素
    {
        T key = arr[i]; //缓存目标元素
        for (int j=i-step; j>=0 && arr[j]>key; j-=step) //反向遍历
        {
            arr[j+step] = arr[j]; //向后腾挪一个位置
        }
        arr[j+step] = key; //放置目标元素
    }
}


/*template<typename T>
void _inner_hier(T arr[], int size, int step) //子序列独立排序
{

    for (int i=0; i<step; i++) //针对每一个逻辑子序列都要做插入排序操作
    {
        for (int j=i+step; j<size; j+=step) //从逻辑子数组的第二个元素开始,均要做前向插入操作
        {
            T key = arr[j]; //缓存目标元素
            for (int k=j-step; k>=i && arr[k]>key; k-=step) //反向遍历
            {
                arr[k+step] = arr[k]; //向后腾挪一个位置
            }
            arr[k+step] = key; //放置目标元素
        }
    }
}*/


template<typename T>
void hier_sort(T arr[], int size)
{
    for (int step = size/3; step >= 1; step/=2) // 步长序列如何设计?
    {
        inner_hier(arr, size, step);
    }
}


//快速排序
//枢纽元(pivot)的选取: 三数(left, center, right)中值分割法(median3)
//以枢纽元为基准,分割数组元素为前后俩部分(S1 <= pivot <= S2)
//与枢纽元相等的元素分割策略:遇等即停

//三数分割中值法:将中值元素放置在第一个位置作为枢纽元。
template<typename T>
void median3(T arr[], int size) 
{   
    if (size <= 1) return;

    int low = 0, high = size-1, mid = size/2;

    //先将三数中最小者放置在mid位置
    if (arr[low] < arr[mid]) myswap(arr[low], arr[mid]);
    if (arr[high] < arr[mid]) myswap(arr[high], arr[mid]);
    
    //再将三数中最大者放置在high位置,中值自然落在low位置
    if (arr[high] < arr[low]) myswap(arr[high], arr[low]);
}

template<typename T>
int partition(T arr[], int size) //分割函数
{
    if (size == 1) return 0; //只有一个元素,则无须进行分割

    median3(arr, size);
    int pivot = 0, low = 1, high = size-1;
    
    while (true) //只有两个元素的情形下(low == high),partition能否正常工作[12; 22; 21]
    {
        //median3方法设置的首尾岗哨可以确保下面俩循环无须进行边界测试
        while (/*low <= high &&*/ arr[low]<arr[pivot]) low++;    //从头至尾跳过小于枢纽元的元素
        while (/*high >= low &&*/ arr[high]>arr[pivot]) high--;  //从尾至头跳过大于枢纽元的元素

        if (low < high) //low小于high才交换
        {
            myswap(arr[low++], arr[high--]);
        }
        //到达这儿,如果low等于high,则意味着arr[low]等于枢纽元
        else //low >= high 直接退出循环
        {
            break;
        }
    }

    myswap(arr[pivot], arr[high]);
    return high;
}


template<typename T>
void quick_sort(T arr[], int size)
{
    if (size <= 1) return; //递归终止条件

    int sep = partition(arr, size);

    quick_sort(arr, sep); //分割元素不参与递归排序
    quick_sort(arr+sep+1, size-sep-1);
}


// 增强版的快速排序
template<typename T>
void enh_quick_sort(T arr[], int size, int stack_depth = 1)
{
    if (size <= 1) return;  //递归终止条件1

    if (size <= 16 || stack_depth >= 16) //递归终止条件2
    {   
        bubble_sort(arr, size);
        return;
    }

    //划分前后两段(前段元素值均小于后段)
    int sep = partition(arr, size);

    //递归调用
    enh_quick_sort(arr, sep, stack_depth+1);
    enh_quick_sort(arr+sep+1, size-sep-1, stack_depth+1); //分隔元素不参与递归排序
}


//扩展 跟据快速排序的思想,寻找一个序列中第K小的元素,亦即第(size-K+1)大的元素。
template<typename T>
T FindKthMinElement(T arr[], int size, int Kth)
{   
    int sep = partition(arr, size);

    if (sep == Kth-1) //递归终结
    {   
        return arr[sep];
    }
    
    if (sep > Kth-1) // 第K小的元素在前段
    {   
        return FindKthMinElement(arr, sep, Kth);
    }
    else // sep < Kth-1 第K小的元素在后段
    {
        return FindKthMinElement(arr+sep+1, size-sep-1, Kth-sep-1);
    }
}



/* 
** 归并排序(Merge sort)简介:
** 
** 归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。
** 该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。
** 
** 归并操作(merge),也叫归并算法,指的是将两个已经排序的序列合并成一个序列的操作。归并排序算法依赖归并操作。
** 
** 算法描述:
** 
** 归并操作的过程如下:
** 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
** 设定两个指针,最初位置分别为两个已经排序序列的起始位置
** 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
** 重复步骤3直到某一指针达到序列尾
** 将另一序列剩下的所有元素直接复制到合并序列尾
*/ 



//【归并排序】标准版本

template<typename T>
void merge_sort_wrapper(const T arr[], T dest[], int size)
{
    T * temp = new T[size]; //临时空间的申请最好不要放在递归例程中
    merge_sort(arr, dest, temp, 0, size-1);

    delete [] temp;
}


template<typename T>
void merge_sort(const T arr[], T dest[], T temp[], int low, int high)
{
    if (low >= high) //递归终结情形
    {   
        dest[low] = arr[low];
        return;
    }

    int mid = low + (high-low)/2;  //防止算术溢出

    merge_sort(arr, temp, dest, low, mid);    //此处temp作为目标, 而dest作为临时
    merge_sort(arr, temp, dest, mid+1, high); //此处temp作为目标, 而dest作为临时

    inner_merge(temp, dest, low, mid, high);
}


//【内部归并】标准版本:以空间换时间

template<typename T>
void inner_merge(const T src[], T dest[], int low, int sep, int high) //分隔符元素划归前段区间所属
{
    int beg1 = low,   end1 = sep+1;   // [low, sep]
    int beg2 = sep+1, end2 = high+1;  // (sep, high]

    int index = low;
    while (beg1 < end1 && beg2 < end2)
    {   
        if (src[beg1] <= src[beg2]) // 保持算法稳定性
        {   
            dest[index++] = src[beg1++];
        }
        else // src[beg2] < src[beg1]
        {
            dest[index++] = src[beg2++];
        }
    }

    if (beg1 < end1)
    {   
        std::copy(src+beg1, src+end1, dest+index);
    }
    else // beg2 < end2
    {
        std::copy(src+beg2, src+end2, dest+index);
    }
}



//【归并排序】版本二:就地归并排序(空间复杂度:O(1))

template<typename T>
void inplace_merge_sort(T arr[], int size)
{
    if (size <= 1) return; //递归终止条件

    //前半段递归调用
    inplace_merge_sort(arr, size/2); 

    //后半段递归调用(注意:size-size/2 >= size/2)
    inplace_merge_sort(arr+size/2, size-size/2); 

    //合并前后两半段
    inner_inplace_merge(arr, size);
}


//【内部归并】版本二:就地归并(内部插入合并)

template<typename T>
void inner_inplace_merge(T arr[], int size)
{   
    for (int i=size/2; i < size; i++)
    {
        bool bSorted = true;

        T key = arr[i]; //缓存目标元素
        for (int j=i-1; j>=0 && arr[j]>key; j--)
        {   
            bSorted = false;
            arr[j+1] = arr[j];
        }

        //针对后半段有序的及时终止操作
        if (bSorted) break;

        arr[j+1] = key;
    }
}





//堆排序


/*

template<typename T>
void upward_adjust_heap(T heap[], int size, int pos);

template<typename T>
void downward_adjust_heap(T heap[], int size, int pos)

template<typename T>
void push_heap(T heap[], int size, const T& data);

template<typename T>
T pop_heap(T heap[], int size);

template<typename T>
void make_heap(T arr[], int size);

*/

template<typename T>
void heap_sort(T arr[], int size)
{
    make_heap(arr, size);

    for(int i=size; i>1; i--)
    {
        arr[i-1] = pop_heap(arr,i);
    }
}


//堆局部排序
//变体:寻找第K小的值;寻找最小的K个值

template<typename T>
void partial_sort(T arr[], int size, int num)
{
    if (num <=0 || num > size) return;
    
    make_heap(arr, num);

    for (int i=num; i<size; i++)
    {
        if (arr[i] < arr[0]) //如果当前元素值比堆顶元素还小,则互换。
        {   
            myswap(arr[i], arr[0]);
            downward_adjust_heap(arr, num, 0); //互换了之后再调整堆
        }       
    }

    for(int i=num; i>1; i--)
    {
        arr[i-1] = pop_heap(arr,i);
    }
}



/* 
** 间接排序简介:
** 
** 一般的排序算法都是基于顺序存储的线性表上进行的,因此当元素本身比较复杂时,
** 会导致元素间的移动/交换开销较大,从而间接影响排序算法的性能及效率,此时,
** 通常的方式有两种:一是对欲排序的数据使用链式存储取代顺序存储,另一种方式
** 就是接下来要介绍的间接排序,即排序时通过移动/交换元素的索引/指针来取代
** 元素本身的移动/交换,从而规避这种开销。
*/ 

template<typename T>
void index_sort(const T arr[], int index[], int size)
{
    for (int i=0; i<size; i++)
    {   
        index[i] = i;
    }

    //使用直接插入排序对元素索引进行排序
    for (int i=1; i<size; i++)
    {   
        int key = index[i];
        for (int j=i-1; j>=0 && arr[index[j]]>arr[key]; j--)
        {   
            index[j+1] = index[j]; //索引(而非元素)往后移
        }
        index[j+1] = key;
    }

    //至此可以根据索引对元素进行有序输出
    for (int i=0; i<size; i++)
    {   
        std::cout << arr[index[i]];
    }
}



//线性排序算法通常指的是基于分配系列的排序,主要有:计数排序,桶排序,基数排序。
//分配排序的基本思想:排序过程无须比较关键字,而是通过"分配"和"收集"过程来实现排序, 它们的时间复杂度可达到线性阶O(n)。


/* 
** 计数排序简介:
** 
** 插入,快速,合并,堆排序等基于比较的排序算法的最坏情况下界为Ω(nlogn),最坏情况下都要进行Ω(nlogn)次比较。
** 假设有一n个元素组成的数组(假设每个元素都不相等),那么一共有n!排列组合,
** 而且这n!排列组合结果都应该在决策树的叶子节点上,
** 对于高度为h的二叉树,叶子节点的个数最多为2h(当为满二叉树时为2h,这里根节点为第0层)。
** 所以n! <= 2h,从而h >= log(n!) = Ω(nlogn)。
** 
** 计数排序(Counting sort)是一种稳定的排序算法。
** 计数排序使用一个额外的数组C,其中第i个元素是待排序数组A中值等于i的元素的个数。
** 然后根据数组C来将A中的元素排到正确的位置。
** 
** 当输入的元素是 n 个 0 到 k 之间的整数时,它的运行时间是 Θ(n + k)。
** 计数排序不是比较排序,排序的速度快于任何比较排序算法。
** 由于用来计数的数组C的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1),
** 这使得计数排序对于数据范围很大的数组,需要大量时间和内存。
** 例如:计数排序是用来排序0到100之间的数字的最好的算法,但是它不适合按字母顺序排序人名。
** 但是,计数排序可以用在基数排序中的算法来排序数据范围很大的数组。
** 
** 算法的步骤如下:
** 1. 找出待排序的数组中最大和最小的元素
** 2. 统计数组中每个值为i的元素出现的次数,存入数组C的第i项
** 3. 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加)
** 4. 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1
*/ 

// 显然地,计数排序的时间复杂度为O(N+K),空间复杂度为O(N+K)。当K不是很大时,这是一个很有效的线性排序算法。
// 更重要的是,它是一种稳定排序算法,即排序后的相同值的元素原有的相对位置不会发生改变,
// 这是计数排序很重要的一个性质,就是根据这个性质,我们才能把它应用到基数排序。




//【计数排序】标准版本

template<typename T>
void counting_sort(const T arr[], T dest[], int size)
{
    int min = 0, max = 0;  

    for (int i=1; i < size; i++) // 确定数组的上下界
    {   
        if (arr[i] < arr[min])
        {   
            min = i;
        }
        else if (arr[i] > arr[max])
        {
            max = i;
        }
    }

    int   range = arr[max] - arr[min] + 1;
    int * count = new int[range]; //申请散列计数空间

    for (int i=0; i < range; i++)  //元素计数清零
    {   
        count[i] = 0;
    }

    for (int i=0; i < size; i++)   //元素计数
    {   
        count[arr[i]-arr[min]]++;  //元素平移散列计数
    }

    for (int i=1; i < range; i++)
    {   
        count[i] += count[i-1]; //将等于计数转换成小于等于计数
    }

    for (int i=size-1; i >= 0; i--) //反向排序输出至dest,保证稳定性
    {   
        dest[count[arr[i]-arr[min]]-1] = arr[i];
        count[arr[i]-arr[min]]--;
    }

    delete [] count;
}

// 可能你会发现,计数排序似乎饶了点弯子,比如当我们刚刚统计出C,C[i]可以表示A中值为i的元素的个数,
// 此时我们直接顺序地扫描C,亦即直接对C进行收集,就可以求出排序后的结果。不过算法将因此而丧失其稳定性。
// 的确是这样,不过这种方法不再是计数排序,而是桶排序(Bucket Sort),确切地说,是桶排序的一种特殊情况。

//【计数排序】版本二(不稳定)

template<typename T>
void counting_sort(T arr[], int size)
{
    int min = arr[0], max = arr[0];  

    for (int i=1; i < size; i++) // 确定数组的上下界
    {   
        if (arr[i] < min)
        {   
            min = arr[i];
        }
        else if (arr[i] > max)
        {
            max = arr[i];
        }
    }

    int   range = max - min + 1;
    int * count = new int[range]; //申请散列计数空间

    for (int i=0; i < range; i++)  //元素计数清零
    {   
        count[i] = 0;
    }

    for (int i=0; i < size; i++)   //元素计数
    {   
        count[arr[i]-min]++;  //元素平移散列计数
    }
    
    //由于元素平移散列计数属于唯一性散列,即唯有两个值相等的元素方能散列在同一个桶
    //因此,对于同一个桶中,多个元素无须进行排序即可进行直接收集

    int index = 0;
    for (int i=0; i < range; i++) //根据计数数组/桶进行直接收集排序
    {   
        for (int j=1; j <= count[i]; j++) // 该元素出现的次数
        {   
            arr[index++] = i+min;
        }
    }

    delete [] count;
}


/* 
** 桶排序(Bucket Sort)简介:
** 
** 桶排序也称箱排序(Bin Sort),其基本思想是:
** 设置若干个桶,依次扫描待排序的序列A[0],A[1],… ,A[N-1]
** 把关键字等于k的元素全都装入到索引为k的桶里(分配)。
** 对每个桶中的元素进行排序,什么排序算法都可以,例如快速排序。 
** 依次收集每个桶中的元素,顺序放置到输出序列中。 
** 
** 元素值到关键字之间存在一定的散列/映射关系,桶的个数取决于关键字的取值范围。
** 桶的类型应设计成链表为宜,不同的元素值可能散列成相同的关键字而进入同一个桶。
** 为保证排序的稳定性,分配过程中装箱及收集过程中的连接必须按先进先出原则进行。
** 唯有保证桶排序的稳定性,才能用于基数排序(见下面的基数排序版本二)。
** 
** 对该算法简单分析,如果数据是期望平均分布的,则每个桶中的元素平均个数为N/M。
** 如果对每个桶中的元素排序使用的算法是快速排序,每次排序的时间复杂度为O(N/M*log(N/M))。
** 则总的时间复杂度为O(N)+O(M)*O(N/M*log(N/M)) = O(N+ N*log(N/M)) = O(N + N*logN – N*logM)。
** 当M接近于N是,桶排序的时间复杂度就可以近似认为是O(N)的。
** 就是桶越多,时间效率就越高,而桶越多,空间却就越大,由此可见时间和空间是一个矛盾的两个方面。
*/



/* 
** 基数排序(Radix Sort)简介:
** 
** 以扑克牌排序为例。每张扑克牌有两个“关键码”:花色和面值。
** 可以先按花色排序,之后再按面值排序;也可以先按面值排序,再按花色排序。
** 这就是多关键码排序。排序后形成的有序序列叫做词典有序序列。
** 如果关键码是由多个数据项组成的数据项组,则依据它进行排序时就需要利用多关键码排序。
** 
** 实现多关键码排序有两种常用的方法:
** (1) 最高位优先MSD (Most Significant Digit first)
** (2) 最低位优先LSD (Least Significant Digit first)
** 
** 最高位优先法通常是一个递归的过程:
** (1) 先根据最高位关键码K1排序,得到若干对象组,对象组中每个对象都有相同关键码K1。 
** (2) 再分别对每组中对象根据关键码K2进行排序,按K2值的不同,再分成若干个更小的子组,每个子组中的对象具有相同的K1和K2值。
** (3) 依此重复,直到对关键码Kd完成排序为止。
** (4) 最后,把所有子组中的对象依次连接起来,就得到一个有序的对象序列。
** 
** 最低位优先法首先依据最低位关键码Kd对所有对象进行一趟排序,
** 再依据次低位关键码Kd-1对上一趟排序的结果再排序,依次重复,
** 直到依据关键码K1最后一趟排序完成,就可以得到一个有序的序列。
** 使用这种排序方法对每一个关键码进行排序时,不需要再分组,而是整个对象组都参加排序。
** 
** 基数排序是采用“分配”与“收集”的办法,用对多关键码进行排序的思想实现对单关键码进行排序的方法。
** 基数排序是典型的LSD排序方法,利用“分配”和“收集”两种运算对单关键码进行排序。
** 
** 链式基数排序:
** 各队列采用链式队列结构,分配到同一队列的关键码用链接指针链接起来。
** 每一队列设置两个队列指针: int front [radix]指示队头, int rear [radix] 指向队尾。
** 为了有效地存储和重排 n 个待排序对象,以静态链表作为它们的存储结构。
** 在对象重排时不必移动对象,只需修改各对象的链接指针即可。
** 
** 1.若每个关键码有d 位,需要重复执行d 趟“分配”与“收集”。每趟对 n 个对象进行“分配”,对radix个队列进行“收集”。总时间复杂度为O ( d ( n+radix ) )。
** 2.若基数radix相同,对于对象个数较多而关键码位数较少的情况,使用链式基数排序较好。
** 3.基数排序需要增加n+2radix个附加链接指针。
** 4.基数排序是稳定的排序方法。
** 
** 基数排序从低位到高位进行,使得最后一次计数排序完成后,数组有序。
** 其原理在于对于待排序的数据,整体权重未知的情况下,
** 先按权重小的因子排序,然后按权重大的因子排序。
** 例如比较时间,先按日排序,再按月排序,最后按年排序,仅需排序三次。
** 但是如果先排序高位就没这么简单了。
** 基数排序源于老式穿孔机,排序器每次只能看到一个列,
** 很多教科书上的基数排序都是对数值排序,数值的大小是已知的,与老式穿孔机不同。
** 将数值按位拆分再排序,是无聊并自找麻烦的事。
** 算法的目的是找到最佳解决问题的方案,而不是把简单的事搞的更复杂。
** 基数排序更适合用于对时间、字符串等这些整体权值未知的数据进行排序。
** 这时候基数排序的思想才能体现出来,例如字符串,如果从高位(第一位)往后排就很麻烦。
** 而反过来,先对影响力较小,排序排重因子较小的低位(最后一位)进行排序就非常简单了。
** 这时候基数排序的思想就能体现出来。
*/ 


//【基数排序】版本一:基于计数排序

template<typename T>
void radix_sort(T arr[], int size)
{   
    const int base = 10; //假设为十进制位

    T maxelem = *std::max_element(arr, arr+size); //序列中最大的元素

    int bits = 0;  // 最大元素的最大位数
    while (maxelem > 0)
    {   
        bits++;
        maxelem /= base;
    }

    int count[base]; //元素计数数组,用于内部计数排序
    T * temp = new T[size]; //临时收集缓存

    int radix = 1;
    for (int n=1; n <= bits; n++, radix *= base) //最低位优先LSD
    {   
        for (int i=0; i < base; i++)  //元素计数清零
        {   
            count[i] = 0;
        }

        for (int i=0; i < size; i++)   //元素计数
        {   
            count[arr[i]/radix%base]++;  //元素基于特定位数值散列计数
        }

        for (int i=1; i < base; i++)
        {   
            count[i] += count[i-1]; //将等于计数转换成小于等于计数
        }

        for (int i=size-1; i >= 0; i--) //反向收集至temp,保证稳定性
        {   
            temp[count[arr[i]/radix%base]-1] = arr[i];
            count[arr[i]/radix%base]--;
        }

        for (int i=0; i < size; i++)
        {   
            arr[i] = temp[i];  //将元素转移至arr中
        }
    }

    delete [] temp;
}



//【基数排序】版本二:基于桶排序

template<typename T>
void radix_sort(T arr[], int size)
{   
    const int base = 10; //假设为十进制位

    T maxelem = *std::max_element(arr, arr+size); //序列中最大的元素

    int bits = 0;  // 最大元素的最大位数
    while (maxelem > 0)
    {   
        bits++;
        maxelem /= base;
    }

    std::vector<T> bucket[base];

    int radix = 1;
    for (int n=1; n <= bits; n++, radix *= base) //最低位优先LSD
    {
        //分配工作
        for (int i=0; i < size; i++) //(1)
        {   
            bucket[arr[i]/radix%base].push_back(arr[i]); //(2)
        }

        //收集工作
        int index = 0;
        for (int i=0; i < base; i++) //(3)
        {   
            for (int j=0; j < bucket[i].size(); j++) //(4)
            {   
                arr[index++] = bucket[i][j]; //(5)+(1)+(2)+(3)+(4) 先进先出保证了稳定性
            }

            bucket[i].clear();  //清空bucket
        }
    }

}



//【基数排序】版本三:链式基数排序(处理链式存储的数据)

// 前面提到了当元素本身比较复杂导致元素移动/交换的代价太大时
// 我们可以考虑使用基于索引的间接排序,另一种方式就是对待排序
// 的数据采用链式存储取代通常的线性顺序存储,而基数排序则非常
// 适合用来排序采用链式存储的数据序列。

template<typename T>
struct ListNode
{   
    T m_data;
    ListNode * m_next;
};


template<typename T>
void radix_sort(ListNode<T> *& head)
{
    const int base = 10; //假设为十进制位
    
    //首先寻找出最大值
    ListNode<T> * max = head;
    ListNode<T> * ptr = head->m_next;
    while (ptr != NULL)
    {   
        if (ptr->m_data > max->m_data)
        {   
            max = ptr;
        }
        ptr = ptr->m_next;
    }

    T maxelem = max->m_data;
    
    //最大元素的位数
    int bits = 0;
    while (maxelem > 0)
    {   
        bits++;
        maxelem /= base;
    }

    std::vector<ListNode<T>*> bucket[base];

    int radix = 1;
    for (int n=1; n <= bits; n++, radix *= base) //最低位优先LSD
    {
        //分配工作
        for (ptr=head; ptr!=NULL; ptr=ptr->m_next) //(1)
        {   
            bucket[ptr->m_data/radix%base].push_back(ptr); //(2)
        }

        //收集工作
        ptr = NULL;
        for (int i=0; i < base; i++) //(3)
        {   
            for (int j=0; j < bucket[i].size(); j++) //(4)
            {   
                if (ptr == NULL)
                {   
                    head = ptr = bucket[i][j]; //修正链表头
                }
                else
                {
                    ptr->m_next = bucket[i][j]; //(5)+(1)+(2)+(3)+(4) 先进先出保证了稳定性
                    ptr = ptr->m_next;
                }
            }
            bucket[i].clear();  //清空bucket
        }
    }
}




//二分查找


//可以返回索引,也可以返回bool值表明查找成功与否,下面是循环版本
template<typename T>
bool BinarySearch(T arr[], int size, const T& key)  //数组的常规传递方式
{
    int low = 0, high = size-1;

    // 相等意味着还有一个元素,需要再迭代
    while (low <= high)
    {
        int mid = low + (high-low)/2;  //防止算术溢出
        if (arr[mid] > key)
        {
            high = mid-1;
        }
        else if (arr[mid] < key)
        {
            low = mid+1;
        }
        else /*(arr[mid] == key)*/
        {
            return true;
        }
    }
    return false;
}

// 递归版本
template<typename RandomIterator, typename T>
bool BinarySearch(RandomIterator first, RandomIterator last, const T& key)  //STL区间传递方式
{   
    if ( last <= first) return false;  //区间为空(last作为超尾,该元素不算在内)

    RandomIterator mid = first+(last-first)/2;
    if (*mid > key)
    {
        return BinarySearch(first, mid, key); // 查找区间[first, mid)
    }
    else if (*mid < key)
    {
        return BinarySearch(mid+1, last, key); // 查找区间[mid+1, last)
    }
    else /*(*mid == key)*/
    {
        return true;
    }
}


//上确界
///
//返回关键值key可以插入的第一个位置(该位置之前),而不破坏原序列的有序性
template<typename RandomIterator, typename T>
RandomIterator my_lower_bound(RandomIterator first, RandomIterator last, const T& key)
{
    while (first < last) //此时区间[first, last)不为空
    {
        RandomIterator mid = first + (last-first)/2;
        if (key <= *mid)
        {
            last = mid;  //末端前向收缩
        }
        else /*(key > *mid)*/
        {
            first = mid+1; //前端后向收缩
        }
    }
    return last;
}


//下确界
///
//返回关键值key可以插入的最后一个位置(该位置之前),而不破坏原序列的有序性
template<typename RandomIterator, typename T>
RandomIterator my_upper_bound(RandomIterator first, RandomIterator last, const T& key)
{
    while (first < last)
    {
        RandomIterator mid = first + (last-first)/2;
        if (key >= *mid)
        {
            first = mid+1; //前端后向收缩
        }
        else
        {
            last = mid;  //末端前向收缩
        }
    }
    return last;
}

//找上下确界与二分查找不同,二分查找一个关键值不一定存在,因此返回值可能无效,但是
//找一个关键值的上下确界却一定存在,因此返回值必然有效,上下确界的实现是基于二分查找的实现
//关键是二分查找在发现中值与关键值吻合的时候就会即刻终止,但上下确界却不然,它要迭代直到整个
//区间长度收缩至0为止,因此中值与关键值吻合时仍要发生迭代,上确界则末端前向收缩,下确界则前端后向收缩。



 

  - - end - - 



 

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值