初阶数据结构学习记录——열둘 排序(1)

目录

一、插入排序

直接插入排序

希尔排序

测试性能(加上堆排序)

二、选择排序

选择排序

三、交换排序

冒泡排序

四、快速排序


常见排序算法实现:

插入排序:直接插入排序,希尔排序

选择排序:选择排序,堆排序

交换排序:冒泡排序,快速排序

归并排序

一、插入排序

直接插入排序

直接插入就是插入一个数据,和之前的数据比较,保证顺序规则不变。我们需要事先存一下新数据,然后让新数据挨个和前面比较。

void InsertSort(int* a, int n)//时间复杂度如果是原本逆序,O(N^2), 如果是升序,O(N)
{
    for (int i = 0; i < n - 1; ++i)
    {
        int end = i;
        int tmp = a[end + 1];
        while (end >= 0)
        {
            if (tmp < a[end])
            {
                a[end + 1] = a[end];
                --end;
            }
            else
            {
                break;
            }
        }
        a[end + 1] = tmp;
    }
}

现在排的是升序,传进来一个数组和数组大小后,从头开始,第一个和第二个数据进行比较,如果第二个小于第一个,第二个就变成第一个的数据,但第二个数据并没有丢,因为事先已经存到了tmp,end继续--,再往前一个找数据比较大小;带入一个例子来看,假设5 3 1,这三个数字比较,当end = 0时,end是5, end + 1是3,tmp是3,小于,那么end + 1就变成了5,--end,end变成-1,不符合条件,这时候下标为end + 1,也就是0的位置换成tmp,也就是3,此时整个序列变成了3 5 1。

当end = 1时,end是5,end + 1就是1,tmp = 1,1比5小,1的位置换成5,这时候是3 5 5,--end,此时end是下标为0的位置,end符合条件,进入循环,tmp是1,end指向3,tmp小于end,那么end + 1变成3,此时就是3 3 5,--end,end为 -1,不符合条件,那么把end + 1,也就是0的位置换成tmp,这时候就是1 3 5了。

这样逐渐就把比tmp大的数据往后挪了一下,直到比首元素小,end再次--小于0后,就退出循环,那么end+1指向首元素,首元素就变成tmp。然后再次开启循环。

如代码里注释所写,最坏的情况就是原始数据是逆序,而我们要排升序。

希尔排序

直接插入排序确实有高效排序的时候,但逆序也确实低效。希尔排序会更好的处理逆序。希尔排序的思路先做一个预排序,再做插入排序。预排序是要排成一个近似成序的数组,对于逆序的数组,9要挪到最后需要好几步,为了减少步数,预排序中把数据分组,设定步长,让9一次就退后步长步,这样就能尽量减少逆序的时间消耗。

假设步长为3,那么第一个数据就后离它3步的数据比较。

9和6比较,再和3比较;8和5比较,再和2比较;7和4比较,再和1比较。可以看出,其实步长多少就可以分为多少组。放到具体下标上,也就是下标+3。所以tmp需要end + gap。以及挪动时也需要挪gap步。

现在看这段代码

void ShellSort(int* a, int n)
{
    int gap = 3;
    for (int i = 0; i < n - gap; i += gap)
    {
        int end = i;
        int tmp = a[end + gap];
        while (end >= 0)
        {
            if (tmp < a[end])
            {
                a[end + gap] = a[end];
                end -= gap;
            }
            else
            {
                break;
            }
        }
        a[end + gap] = tmp;
    }
}

其实还是一样,只是可以把之前的代码看做步长为1。加上循环for (int i = 0; i < n - gap; i += gap)

这样就是一个个小组内排序,end = 0。9到6之间进行排序,排完后,i +3,end也 + 3,然后就是6到3的排序。i < n - gap ,这样a[end]就会最多指向4,避免越界,也确定了最后一个排序的开始位置。

但是现在这个代码只是走了一组,所以我们还需要j,走完所有组。

void ShellSort(int* a, int n)
{
    int gap = 3;
    for (int j = 0; j < gap; j++)
    {
        for (int i = j; i < n - gap; i += gap)
        {
            int end = i;
            int tmp = a[end + gap];
            while (end >= 0)
            {
                if (tmp < a[end])
                {
                    a[end + gap] = a[end];
                    end -= gap;
                }
                else
                {
                    break;
                }
            }
            a[end + gap] = tmp;
        }
    }
}

i里面的循环里,一组有n / gap个数据,所以挪动1 到 n / gap次,总共有gap组,时间复杂度就O(N ^ 2) , O(N)。

 简化一下代码

void ShellSort(int* a, int n)
{
    int gap = 3;
    for (int i = 0; i < n - gap; ++i)
    {
        int end = i;
        int tmp = a[end + gap];
        while (end >= 0)
        {
            if (tmp < a[end])
            {
                a[end + gap] = a[end];
                end -= gap;
            }
            else
            {
                break;
            }
        }
        a[end + gap] = tmp;
    }
}

这个是怎么样呢?排完第一组第一个,再排第二组第一个,再排第三组第一个,然后又排第一组。

现在把gap定型

void ShellSort(int* a, int n)
{
    // gap > 1 预排序
    // gap == 1 直接插入排序
    int gap = n;
    while (gap > 1)
    {
        // gap = gap / 2; 
        // gap = gap / 3;  // 9  8 不能保证最后一次一定是1
        gap = gap / 3 + 1;  // 9  8
        for (int i = 0; i < n - gap; ++i)
        {
            int end = i;
            int tmp = a[end + gap];
            while (end >= 0)
            {
                if (tmp < a[end])
                {
                    a[end + gap] = a[end];
                    end -= gap;
                }
                else
                {
                    break;
                }
            }
            a[end + gap] = tmp;
        }
    }
}

gap /  3也可。不过gap / 2可以保证gap最后一定为1,为1的时候就是直接插入排序。gap / 3 + 1就可。 

对于预排序而言

gap越大,大的数越快排到后面,小的数越快排到前面,但越不接近有序。gap值越大,那么n-gap也就越小,我们能调的数据下标也越小了,自然也就越不接近有序。

gap越小,数据跳动越慢,越接近有序。

测试一下

void PrintArray(int* a, int n)
{
    for (int i = 0; i < n; ++i)
    {
        printf("%d ", a[i]);
    }
    printf("\n");
}

void ShellSort(int* a, int n)
{
    //gap > 1 预排序
    //gap == 1 直接插入排序
    int gap = n;
    while (gap > 1)
    {
        gap = gap / 2; 
        //gap = gap / 3;  // 9  8 不能保证最后一次一定是1
        //gap = gap / 3 + 1;  // 9  8
        for (int i = 0; i < n - gap; ++i)
        {
            int end = i;
            int tmp = a[end + gap];
            while (end >= 0)
            {
                if (tmp < a[end])
                {
                    a[end + gap] = a[end];
                    end -= gap;
                }
                else
                {
                    break;
                }
            }
            a[end + gap] = tmp;
        }
    }
}

void TestShellSort()
{
    //int a[] = { 9, 1, 2, 5, 7, 4, 8, 6, 3, 5 };
    int a[] = { 9,8,7,6,5,4,3,2,1,0,5,4,2,3,6,2,0,2,1,-1,-2,-1,-3 };
    PrintArray(a, sizeof(a) / sizeof(int));
    ShellSort(a, sizeof(a) / sizeof(int));
    PrintArray(a, sizeof(a) / sizeof(int));
}

int main()
{
    //TestInsertSort();
    TestShellSort();
    return 0;
}

每换完一遍打印一下。看结果可以发现,第一次换,几个比较大的数就已经来到后面了,然后形成一个前小后大的序,再直接插入排序,就排好了。

测试性能(加上堆排序)

测试性能的时候用Release模式。

void Test()
{
    srand(time(0));
    const int N = 1000;
    int* a1 = (int*)malloc(sizeof(int) * N);
    int* a2 = (int*)malloc(sizeof(int) * N);

    for (int i = 0; i < N; ++i)
    {
        a1[i] = rand();
        a2[i] = a1[i];
    }

    int begin1 = clock();
    InsertSort(a1, N);
    int end1 = clock();

    int begin2 = clock();
    ShellSort(a2, N);
    int end2 = clock();

    printf("InsertSort:%d\n", end1 - begin1);
    printf("ShellSort:%d\n", end2 - begin2);

    free(a1);
    free(a2);
}

后面再写几个排序算法后,就加上a3,a4等。

1000个数据时,这两个没有多大差距,给上10万个。 

816毫秒,7毫秒。

再把堆排序弄过来算一算。 

void Test()
{
    srand(time(0));
    const int N = 1000;
    int* a1 = (int*)malloc(sizeof(int) * N);
    int* a2 = (int*)malloc(sizeof(int) * N);
    int* a3 = (int*)malloc(sizeof(int) * N);

    for (int i = 0; i < N; ++i)
    {
        a1[i] = rand();
        a2[i] = a1[i];
        a3[i] = a1[i];
    }

    int begin1 = clock();
    InsertSort(a1, N);
    int end1 = clock();

    int begin2 = clock();
    ShellSort(a2, N);
    int end2 = clock();

    int begin3 = clock();
    HeapSort(a3, N);
    int end3 = clock();

    printf("InsertSort:%d\n", end1 - begin1);
    printf("ShellSort:%d\n", end2 - begin2);
    printf("HeapSort:%d\n", end3 - begin3);

    free(a1);
    free(a2);
    free(a3);
}

 

其实来讲希尔排序和堆排序速度都很快,差别不多,到千万以上的数据时,两个排序都需要几百毫秒。有时候希尔排序会更快。当百万数据的时候再带上直接插入排序就会慢很多,Release模式也需要几十秒。所以后面只用十万个随机数。

希尔排序算法的时间复杂度是一个模糊的数据。真正求希尔排序的时间复杂度需要浑厚的数学功底,也不一定算得出来,希尔排序的时间复杂度仍然是一个难题。我们可以求一些最坏最好这样的情况,不过这里就不作分析了。现在普遍认可的数据是O(N^1.3)。

二、选择排序

选择排序

这个排序的思想很简单,从第一个开始遍历,遍历完所有的后,把最小的数选出来,放在前面。一次次遍历就结束了。

不过这个效率不高,我们不如在一次遍历中选出最大和最小的,放在右边和左边,然后再次遍历剩下的。

// O(N^2)
// 跟直接插入排序比较,谁更好 -- 插入
// 插入适应性很强,对于有序,局部有序,都能效率提升
// 任何情况都是O(N^2)  包括有序或接近有序
void SelectSort(int* a, int n)
{
    int begin = 0, end = n - 1;
    while (begin < end)
    {
        int mini = begin, maxi = begin;
        for (int i = begin + 1; i <= end; ++i)
        {
            if (a[i] < a[mini])
            {
                mini = i;
            }

            if (a[i] > a[maxi])
            {
                maxi = i;
            }
        }

        Swap(&a[begin], &a[mini]);
        if (maxi == begin)
            maxi = mini;

        Swap(&a[end], &a[maxi]);
        ++begin;
        --end;
    }
}

选择排序需要注意的就是max和min的值变化,所以在交换时有做判断。

看一下十万个数的性能

因为选择排序,10万个数程序的运行就需要几秒时间。

而堆排序比它好的多的原因就是因为堆排序不需要每个都要比,堆排序借助二叉树结构,和堆顶比较即可,然后进行向下调整算法,logN的结果也不大。

三、交换排序

冒泡排序

冒泡排序是个耳熟能详的排序了。每一次把大的数据往后放。

做测试

还是10万个数据。再做1万个数据

百万数据就慢了,计算机需要一段时间才行。不过有时候会发现冒泡比选择还要慢,现在对冒泡做一下优化

//冒泡排序
void BubbleSort(int* a, int n)// O(N^2)
{
    for (int j = 0; j < n; ++j)
    {
        int exchange = 0;
        for (int i = 1; i < n - j; ++i)
        {
            if (a[i - 1] > a[i])
            {
                Swap(&a[i - 1], &a[i]);
                exchange = 1;
            }
        }
        if (exchange == 0)
            break;
    }
}

 再走10万个数据

没有什么区别呀,这做不做优化,冒泡也还是要1000多毫秒啊。其实来讲是因为,冒泡排序并不能适应乱序。冒泡还是需要一个个比较,所以乱序情况下选择和冒泡并没有什么区别,只有在有序的情况下冒泡才会有优势。 

到现在为止,会发现堆排和希尔都很快,插入排序有一定的价值,冒泡和选择都不算太好。

也可以对随机数做些更改,让其更随机。

接下来的博客看一个顾名思义的排序以及一样厉害的归并排序。

四、快速排序

既然叫快排,它既然是很快的。但是快排内容很多,需要慢慢理解。

举一个例子

数组 6  1  2  7  9  3  4  5  10  8。

设定key为6,然后一左L一右R走,右边先走,找到比key小的,找到后停下来,左边再走找到比key大的,找到后停下来,两者互换,小的就被调到左边去了,小的数就来到了L的位置,在它和key之间的数也一定小于key,然后R和L再重复之前操作,R到4这里停下,L到9这里停下,然后互换,最后LR都来到3,3和6互换。这样换完后还没结束,因为这时候以最后LR位置为轴,左右区间还不是有序,那么这时候左右区间就是一个子问题了,在两个区间里继续做上述操作,到实际代码上也就是递归做法。

具体地可以多画图,这里就不展示了。

规律:如果左边做key,右边就先走,能够保证相遇位置比key小;右边做key,左边就先走,能够保证相遇位置比key大。

实际上,相遇的情况有这两种,R停下,L遇到了R,相遇位置就是R停住的位置;或者L停下,R遇到了L,相遇位置就是L停住的位置。

key为6,第一遍走完后,L在7, R在5,交换,L位置是5, R位置是7,L位置小于key,假如9  3  4改为9   13   14  ,那么R就会一直往左走,直到遇见L,这也就是第二种情况,L停住的位置原本数据就是大于6的。

第一种情况就是按照这个数据 6  1  2  7  9  3  4  5  10  8。走,R会在3处停下,而L就会遇到R,而R停住的位置是一定小于key的。

当然也有极端情况,比如都比6大,R就一往无前地找到L,即使这样,相遇的位置是6,也<=key,所以第二种情况就是相遇位置 >= key,第一种情况则是 <= key。

代码实现

先写全部区间的遍历,这里要考虑到可能某个变量,另一个变量走全部。

以及还有一个情况,两边区间都有和key一样的值,这样即使停下来交换了,那么左右两边仍然动不了,所以要写>= 和 <=。

当停下后,交换key和停下位置的值,这时候分成两个区间,把停下位置的值给到key,然后分成[begin, key - 1] 和[key + 1, end]两个区间继续递归。

void QuickSort(int* a, int begin, int end)//用begin和end来表示区间
{
    if (begin >= end)
    {
        return;
    }
    int left = begin, right = end;
    int key = left;//最终要换的时候跟最左边的换,所以这里=left
    while (left < right)
    {
        //右边先走,找小
        while (left < right && a[right] >= a[key])
        {
            right--;
        }
        //左边再走,找大
        while (left < right && a[left] <= a[key])
        {
            left++;
        }
        Swap(&a[left], &a[right]);
    }
    Swap(&a[left], &a[key]);
    key = left;
    //[begin, key - 1]  [key + 1, end]
    QuickSort(a, begin, key - 1);
    QuickSort(a, key + 1, end);
}

再做性能测试。

这里就不弄选择和冒泡排序了。

 百万数据

所以其实堆排和快排和希尔排都是差不多等级的。

下一篇继续优化快排以及写归并排序。

结束。 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值