• STL sort解析


    从上学接触到编程开始,到工作了几年。有关排序算法的内容反反复复有接触,但是要说每一种排序算法的细节都能说清,那就有难度了。

    一来算法难度有深有浅。有比较简单的冒泡,插入,也有复杂的堆排序,快排这些。

    二来排序算法实际上成型已久,是个套路的东西。

    三来平时工作用不到每一种算法。实际数据不多的时候用冒泡足够了。数据多时,用快排足够了。

    关联容器内部有自动排序,序列容器中只有vector,deque的迭代器是随机位置迭代器(RandomAccessIterators),因此只有vector和deque能使用std::sort()。

    后面讲快排会讲为什么sort()必须是随机位置迭代器。

    今天看到一个实际工作中经常用到的例子,但是平时没有注意它的原理。

    这个例子有助于加深对排序的理解。便是STL里面的sort()算法。

    sort()是有策略进行排序。具体规则如下:

    // 1.数据量大时Quick Sort(快排)
    // 2.分段时数据量小于门槛,改用Inserition Sort(插入排序)
    // 3.递归层次太深,改用Heap Sort(堆排序)

    里面使用了3种排序。既有复杂度O(n*n)的插入排序,也有复杂度O(nlogn)的快排和堆排序。

    先分别来看这3种排序。从简单的插入排序开始吧。

    插入排序

    假设待排序的序列是a[0..n - 1],总共n个数据。

    1)a[0]是已排序的序列,a[i, n - 1]是未排序的序列。令i = 1。

    2)取a[i]插入到有序序列合适位置,使得新序列有序。

    3)重复2步骤,直到i == n - 1。

    外层循环是从1到n-1,内层循环是找插入位置,无需遍历所有位置,当a[j - 1] < a[j]即可停止,否则继续找位置,同时交换a[j - 1],a[j]的位置。

    void InsertSort(int *a, int count)
    {
        if (count <= 1)
        {
            return;
        }
    
        for (int i = 1; i < count; ++i)
        {
            for (int j = i; j >= 0 && a[j - 1] > a[j]; j--)
            {
                swap(a[j - 1], a[j]);
            }
        }
    }

    有两层循环,算法复杂度是O(n*n)  。稳定不稳定,取决于中间的判断条件。a[j - 1] > a[j]则是稳定的,条件变成a[j - 1] >= a[j]则是不稳定的。

    快速排序

    快排的核心是分治法

    1.如果序列S中数据的数量是0或者1。结束。

    2.取S中的任意一个数字,当作枢纽v

    3.将S分割成两段L,R,使得L中的元素全部小于等于v,R中的元素全部大于等于v。

    4.将L,R递归执行快排。

    其中第2步,选取一个数字做枢纽。可以是任意的数字,通常用的是median-of-three。从首尾和中间数中,选择中值。

    比如序列:1,4,5,7,9,8,3。那么1,7,3中选3作为枢纽来对序列进行分割。

    为了能快速取出中间元素,迭代器必须支持随机位置(RandomAccess)。

    三个数取中值的函数,只有画线段标位置,容易看出a,b,c三者的大小关系。

    int median(int a, int b, int c)
    {
        if (a < b)
        {
            if (b < c)
            {
                return b; // a < b < c
            }
            else
            {
                if (a < c)
                {
                    return c;    // a < c <= b
                }
                else
                {
                    return a;    // c <= a < b
                }
            }
        }
        else
        {
            if (b < c)
            {
                if (a < c)
                {
                    return a;    // b <= a < c
                }
                else
                {
                    return c;    // b < c < a
                }
            }
            else
            {
                return b;        // c <= b <= a
            }
        }
    }

    再来看分割算法:

    令first指向头元素,last指向尾元素。first向尾端移动,last向头端移动。

    当first大于等于枢纽时,停止。当last小于等于枢纽时,停止。

    检查first,last是否交错(first >= last),如果交错返回first。如果没有交错,first,last各自向中央移动一个位置。

    重复上面的步骤。

    最终返回的first是右半部分的起点。左半部分全部小于等于枢纽,右半部分全部大于等于枢纽。

    来看代码,RandomAccessIter可以理解成一个普通指针。

    int* partition(int *first, int *last, int pivot)
    {
        while (true)
        {
            while (*first < pivot) first++;
            while (*last > pivot) last--;
            if (first >= last) return first;
            swap(*first, *last);
            first++;
            last--;
        }
    }

    引用:

    1.《STL源码剖析》

    2.《MoreWindows白话经典算法之七大排序第2版》

  • 相关阅读:
    使用tcmalloc编译启动时宕机
    使用tcmalloc编译出现undefined reference to `sem_init'
    使用AddressSanitizer做内存分析(一)——入门篇
    VIM-美化你的标签栏
    Entity Framework Code First (六)存储过程
    Entity Framework Code First (五)Fluent API
    Entity Framework Code First (四)Fluent API
    Entity Framework Code First (三)Data Annotations
    Entity Framework Code First (二)Custom Conventions
    Entity Framework Code First (一)Conventions
  • 原文地址:https://www.cnblogs.com/yao2yaoblog/p/7443804.html
Copyright © 2020-2023  润新知