数据结构之排序算法建议收藏

排序(Sorting),特别是高效的排序一直是计算机工作学习和研究的重要课题之一,排序有内部排序和外部排序之分,若整个排序过程不需要访问外存便能完成,则称此类排序为内部排序,反之则为外部排序。本篇将对

大家好,又见面了,我是全栈君,今天给大家准备了Idea注册码。

数据结构之排序算法建议收藏此处内容已经被作者隐藏,请输入验证码查看内容
验证码:
请关注本站微信公众号,回复“”,获取验证码。在微信里搜索“”或者“”或者微信扫描右侧二维码都可以关注本站微信公众号。

  排序(Sorting),特别是高效的排序一直是计算机工作学习和研究的重要课题之一,排序有内部排序和外部排序之分,若整个排序过程不需要访问外存便能完成,则称此类排序为内部排序,反之则为外部排序。本篇将对常用排序算法进行总结。

  在进行排序总结之前先介绍测试中常用到的生成随机数方法和计算执行时间的方法。

1. C++生成随机数

  C++中没有自带的random函数,要实现随机数的生成就需要使用rand()和srand()。不过由于rand()的内部实现是线性同余法左的,所以生成的并不是真正的随机数,而是在一定范围内可看为随机的伪随机数。

(1)rand

  单纯的rand()会返回一个0到RAND_MAX之间的随机数值,而RAND_MAX的值与int位数有关,最小为32767。不过rand()是一次性的,因为系统默认的随机种子数是1,只要随机种子树不变,其生成的随机数序列不会改变。

  对于rand()的范围,我们是可以进行人为设定的,只需要在宏定义中定义一个random(int x)函数,就可以生成范围为0至x的随机数值。当然,也可以定义为random(a,b),使其生成范围为a至b的随机数值。具体定义方法在通式部分。

(2)srand

  srand()可用来设置rand()产生随机数时的随机数种子。通过设置不同的种子,我们可以获取不同的随机数序列。可以利用srand((unsigned int)(time(NULL))的方法,利用系统时钟,产生不同的随机数种子。不过要调用time(),需要加入头文件< ctime >。

(3)通式

  产生一定范围随机数的通用表示公式是:

  • 取得(0,x)的随机整数:rand()%x;
  • 取得(a,b)的随机整数:rand()%(b-a);
  • 取得[a,b)的随机整数:rand()%(b-a)+a;
  • 取得[a,b]的随机整数:rand()%(b-a+1)+a;
  • 取得(a,b]的随机整数:rand()%(b-a)+a+1;
  • 取得0-1之间的浮点数:rand()/double(RAND_MAX)。

2. 线程执行时间

  为了得到us级的时间计数,Windows提供了如下高精度的性能函数:

  BOOL QueryPerformanceFrequency(LARGE_INTEGER *pliFrequency);

  BOOL QueryPerformanceCounter(LARGE_INTEGER *pliCount);

class CStopwatch
{
private:
    // 64位有符号整数可以用INT64 _int64 LARGE_INTEGER表示
    LARGE_INTEGER m_nPerfFrequency;
    LARGE_INTEGER m_nPerFrefStart;

public:
    CStopwatch(){QueryPerformanceFrequency(&m_nPerfFrequency); Start();}
    void Start(){QueryPerformanceCounter(&m_nPerFrefStart);}

    INT64 Now() const
    {
        LARGE_INTEGER nPerfNow;
        QueryPerformanceCounter(&nPerfNow);
        return ((nPerfNow.QuadPart - m_nPerFrefStart.QuadPart) * 1000) / m_nPerfFrequency.QuadPart;
    }

    INT64 NowInMicro() const
    {
        LARGE_INTEGER nPerfNow;
        QueryPerformanceCounter(&nPerfNow);
        return ((nPerfNow.QuadPart - m_nPerFrefStart.QuadPart) * 1000000) / m_nPerfFrequency.QuadPart;
    }
};

3. 插入排序

(1)排序思想

  插入排序(Insertion Sort)的主要思想是不断地将待排序的元素插入到有序序列中,是有序序列不断地扩大,直至所有元素都被插入到有序序列中。例如我们平常玩扑克牌时的抓牌操作就是一个插入操作的例子,每抓一张牌后我们便将其插入到合适的位置,直到抓完牌位置,这时我们手上的牌就成了一个有序序列。

(2)代码实现

// 插入排序
template <typename T>
void InsertSort(T *array, int nLen)
{
    int i,j,k;
    for (i = 1; i < nLen; i ++)
    {
        for (j = i - 1; j >=0; j--)
        {
            if (array[i] > array[j])
            {
                break;
            }
        }
        
        T temp = array[i];
        for (k = i-1; k >j; k --)
        {
            array[k + 1] = array[k];
        }
        
        array[j+1] = temp;
    }
}

  总结:直接插入排序最好情况时间复杂度为O(n),最坏情况下(逆序表)时间复杂度为O(n2),因此它只适合于数据量较少的情况使用

4. 冒泡排序

(1)排序思想

  冒泡排序(Bubble Sort)是一种简单的交换排序方法,其基本思想是:两两比较相邻记录的关键字,如果反序则交换,直到没有反序的记录为止

(2)代码实现

// 冒泡排序
template<typename T>
void BubbleSort(T* array, int nLen)
{
    if (array == NULL || nLen <=0 )
    {
        return;
    }
    for (int i = 1; i < nLen; i ++)
    {
        for (int j = 0; j < nLen - i; j ++)
        {
            if (array[j+1] < array[j])
            {
                T temp = array[j+1];
                array[j] = array[j+1];
                array[j+1] = temp;
            }
        }
    }
}

  总结:冒泡排序在运行时间方面,待排序的记录越接近有序,算法的执行效率就越高,反之,执行效率则越低,它的平均时间复杂度为O(n2)

5. 选择排序

(1)排序思想

  选择排序的基本思想是:第一趟从所有的n个记录中选择最小的记录放在第一位,第二趟从n-1个记录中选择最小的记录放到第二位。以此类推,经过n-1趟排序之后,整个待排序序列就成为有序序列了

(2)代码实现

// 选择排序
template<typename T>
void SelectSort(T *array, int nLen)
{
    if (array == NULL || nLen <= 0)
    {
        return;
    }
    
    int i,j,k;
    for (i = 0; i < nLen - 1; i ++)
    {
        k = i;
        for (j = i + 1; j < nLen; j ++)
        {
            if (array[j] < array[k])
            {
                k = j;
            }
        }

        if (k != i)
        {
            T temp = array[k];
            array[k] = array[i];
            array[i] = temp;
        }
    }
}

  总结:选择排序外循环n-1趟,内循环执行n-i趟,因此,简单选择排序的平均时间复杂度为O(n2),和直接插入排序、冒泡排序一样均超过了1秒钟。

6. 快速排序

(1)排序思想

  快速排序的基本思想是:通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序的目的

快速排序的核心步骤为:

①获取中轴元素

②i从左至右扫描,如果小于基准元素,则i自增,否则记下a[i]

③j从右至左扫描,如果大于基准元素,则i自减,否则记下a[j]

④交换a[i]和a[j]

⑤重复这一步骤直至i和j交错,然后和基准元素比较,然后交换。

(2)代码实现

template<typename T>
void QuickSort_1(T *array, int start, int end)
{
    if (array == NULL || start >= end)
    {
        return;
    }
    int i = start;
    int j = end;
    T temp = array[start];
    while (i < j)
    {
        while ((i < j) && array[j] >= temp)
        {
            j --;
        }
        array[i] = array[j];
        while ((i < j) && array[i] <= temp)
        {
            i ++;
        }
        array[j] = array[i];
    }
    array[i] = temp;
    QuickSort_1(array, start, i - 1);
    QuickSort_1(array, i + 1, end);
}

// 快速排序
template<typename T>
void QuickSort(T *array, int nLen)
{
    if (array == NULL || nLen <= 0)
    {
        return;
    }

    QuickSort_1(array, 0, nLen - 1);
}

  总结:快速排序的平均时间复杂度为O(nlog2n),在平均时间下,快速排序时目前被认为最好的内部排序方法。但是,如果待排序记录的初始状态有序,则快速排序则会退化为冒泡排序,其时间复杂度为O(n2)。换句话说,待排序记录越无序,基准两侧记录数量越接近,排序速度越快;相反,待排序记录越有序,则排序速度越慢

  对于快速排序的改进一般集中在以下几个方面:

  ①当划分到较小的子序列时,通常可以使用插入排序替代快速排序;

  ②使用三平均分区法代替第一个元素作为基准值所出现的某些分区严重不均的极端情况;

  ③使用并行化处理排序;

7. 堆排序

(1)排序思想

  堆排序(Heap Sort)是由J.Williams在1964年提出的,它是在选择排序的基础上发展起来的,比选择排序的效率要高,因此也可以说堆排序是选择排序的升级版。堆排序除了是一种排序方法外,还涉及到方法之外的一些概念:堆和完全二叉树。这里主要说说什么是堆?

  如果将堆看成一棵完全二叉树,则这棵完全二叉树中的每个非叶子节点的值均不大于(或不小于)其左、右孩子节点的值。由此可知,若一棵完全二叉树是堆,则根节点一定是这棵树的所有节点的最小元素或最大元素。非叶子节点的值大于其左、右孩子节点的值的堆称为大根堆,反之则称为下小根堆,如下图所示。

数据结构之排序算法建议收藏

  如果按照层序遍历的方式给结点从1开始编号,则结点之间满足如下关系:

数据结构之排序算法建议收藏

  (1)基本思想

  堆排序的基本思想是:首先将待排序的记录序列构造为一个堆,此时选择堆中所有记录的最小记录或最大记录,然后将它从堆中移出,并将剩余的记录再调整成堆,这样就又找到了次大(或次小)的记录。以此类推,直到堆中只有一个记录为止,每个记录出堆的顺序就是一个有序序列

  (2)处理步骤

  堆排序的处理步骤如下:

①设堆中元素个数为n,先取i=n/2-1,将以i节点为根的子树调整成堆,然后令i=i-1。再将以i节点为根的子树调整成堆,如此反复,直到i=0为止,即完成初始堆的创建过程;

②首先输出堆顶元素,将堆中最后一个元素上移到原堆顶位置,这样可能会破坏原有堆的特性,这时需要重复步骤①的操作来恢复堆;

③重复执行步骤②,直到输出全部元素为止。按输出元素的前后次序排列起来,就是一个有序序列,从而也就完成了对排序操作。

  假设待排序序列为(3,6,5,9,7,1,8,2,4),那么根据此序列创建大根堆的过程如下:

  ①将(3,6,5,9,7,1,8,2,4)按照二叉树的顺序存储结构转换为如下图所示的完全二叉树;

数据结构之排序算法建议收藏

  ②首先,因为n=9,所以i=n/2-1=3,即调整以节点9为根的子树,由于节点9均大于它的孩子节点2和4,所以不需要交换;最后,i=i-1=2。

  ③当i=2时,即调整以节点5为根的子树,由于节点5小于它的右孩子8,所以5需要与8交换;最后,i=i-1=1。

数据结构之排序算法建议收藏

  ④当i=1时,即调整以节点6为根的子树,由于节点6均小于它的左、右孩子9和7,故节点6需要与较大的左孩子9交换;最后i=i-1=0。

数据结构之排序算法建议收藏

  ⑤当i=0时,即调整以3为根的子树,由于节点3均小于它的左、右孩子9和8,故节点3需要与较大的左孩子9交换;交换之后又因为节点3小于它的左、右孩子节点6和7,于是需要与较大的右孩子7交换。

数据结构之排序算法建议收藏

  ⑥如上图所示,至此就完成了初始堆的创建,待排序序列变为(9,7,8,6,3,1,5,2,4)。

(2)代码实现

// 堆排序
template<typename T>
 void maxHeapDown(T* array, int start, int end)
 {
    int current = start; // 当前节点位置
    int left = 2 * current + 1; // 左孩子节点
    T temp= array[current];
    for (;left <= end; current = left,left = 2 * left + 1)
    {
        if (left < end && array[left] < array[left+1])
        {
            left ++;
        }
        if (temp >= array[left])
        {
            break;
        }
        else
        {
            array[current] = array[left];
            array[left] = temp;
        }
    }
 }

 template<typename T>
 void HeapSort(T *array, int nLen)
 {
    int i;
    T temp;
    // 从(n/2-1) --> 0逐次遍历。遍历之后,得到的数组实际上是一个(最大)二叉堆
    for (i = nLen/2-1; i >=0; i --)
    {
        maxHeapDown(array, i, nLen-1);
    }
    
    // 从最后一个元素开始对序列进行调整,不断的缩小调整的范围直到第一个元素
    for (i = nLen - 1; i > 0; i--)
    {
        temp = array[0];
        array[0] = array[i];
        array[i] = temp;

        maxHeapDown(array, 0, i - 1);
    }
 }

  总结:堆排序的执行时间主要由建立初始堆和反复调整堆这两个部分的时间开销组成,由于堆排序对原始记录的排序状态并不敏感,因此它无论是最好、最坏和平均时间复杂度均为O(nlog2n)。这在性能上显然要远远好过于冒泡、简单选择、直接插入的O(n2)的时间复杂度了。另外,由于初始构建堆所需的比较次数较多,因此,它并不适合待排序序列个数较少的情况。

8. 归并排序

(1)排序思想

  归并排序(Merging Sort)是利用“归并”技术进行的排序,所谓归并是指将两个或两个以上的有序表合并成一个新的有序表。其基本思想是:将这些有序的子序列进行合并,从而得到有序的序列

  如果初始序列含有n个记录,则可以看成是n个有序的子序列,每个子序列的长度为1,然后两两归并,得到(表示不小于x的最小整数)个长度为2或1的有序子序列;再两两归并,……,如此重复,直至得到一个长度为n的有序序列为止

  例如一个本是无序的数组序列{16,7,13,10,9,15,3,2,5,8,12,1,11,4,6,14},通过两两合并排序后再合并,最终获得了一个有序的数组,如下图所示:

数据结构之排序算法建议收藏

  观察上图,细心的你会惊讶,它十分像一棵倒置的完全二叉树,通常涉及到完全二叉树结构的排序算法,效率一般都不会低。

(2)实现代码

 // 将一个数组中两个相连的有序数组合并成一个
 template<typename T>
 void Merge(T *array, int start, int mid, int end)
 {
     T *tmp = (T *)malloc((end-start+1)*sizeof(T));    // tmp是汇总2个有序区的临时区域
     int i = start;            // 第1个有序区的索引
     int j = mid + 1;        // 第2个有序区的索引
     int k = 0;                // 临时区域的索引

     while(i <= mid && j <= end)
     {
         if (array[i] <= array[j])
         {
             tmp[k++] = array[i++];
         }
         else
         {
             tmp[k++] = array[j++];
         }
     }

     while(i <= mid)
     {
         tmp[k++] = array[i++];
     }

     while(j <= end)
     {
         tmp[k++] = array[j++];
     }

     // 将排序后的元素,全部都整合到数组a中。
     for (i = 0; i < k; i++)
     {
         array[start + i] = tmp[i];
     }

     free(tmp);
 }

template<typename T>
void MergeSort(T *array, int start, int end)
{
    if (array == NULL || start >= end)
    {
        return;
    }
    int mid = (end+start) / 2;
    MergeSort(array, start, mid);
    MergeSort(array, mid + 1, end);

    Merge(array, start, mid, end);
}

  总结:二路归并排序易于在链表上实现,它的时间复杂度在最好、最坏情况下均为O(nlog2n),但二路归并排序与其他排序相比,需要更多的临时空间。从Merge方法可以看出,需要频繁地创建临时空间来存储合并后的数据,可以让所有的Merge方法共用同一块临时空间,以最大限度地减少内存使用。

9. 以上排序算法用时比较

  (1)对比条件:对10000个随机整数进行排序

  (2)测试结果:

  数据结构之排序算法建议收藏

  数据结构之排序算法建议收藏

  数据结构之排序算法建议收藏

  数据结构之排序算法建议收藏

  数据结构之排序算法建议收藏

  数据结构之排序算法建议收藏

  (3)全部代码

数据结构之排序算法建议收藏
数据结构之排序算法建议收藏

#include "stdio.h"
#include<ctime>
#include <stdlib.h>
#include <iostream>
using namespace std;
#include <Windows.h>

#define random(a,b) (rand()%(b-a+1)+a)
class CStopwatch
{
private:
    // 64位有符号整数可以用INT64 _int64 LARGE_INTEGER表示
    LARGE_INTEGER m_nPerfFrequency;
    LARGE_INTEGER m_nPerFrefStart;

public:
    CStopwatch(){QueryPerformanceFrequency(&m_nPerfFrequency); Start();}
    void Start(){QueryPerformanceCounter(&m_nPerFrefStart);}

    INT64 Now() const
    {
        LARGE_INTEGER nPerfNow;
        QueryPerformanceCounter(&nPerfNow);
        return ((nPerfNow.QuadPart - m_nPerFrefStart.QuadPart) * 1000) / m_nPerfFrequency.QuadPart;
    }

    INT64 NowInMicro() const
    {
        LARGE_INTEGER nPerfNow;
        QueryPerformanceCounter(&nPerfNow);
        return ((nPerfNow.QuadPart - m_nPerFrefStart.QuadPart) * 1000000) / m_nPerfFrequency.QuadPart;
    }
};

// 插入排序
template <typename T>
void InsertSort(T *array, int nLen)
{
    int i,j,k;
    for (i = 1; i < nLen; i ++)
    {
        for (j = i - 1; j >=0; j--)
        {
            if (array[i] > array[j])
            {
                break;
            }
        }
        
        T temp = array[i];
        for (k = i-1; k >j; k --)
        {
            array[k + 1] = array[k];
        }
        
        array[j+1] = temp;
    }
}

template<typename T>
void QuickSort_1(T *array, int start, int end)
{
    if (array == NULL || start >= end)
    {
        return;
    }
    int i = start;
    int j = end;
    T temp = array[start];
    while (i < j)
    {
        while ((i < j) && array[j] >= temp)
        {
            j --;
        }
        array[i] = array[j];
        while ((i < j) && array[i] <= temp)
        {
            i ++;
        }
        array[j] = array[i];
    }
    array[i] = temp;
    QuickSort_1(array, start, i - 1);
    QuickSort_1(array, i + 1, end);
}

// 快速排序
template<typename T>
void QuickSort(T *array, int nLen)
{
    if (array == NULL || nLen <= 0)
    {
        return;
    }

    QuickSort_1(array, 0, nLen - 1);
}

// 冒泡排序
template<typename T>
void BubbleSort(T* array, int nLen)
{
    if (array == NULL || nLen <=0 )
    {
        return;
    }
    for (int i = 1; i < nLen; i ++)
    {
        for (int j = 0; j < nLen - i; j ++)
        {
            if (array[j+1] < array[j])
            {
                T temp = array[j+1];
                array[j] = array[j+1];
                array[j+1] = temp;
            }
        }
    }
}

// 选择排序
template<typename T>
void SelectSort(T *array, int nLen)
{
    if (array == NULL || nLen <= 0)
    {
        return;
    }
    
    int i,j,k;
    for (i = 0; i < nLen - 1; i ++)
    {
        k = i;
        for (j = i + 1; j < nLen; j ++)
        {
            if (array[j] < array[k])
            {
                k = j;
            }
        }

        if (k != i)
        {
            T temp = array[k];
            array[k] = array[i];
            array[i] = temp;
        }
    }
}

// 堆排序
template<typename T>
 void maxHeapDown(T* array, int start, int end)
 {
    int current = start; // 当前节点位置
    int left = 2 * current + 1; // 左孩子节点
    T temp= array[current];
    for (;left <= end; current = left,left = 2 * left + 1)
    {
        if (left < end && array[left] < array[left+1])
        {
            left ++;
        }
        if (temp >= array[left])
        {
            break;
        }
        else
        {
            array[current] = array[left];
            array[left] = temp;
        }
    }
 }

 template<typename T>
 void HeapSort(T *array, int nLen)
 {
    int i;
    T temp;
    // 从(n/2-1) --> 0逐次遍历。遍历之后,得到的数组实际上是一个(最大)二叉堆
    for (i = nLen/2-1; i >=0; i --)
    {
        maxHeapDown(array, i, nLen-1);
    }
    
    // 从最后一个元素开始对序列进行调整,不断的缩小调整的范围直到第一个元素
    for (i = nLen - 1; i > 0; i--)
    {
        temp = array[0];
        array[0] = array[i];
        array[i] = temp;

        maxHeapDown(array, 0, i - 1);
    }
 }

void main()
{
    srand((unsigned)time(NULL));
    //cout << "随机得到的原数组:";  
    int *arr = new int[10000];
    for(int i=0;i<10000;i++)
    {
        arr[i] = random(1,10000);
        //cout << arr[i] << " ";
    }
    //cout << endl;
    //cout << "经过排序后得到的数组:";
    CStopwatch watch;
    //InsertSort(arr, 10000);
    //BubbleSort(arr, 10000);
    //SelectSort(arr, 10000);
    //QuickSort(arr, 10000);
    HeapSort(arr, 10000);
     
    INT64 delay = watch.Now();
    /*for (int i = 0; i < 1000; i ++)
    {
        cout << arr[i] << " ";
    }*/
    //cout << endl;
    cout << "对10000个随机数进行堆排序总用时:" << delay << "ms" << endl;
    
    delete[] arr;
    return;
}

View Code

注:参考博客:

  (1)
http://www.cnblogs.com/edisonchou/p/4713551.html

  (2)
http://www.cnblogs.com/skywang12345/p/3603935.html

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/120145.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)


相关推荐

  • 登录mysql数据库的几种方式

    登录mysql数据库的几种方式登录mysql数据库的几种方式

  • 2022年面试工具篇Jmeter接口面试题及答案「建议收藏」

    2022年面试工具篇Jmeter接口面试题及答案「建议收藏」问题列表•在项目中如何用jmeter进行http接口测试?•Jmeter常用元件有哪些?•jmeter如何管理cookie和session信息?•jmeter中如何实现关联?•jmeter中断言方式?•jmeter参数化的方式有哪几种可以实现?•Jmeter怎么录制,怎么过滤?•JMeter结果树响应数据中文乱码如何解决?•用户定义的变量和用户参数的区别?•Jmeter怎么实现持续集成测试?在项目中如何用jmeter进行http接口测试?(重点)•在Jmeter安装目录bin…

  • Ubuntu下代替dos2unix命令

    Ubuntu下代替dos2unix命令Ubuntu系统打开Windows下生成的文本文件,会在每行的末尾出现’^M’原因就是Windows和Linux的回车符是不同的在Windows下回车符是\r\n回车换行在Linux下回车符是\n最简单、最常用的解决方法是使用dos2unix命令转换:dos2unixfilenameUbuntu下dos2unix和unix2dos命令在tofrodos包中

  • Spring Batch事务处理

    Spring Batch事务处理之前一直对SpringBatch的使用有些迷糊,尤其是事务这块,经常出些莫名其妙的问题,仔细了解了一下,做个小总结

  • Java基础之—反射(非常重要)

    Java基础之—反射(非常重要)反射是框架设计的灵魂(使用的前提条件:必须先得到代表的字节码的Class,Class类用于表示.class文件(字节码))一、反射的概述JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。要想解剖一个类,必须先要获取到该类的

  • int什么数据类型_SQL基本数据类型

    int什么数据类型_SQL基本数据类型 一开始看到Int16,Int32,Int64这三种类型就觉得有点怪,为什么要整个数字结尾的,这么干就是想让大家一眼就知道这个数据类型占多大空间吧.Int16,等于short,占2个字节.-3276832767Int32,等于int,占4个字节.-21474836482147483647Int64,等于long,占8个字节.-9223372036854…

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号