详解内部排序算法(C语言)

Last Updated: 2023-07-15 09:05:56 Saturday

-- TOC --

内部排序算法,Internal Sorting Algorithm。所谓内部,是指在系统内存的内部,仅在内存中进行计算的排序算法。

排序算法的稳定性:排序前后,相等元素的顺序不发生变化!在某些情况下,稳定性是个重要的属性,比如:

排序算法的稳定性,与算法逻辑和实现方式都有关。

采用分而治之思想的算法,都挺快的!分布式,多核等等,都是类似的思想。

非递归算法有个潜在的优势,即计算过程调用栈不会增长,在嵌入式或特殊场景下很受欢迎!

在存储越来越便宜的情况下,空间复杂度显得就相对不那么重要了,空间换时间一般都很值!

冒泡排序(Bubble)

稳定

时间复杂度: \(O(n^2)\)

空间复杂度: \(O(1)\)

参考实现:

void bubble(int a[], size_t n){
    if (n > 1) {
        size_t i,j;

        for (i=0; i<n-1; ++i) {
            for (j=0; j<n-i-1; ++j) {
                if (a[j] > a[j+1])
                    swap(a+j, a+j+1);
            }
        }
    }
}

比较临近两个元素,一步步将最大或最小的那个推上去,就像泡泡一样,缓慢从水底升起。第1个泡是最大或最小的,第2个泡是次大或次小的。

bubble.png

选择排序(Select)

稳定/不稳定,与具体实现有关。

时间复杂度:\(O(n^2)\)

空间复杂度:\(O(1)\)

参考实现:

void selects(int a[], size_t n){  // conflict name with select
    size_t i,j,k;

    for (i=n; i>1; --i) {
        k = 0;
        for (j=1; j<i; ++j)
            if (a[k] < a[j])
                k = j;
        if (k != j-1)
            swap(a+k, a+j-1);
    }
}

在每一个内循环中,找出(选择)最大的那个,用k定位,最后如果k的位置不对,执行swap。选择排序比冒泡排序快一点,因为在每一轮迭代中,它会比较N次,但只交换1次。相同元素的处理方式决定了稳定性。

select_sort.gif

插入排序(Insert)

稳定/不稳定,可能与具体实现有关,当把一个元素插入已排序队列时,相同元素是前插还是后插,可能会因为性能考虑而成为一个选择。当具体实现总是可以控制稳定与否。

时间复杂度:\(O(n^2)\)

空间复杂度:\(O(1)\)

参考实现:

/* If data is linked in memory by pointer, by using insert algorithm,
 * there is no need to move elements. We could directly modify the
 * pointers to complete the insert action. That would be more efficient.
 * Below is an implementation of the idea of insert on int array. */
void insert(int a[], size_t n){ // stable
    int t;
    size_t i,j;

    for (i=1; i<n; ++i) {
        t = a[i];
        for (j=i; j>0&&a[j-1]>t; --j);
        memmove(a+j+1, a+j, sizeof(int)*(i-j));  // cannot be memcpy
        a[j] = t;
    }
}

参考:memcpy和memmove的区别

void insert2(int a[], size_t n){ // stable
    int t;
    size_t i,j;

    for (i=1; i<n; ++i) {
        t = a[i];
        for (j=i; j>0; --j)
            if (a[j-1] > t)
                a[j] = a[j-1];
            else
                break;
        a[j] = t;
    }
}

t是每一轮迭代待排序的那个元素,t前面是已经排好序的,寻找t的位置,移动,插入。

insert.png

选择排序的关键点在于,选出来后,放入合适位置。插入排序的关键点再于,它不选,按顺序取元素后,插入合适位置。

从在int array上的测试结果来看,性能排序为:Insert > Selects > Bubble。Insert比Selects快一点点,但比Bubble快了一个数量级。上面的参考实现,insert2最快!

二分查找插入排序(Binary Search Insert)

我很喜欢这种把多个算法思想合并在一起使用的idea!

时间复杂度:\(O(n\log{n})\)

空间复杂度:\(O(1)\)

插入排序算法的关键在于,在已经有序的数据区中寻找插入的位置。二分查找插入排序算法结合查找算法,使用二分查找的方式来寻找插入位置,比单纯的循环遍历查找更快!下面是参考实现:

/* binary insert */
void binsert(int a[], size_t n){
    int t;
    size_t i,j,low,mid,high;

    for (i=1; i<n; ++i) {
        t = a[i];
        /* binary search */
        low = 0;
        high = i-1;
        while (low < high) {
            mid = (low+high) / 2;
            if (a[mid] > t) high = mid;
            else low = mid + 1;
        }
        /* here we get low == high */
        if (a[low] > t) j = low;
        if (a[low] <= t) j = low + 1;
        if (j == i) continue;
        memmove(a+j+1, a+j, sizeof(int)*(i-j));
        a[j] = t;
    }
}
void binsert2(int a[], size_t n){
    int t;
    size_t i,j,k,low,mid,high;

    for (i=1; i<n; ++i) {
        t = a[i];
        /* binary search */
        low = 0;
        high = i-1;
        while (low < high) {
            mid = (low+high) / 2;
            if (a[mid] > t) high = mid;
            else low = mid + 1;
        }
        /* here we get low == high */
        if (a[low] > t) j = low;
        if (a[low] <= t) j = low + 1;
        if (j == i) continue;
        k = i;
        while (k != j) {
            a[k] = a[k-1];
            k--;
        }
        a[j] = t;
    }
}

二分查找算法Python版的两种实现和比较

希尔排序(Shell)

对shell sort的分析很复杂,我这里的这点信息非常单薄,参考wiki。

不稳定

时间复杂度:\(O(n\cdot\log{n})\)

从此算法的实现上看,有好多for loop嵌套,但它的速度的确非常快!不能单纯地用for loop嵌套数量来断言算法的时间复杂度,很多快速的算法,实现都很复杂。

空间复杂度:\(O(1)\)

这里的Shell是科学家的名字!下面的参考实现代码虽然的我自己写的,但是过一段时间后,我自己也看不懂了.......可见这个算法的复杂!

插入排序在数据量很大,或者数据移动位次太多,会导致效率低下。很多排序算法都会想办法拆分序列然后组合(分而治之),希尔排序就是以一种特殊的方式对数据进行预处理,考虑到了「数据量和有序性」两个方面纬度来设计算法。使得序列前后之间小的尽量在前面,大的尽量在后面,进行若干次的分组计算交换后,最后一遍计算,即是一趟经典的插入排序。

参考实现:

void shell(int a[], size_t n){
    int t;
    size_t i,j,k,u;

    for (i=n/2; i>0; i/=2) {          // define distance
        for (j=0; j<i; ++j) {         // rounds of insert
            for (k=i+j; k<n; k+=i) {  // single insert
                t = a[k];
                for (u=k; u>=i; u-=i) {
                    if (a[u-i] > t)
                        a[u] = a[u-i];
                    else
                        break;
                }
                a[u] = t;
            }
        }
    }
}
void shell2(int a[], size_t n){
    int t;
    size_t i,j,k,u;

    for (i=n/2; i>0; i/=2) {          // define distance
        if (i == 1)
            binsert(a, n);
        else {
            for (j=0; j<i; ++j) {         // rounds of insert
                for (k=i+j; k<n; k+=i) {  // single insert
                    t = a[k];
                    for (u=k; u>=i; u-=i) {
                        if (a[u-i] > t)
                            a[u] = a[u-i];
                        else break;
                    }
                    a[u] = t;
                }
            }
        }
    }
}

希尔排序是将待排序的数组元素按位置下标的一定增量分组,逻辑上分成多个子序列,然后对各个子序列进行直接插入排序算法排序。算法最外层循环逐渐缩减增量,直到增量为1时,进行最后一次直接插入排序,排序结束。

希尔排序是基于直接插入排序的以下两点性质而提出的改进方法:

  1. 插入排序在对几乎已经排好序的数据操作时,效率很高,即可以达到线性排序的效率。
  2. 插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位。

希尔排序是非稳定排序算法,因DL.Shell于1959年提出而得名。

shell_sort.png

快速排序(Qsort)

不稳定

时间复杂度:\(O(n\cdot\log{n})\)

空间复杂度:\(O(1)\)

快速排序(Quicksort)是对冒泡排序的一种改进,并结合了分治的思想,由C. A. R. Hoare在1962年提出。快速排序的基本思想是:挖坑填数 + 分治法。

快排需要将序列变成两个部分,左序列全部小于一个数,右序列全部大于一个数,然后利用递归的方法,将左序列当成一个完整的序列再进行排序,同样把右序列也当成一个完整的序列进行排序。其中这个用来区分左右序列的数,在这个序列中是可以随机选取,可以取最左边,可以取最右边,当然也可以随机位置。但通常情况,我们取最左边的那个数。

参考实现:

void quick(int a[], size_t li, size_t ri){
    if (li >= ri) return;
    size_t i,p;

    p = li;
    for (i=li+1; i<=ri; ++i) {
        if (a[li] > a[i])
            if (++p != i)
                swap(a+p, a+i);
    }
    swap(a+li, a+p);

    quick(a, li, p);  // p-1 may cause overflow.
    quick(a, p+1, ri);
}

quick_sort.png

归并排序(Merge)

稳定

时间复杂度:\(O(n\cdot\log{n})\)

空间复杂度:\(O(n)\)

分而治之排序算法的典型,在数据量大且分散(重复少)的场景下非常快。

merge就是把两部分合并在一起,在合并的时候,考虑大小并进行排序,就是merge sort的基本思想。

自下而上(非递归)

下面是自下而上的非递归参考实现,我比较喜欢这个实现,代码量小:

int merge(int a[], size_t n){
    if (n > 1) {
        size_t i,j,k,s,hs,p;
        int *t = (int*)malloc(sizeof(int)*n);
        if (!t)
            return 1;

        for (s=2; s<n*2; s*=2) {
            hs = s/2;
            p = 0;
            for (i=0; i<n; i+=s) {
                j = i;
                k = i + hs;
                while (p<i+s && p<n) {
                    if (j==i+hs && k<i+s) {
                        t[p++] = a[k++]; continue;
                    }
                    if (k==i+s && j<i+hs) {
                        t[p++] = a[j++]; continue;
                    }
                    if (k>=n && j<n) {
                        t[p++] = a[j++]; continue;
                    }
                    t[p++] = a[j]<a[k]?a[j++]:a[k++];
                }
            }
            memcpy(a, t, sizeof(int)*n);
        }
        free(t);
    }
    return 0;
}

这个图很赞:

merge_sort.gif

自上而下(递归)

下面是自上而下的递归版本:

void _merge(int a[], int *t, size_t li, size_t m, size_t ri){
    size_t i = li;
    size_t j = m+1;
    size_t k = 0;
    size_t len = ri - li + 1;

    while (k < len) {
        if (i == m+1) {
            t[k++] = a[j++]; continue;
        }
        if (j == ri+1) {
            t[k++] = a[i++]; continue;
        }
        t[k++] = a[i]<a[j]?a[i++]:a[j++];
    }
    memcpy(a+li, t, sizeof(int)*len);
}

void _merge_r(int a[], int *t, size_t li, size_t ri){
    if (li < ri) {
        size_t m = (li+ri)/2;
        _merge_r(a, t, li, m);
        _merge_r(a, t, m+1, ri);
        _merge(a, t, li, m, ri);
    }
}

/* merge sort, recursive version */
int merge_r(int a[], size_t li, size_t ri){
    if (li < ri) {
        int *t = (int*)malloc(sizeof(int)*(ri-li+1));
        if (!t)
            return 1;
        _merge_r(a, t, li, ri);
        free(t);
    }
    return 0;
}

算法逻辑示意图:

merge_recursive_sort.png

从测试结果看,merge_r递归版本总是要比merge非递归版本快一丢丢!

归并排序Python版的两种实现和比较

堆排序(Heapsort)

不稳定

时间复杂度:\(O(n\cdot\log{n})\)

空间复杂度:\(O(1)\)

堆是一棵完全二叉树(complete binary tree),分为大根堆小根堆,完全二叉树中所有节点均大于等于(或小于等于)它的孩子节点,所以这里就分为两种情况:

sorted_heap.png

A complete binary tree is a binary tree in which every level, except possibly the last, is completely filled, and all nodes in the last level are as far left as possible.

完全二叉树通常采用数组而不是链表存储,采用数组存储更节省内存,从node到其children nodes,计算index就可以了。按照惯例,index从0开始,假设某node在array中的index是k,那么,它的两个子节点的index分别是2k+12k+2

/* The Complete Binary Tree has a very import feature:
 * if i is index+1, which means index starts from one, not zero,
 * a[2i] is the left node of a[i], a[2i+1] is just the right node,
 * so, a[i/2] is always the parent of a[i],
 * so, if there are n nodes, a[n/2] is the last node which has leaves. */


/* Adjust heap, assuming from a[s+1] to a[e] is already an adjusted heap.
 * We only need to find the right place for a[s]. */
static void
_heap_adjust(int a[], size_t s, size_t e)  // s and e are index+1
{
    size_t i;

    for (i=s*2; i<=e; i*=2) {
        if (i+1<=e && a[i-1]<a[i]) ++i;  // find the bigger child
        if (a[s-1] < a[i-1]) {
            swap(a+s-1, a+i-1);
            s = i;
        }
        else break;
    }
}


void
heapify(int a[], size_t n)
{
    if (n > 1) {
        size_t i;

        /* create big heap  */
        for (i=n/2; i>0; --i) {
            _heap_adjust(a, i, n);
        }

        /* sorting */
        for (i=n; i>1; --i) {
            swap(a, a+i-1);
            _heap_adjust(a, 1, i-1);
        }
    }
}

以上代码,在数组的基础上,先调整heap,调整完后,最大的数就在顶点,然后将最大的数放交换到最后,继续调整缩小后的heap,继续交换......额,这段代码不标准,创建heap的过程,应该是一个不断add的过程,在add过程中,始终保持heap特性。heap创建好后,一路pop-max,得到的就是一个排好序的结果。以上这段代码的唯一优势,就是没有使用任何额外的空间。(现在内存便宜了...)

各种binary tree的定义

A rooted binary tree has a root node and every node has at most two children.

A full binary tree (sometimes referred to as a proper or plane or strict binary tree) is a tree in which every node has either 0 or 2 children.

A perfect binary tree is a binary tree in which all interior nodes have two children and all leaves have the same depth or same level.

heap这种数据结构常用来实现priority queue,这种queue,出队的总是最高优先级(array[0]),\(O(1)\),入队后保持heap队形,\(O(\log{n})\),初始创建heap的过程,\(O(n)\)。

queue这种数据结构的基本操作,就是入队和出队,常见的有fifo(queue),lifo(stack)。

学习Binary Tree

二叉树排序

不稳定,所有数据在遍历排序树的时候,都重写了一次。

时间复杂度:\(O(n\log_2n)\)

空间复杂度:\(O(m*n)\)

二叉树排序的基本原理:使用第一个元素作为根节点,如果之后的元素比第一个小,则放到左子树,否则放到右子树,之后按中序遍历。

二叉排序树(二叉查找树),Binary Search Tree,它是一种特殊结构的二叉树。

其定义为:二叉树排序树或者是一棵空树,或者是具有如下性质的二叉树:

  1. 若它的左子树非空,则左子树上所有结点的值均小于根结点的值;
  2. 若它的右子树非空,则右子树上所有结点的值均大于(或大于等于)根结点的值;
  3. 它的左右子树也分别为二叉排序树
typedef struct bstree_node {
    int val;
    size_t count;
    struct bstree_node *left;
    struct bstree_node *right;
} BSTNODE;


static BSTNODE* insert_bstree(BSTNODE *root, int a, BSTNODE *node){
    if (root == NULL) {
        node->left = node->right = NULL;
        node->val = a;
        node->count = 1;
        root = node;
    }
    else if (a < root->val) {
        root->left = insert_bstree(root->left, a, node);
    }
    else if (a == root->val) {
        root->count++;
    }
    else if (a > root->val) {
        root->right = insert_bstree(root->right, a, node);
    }
    return root;
}


static void walk_bstree(int a[], BSTNODE *root, size_t *i){
    if (root != NULL) {
        walk_bstree(a, root->left, i);
        while (root->count--)
            a[(*i)++] = root->val;
        walk_bstree(a, root->right, i);
    }
}


int bstree(int a[], size_t n){
    if (n > 1) {
        BSTNODE *bmem = (BSTNODE*)malloc(sizeof(BSTNODE)*n);
        if (bmem == NULL) return 1;
        size_t *mi = (size_t*)malloc(sizeof(size_t));
        if (mi == NULL) {
            free(bmem);
            return 1;
        }
        BSTNODE *root = NULL;

        for (size_t i=0; i<n; ++i)
            root = insert_bstree(root, a[i], bmem+i);

        *mi = 0;
        walk_bstree(a, root, mi);
        free(bmem);
    }
    return 0;
} 

基数排序(Radix)

时间复杂度:\(O(n)\)

空间复杂度:\(O(m*n+k)\)

基数排序(Radix Sort)属于“分配式排序”,又称为“桶子法”。

/* Both radix and count are based on bucket sort idea!  */
typedef struct intdata {
    int val;
    struct intdata *link;  // to next
} INTD;


typedef struct {
    UINT radix;
    INTD *head;
    INTD *tail;
} RADIX;


/* radix is just like base */
UINT
get_radix(int a, UINT order)
{
    a = abs(a);
    for (UINT i=0; i<order; ++i) a /= 10;
    return a%10;
}


/* pos: boundary between negative and positive int,
 * pos is the first positive int position. */
size_t
split_np(int a[], size_t n)
{
    size_t pos=0,i;

    while (a[pos]<0 && pos<n) ++pos;
    if (pos != n) {
        for (i=pos+1; i<n; ++i)
            if (a[i] < 0) {
                swap(a+i, a+pos++);
            }
    }
    return pos;
}


static int
distribute(int a[], RADIX radix[], INTD *ad, size_t si, size_t n, UINT order)
{
    size_t i,j,k=0;
    INTD *t;

    for (i=si; i<n; ++i) {
        j = get_radix(a[i], order);
        if (j != 0) k=1;
        t = ad + i - si;
        t->val = a[i];
        t->link = NULL;
        if (radix[j].head == NULL) {
            radix[j].head = t;
            radix[j].tail = t;
        }
        else {
            radix[j].tail->link = t;
            radix[j].tail = t;
        }
    }
    return k;
}


static void
collect(int a[], RADIX radix[], size_t si, int ascend)
{
    size_t i,j=si;
    INTD *t;
    UINT b[] = {0,1,2,3,4,5,6,7,8,9};

    if (ascend == 0)
        for (i=0; i<10; ++i) b[i] = 9-i;

    for (i=0; i<10; ++i) {
        while (radix[b[i]].head != NULL) {
            t = radix[b[i]].head;
            radix[b[i]].head = radix[b[i]].head->link;
            a[j++] = t->val;
        }
    }
}


int
radix(int a[], size_t n)
{
    if (n > 1) {
        UINT k,order;
        RADIX radix[] = {
                    {0, NULL, NULL},
                    {1, NULL, NULL},
                    {2, NULL, NULL},
                    {3, NULL, NULL},
                    {4, NULL, NULL},
                    {5, NULL, NULL},
                    {6, NULL, NULL},
                    {7, NULL, NULL},
                    {8, NULL, NULL},
                    {9, NULL, NULL},
              };
        /* first of all, try to get enough memory */
        INTD *ad = (INTD*)malloc(sizeof(INTD)*n);
        if (ad == NULL) return 1;

        const size_t pos = split_np(a, n);
        /* negative part */
        if (pos != 0) {
            order = 0;
            do {
                k = distribute(a, radix, ad, 0, pos, order++);
                collect(a, radix, 0, 0);
            } while (k);
        }

        /* positive part */
        if (pos != n) {
            order = 0;
            do {
                k = distribute(a, radix, ad, pos, n, order++);
                collect(a, radix, pos, 1);
            } while (k);
        }

        free(ad);
    }
    return 0;
}

radix_sort.gif

计数排序(Count)

时间复杂度:\(O(n)\)

空间复杂度:??

计数排序是一种特殊的桶排序,每个桶的大小为1,每个桶不在用List表示,而通常用一个值用来计数。在「设计具体算法的时候」,先找到最小值min,再找最大值max,然后创建这个区间大小的内存数组。

此算法适合数据的值跨度不大的场景,否则消耗的内存会非常多,可能会多到算法执行失败的程度。

int
get_max(int a[], size_t n)
{
    assert(n != 0);
    int t = a[0];
    for (size_t i=1; i<n; ++i)
        if (t < a[i]) t = a[i];
    return t;
}


int
get_min(int a[], size_t n)
{
    assert(n != 0);
    int t = a[0];
    for (size_t i=1; i<n; ++i)
        if (t > a[i]) t = a[i];
    return t;
}


int
count(int a[], size_t n, size_t mem_limit)
{
    if (n > 1) {
        if (mem_limit == 0) return 3;
        size_t i,j=0;
        int m = get_min(a, n);
        int k = get_max(a, n);
        size_t mlen = k-m+1;
        if (mlen == 0) {  // int overflow
            if (sizeof(size_t) > sizeof(int))
                mlen = ((size_t)max_int()+1)*2;  // int size
            else if (sizeof(size_t) == sizeof(int))
                mlen = -1;  // biggest size_t
            else assert(0);
        }
        /* memory limit check  */
        if (mlen > mem_limit) return 3;
        /* start malloc */
        UINT8 *counter = (UINT8*)malloc(mlen);
        if (counter == NULL) return 1;  // OOM
        memset(counter, 0, mlen);
        /* count */
        for (i=0; i<n; ++i) {
            if (counter[a[i]-m] == 255) {
                free(counter);
                return 2;  // too man duplicated
            }
            counter[a[i]-m]++;
        }
        /* sort */
        for (i=0; i<mlen; ++i)
            while (counter[i]--) a[j++] = i + m;
        free(counter);
    }
    return 0;
}

测试

测试代码在这里(private):https://github.com/xinlin-z/internal_sorting_algorithm

单元测试

$ python3 utest.py -v

# common.c:
sizeof(int) =  4
sizeof(size_t) =  8

test_cmp_int (__main__.test_common_c) ... ok
test_max_int (__main__.test_common_c) ... ok
test_min_int (__main__.test_common_c) ... ok

# sort.c:
sizeof(int) =  4
sizeof(size_t) =  8

test_binsert (__main__.test_sort_c) ... ok
test_binsert2 (__main__.test_sort_c) ... ok
test_btree (__main__.test_sort_c) ... ok
test_bubble (__main__.test_sort_c) ... ok
test_count (__main__.test_sort_c) ... ok
test_get_max (__main__.test_sort_c) ... ok
test_get_min (__main__.test_sort_c) ... ok
test_get_radix (__main__.test_sort_c) ... ok
test_heapify (__main__.test_sort_c) ... ok
test_insert (__main__.test_sort_c) ... ok
test_insert2 (__main__.test_sort_c) ... ok
test_merge (__main__.test_sort_c) ... ok
test_merge_r (__main__.test_sort_c) ... ok
test_quick (__main__.test_sort_c) ... ok
test_radix (__main__.test_sort_c) ... ok
test_selects (__main__.test_sort_c) ... ok
test_shell (__main__.test_sort_c) ... ok
test_shell2 (__main__.test_sort_c) ... ok
test_split_np (__main__.test_sort_c) ... ok

----------------------------------------------------------------------
Ran 22 tests in 12.844s

OK

速度测试

$ python3 test_sort_time.py
# test sort time (cpu) with same data:

sizeof(int) = 4
sizeof(size_t) = 8
Data1: 200000 integers in random from -500 to 500 (many duplicated)
     Algo Name                   Time(s)
1    count                    0.0002865390
2    radix                    0.0063802410
3    btree                    0.0100811730
4    merge_r                  0.0127625810
5    merge                    0.0130818440
6    quick                    0.0144666640
7    heapify                  0.0145566040
8    shell                    0.0149387610
9    np.sort(numpy)           0.0159584780
10   qsort(glibc)             0.0162465800
11   shell2                   0.0187523920
12   list.sort(python)        0.0216216080
13   sorted(python)           0.0228952610
14   binsert2                 0.6138220350
15   binsert                  0.6154471680
16   insert2                  3.3854721020
17   insert                   4.6864306390
18   selects                  7.7486197970
19   bubble                   48.3289254720

Data2: 200000 integers in random from -2147483648 to 2147483647
     Algo Name                   Time(s)
1    quick                    0.0121813170
2    merge_r                  0.0154146260
3    merge                    0.0159740240
4    heapify                  0.0164701320
5    shell                    0.0185461380
6    qsort(glibc)             0.0195465400
7    radix                    0.0201735410
8    np.sort(numpy)           0.0210044280
9    shell2                   0.0234900200
10   btree                    0.0437949200
11   sorted(python)           0.0453653430
12   list.sort(python)        0.0466786440
13   binsert                  0.5995638920
14   binsert2                 0.6202496610
15   insert2                  3.3880157650
16   insert                   4.6766841610
17   selects                  7.7102677510
18   bubble                   48.3651019260
19   count                    E:3

本文链接:https://cs.pynote.net/ag/202206101/

-- EOF --

-- MORE --