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

目录

一、归并排序

二、非递归归并

end1越界

begin2越界

end2越界

三、排序稳定性

插入排序(希尔和插入)

选择排序(选择和堆排)

交换排序(快排和冒泡)

归并排序

四、特殊的例子(快排)


一、归并排序

归并的思路其实和二叉树,快排都有点像。归并希望左、右半区间有序。和快排不同,先分裂后排序,一半一半分,分到最后每个区间只剩一个1个数字,这个区间一定是有序的,因为只有一个数字,往回走,两个数排序一下,继续往回走,逐渐排序好后,回到初始的数组,这时候整个数组就已经有序了。这里我们用另一个数组来临时存储分出来的区间去排序,排好后再拷贝回去

要开辟一块新空间的话我们需要在另一个函数里开辟,不能在一个空间重复开辟。

//归并排序
void _MergeSort(int* a, int begin, int end, int* tmp)//时间复杂度:O(N*logN),空间复杂度:O(N)
{
    if (begin >= end)
        return;
    int mid = (begin + end) / 2;
    // [begin, mid] [mid+1, end] 递归让子区间有序
    _MergeSort(a, begin, mid, tmp);
    _MergeSort(a, mid + 1, end, tmp);
    // 归并[begin, mid] [mid+1, end]
    int begin1 = begin, end1 = mid;
    int begin2 = mid + 1, end2 = end;
    int i = begin;//区间左右端都记录下来,然后i从左端开始走
    while (begin1 <= end1 && begin2 <= end2)
    {
        if (a[begin1] <= a[begin2])
        {
            tmp[i++] = a[begin1++];
        }
        else
        {
            tmp[i++] = a[begin2++];
        }
    }
    while (begin1 <= end1)
    {
        tmp[i++] = a[begin1++];
    }
    while (begin2 <= end2)
    {
        tmp[i++] = a[begin2++];
    }
    memcpy(a + begin, tmp + begin, sizeof(int) * (end - begin + 1));
}

void MergeSort(int* a, int n)
{
    int* tmp = (int*)malloc(sizeof(int) * n);
    if (tmp == NULL)
    {
        perror("malloc fail");
        exit(-1);
    }
    _MergeSort(a, 0, n - 1, tmp);
    free(tmp);
    tmp = NULL;
}

由于是先分裂,所以先递归出区间,直到一个区间只剩2个数字,1个数字的话,begin = end就会返回,因为1个数字必然是有序的,所以直接返回,当只剩2个变量时,begin = 0,end = 1。一个区间分成两份,用四个变量记录下来两个区间两端,进入while,这两个区间从头开始一个个比较,小的就放进tmp里,这样就有序了,当然这里会出现哪个区间先结束的情况,所以后面两个while就把没出完数据的那个区间都拿出来,然后拷贝到a里,这里用a + begin是因为不可能每次都从a的首元素处拷贝,所以加上begin,拷贝每个排好的区间。

可画递归展开图来理解代码。

归并的时间复杂度不难算,整个递归像一个二叉树,高度也就是logN。每一层的归并操作可以看出就是N,所以归并是个很正规的O(N * logN)。而空间复杂度也是经典的O(N)。因为额外开了tmp这个N个数据的空间。

而上一篇的快排虽然没有额外开辟空间,但它也是递归,递归层数也是logN。每一层快排没有额外的操作,是O(1),所以它的空间复杂度是O(logN)。

归并的空间复杂度刚才算了tmp,不过递归层数也要计入,所以应该是N + logN,但N很大时,logN很小,可以忽略不计。所以还是O(N)。

二、非递归归并

归并的非递归需要用到链表,不过我们这里可以不用这样。和快排不一样,归并并不一定要按照递归做非递归。归并的分区间是对半分,分到每个区间一个数。所以我们重点在于控制单个区间的大小。定义一个range变量控制区间,这里其实和斐波那契数列改非递归思路一样,斐波那契数列非递归就是先写出头两个数字,然后在循环里加,得到下一个数字,然后头两个数字也往后移一下。归并的非递归则是先把数组里所有的数分成若干个两个数的单位,两个数之间排序,然后让range * 2,四个数之间再排序,然后再次 * 2,变成8个数之间排序等等。虽然这个思路很不错,但是实际写代码时细节问题却有很多。这个方法也需要一个tmp。

void MergeSortNonR(int* a, int n)
{
    int* tmp = (int*)malloc(sizeof(int) * n);
    if (tmp == NULL)
    {
        perror("malloc fail");
        exit(-1);
    }
    //归并每组数据个数,从1开始,因为1个认为是有序的,可以直接归并
    int range = 1;
    for (int i = 0; i < n; i += (range * 2))
    {
        //[begin1, end1] [begin2, end2]
        int begin1 = i, end1 = i + range - 1;//从1个数据开始,所以这里i = 0,整个区间就是[0, 0]
        int begin2 = i + range, end2 = i + 2 * range - 1;
        int j = i;
        //主要问题是防止end1,begin2,end2越界
        while (begin1 <= end1 && begin2 <= end2)
        {
            if (a[begin1] <= a[begin2])
            {
                tmp[j++] = a[begin1++];
            }
            else
            {
                tmp[j++] = a[begin2++];
            }
        }
        while (begin1 <= end1)
        {
            tmp[j++] = a[begin1++];
        }
        while (begin2 <= end2)
        {
            tmp[j++] = a[begin2++];
        }
        memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));
    }
    free(tmp);
    tmp = NULL;
}

 进入for循环后我们还是和之前一样的归并,所以直接复制过来,改变变量,而memcpy可以在整个循环结束后再拷贝,也可以一次次地拷贝,只不过在内部拷贝会更容易控制些。range * 2继续下一个循环。

现在range一直都是1,i循环下来,区间一直都是一个变量,所以还得套上一个while,让range*2,继续扩大范围去排序

void MergeSortNonR(int* a, int n)
{
    int* tmp = (int*)malloc(sizeof(int) * n);
    if (tmp == NULL)
    {
        perror("malloc fail");
        exit(-1);
    }
    //归并每组数据个数,从1开始,因为1个认为是有序的,可以直接归并
    int range = 1;
    while (range < n)
    {
        for (int i = 0; i < n; i += (range * 2))
        {
            //[begin1, end1] [begin2, end2]
            int begin1 = i, end1 = i + range - 1;//从1个数据开始,所以这里i = 0,整个区间就是[0, 0]
            int begin2 = i + range, end2 = i + 2 * range - 1;
            int j = i;
            //主要问题是防止end1,begin2,end2越界
            while (begin1 <= end1 && begin2 <= end2)
            {
                if (a[begin1] <= a[begin2])
                {
                    tmp[j++] = a[begin1++];
                }
                else
                {
                    tmp[j++] = a[begin2++];
                }
            }
            while (begin1 <= end1)
            {
                tmp[j++] = a[begin1++];
            }
            while (begin2 <= end2)
            {
                tmp[j++] = a[begin2++];
            }
            //归并一部分,拷贝一部分
            memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));
        }
        range *= 2;
    }
    free(tmp);
    tmp = NULL;
}

不过这个代码还没有结束。

void TestMergeSort()
{
    int a[] = { 10, 6, 7, 1, 3, 9, 4, 2 };
    MergeSortNonR(a, sizeof(a) / sizeof(int));
    PrintArray(a, sizeof(a) / sizeof(int));
}

按照这个例子,程序自然可以排序出来,但这里的问题正如对半分区间一样,并不是每个数组都可以完全对半分,总会有一些数字和数组外的数据组成了一个区间,所以程序就出来问题,比如排序10个数程序就错了,只要数据总数不是2的幂次方个就有问题。现在这个程序中间归并的过程没有问题,问题应当出现在拷贝或者开始归并之前是否缺少了判断。

为了观察越界问题,在确定好begin和end参数后打印出来看看。1-10这10个数的处理结果 

 那么如何规避越界问题?

从代码角度看,end1,begin2,end2都容易越界,begin1 = i,所以不太会越界。图中也是,三个都出现越界了。现在的越界情况可以分为三种。如果end1越界了,那么begin2和end2肯定都越界了,比如图中[8, 11]那一行;end1没越界,begin2越界了,end2也就越界了,比如图中[8, 9]那一行;end2自己越界了,比如图中[0, 7]那一行 ,所以要针对这三个情况来处理。

以10个数为例

end1越界

发生越界了,那么修正一下区间是不是就可行了?end1如果 >= n ,那么就改成n - 1,这样begin2 ,end2也就要被改成n和n - 1了,那么begin2这个区间就一定不会进入了,只排序begin1这个区间。这时候begin2这个区间就是[10, 9],实际上已经越界,不过下面的归并算法并不会让他进入循环,所以只是一个数字罢了,而end1是9,这样就可以避免越界问题的发生,数组所有元素也都进入了排序,这种解决办法对于后面的拷贝没有多少影响,全部排序完再拷贝(外部拷贝)或者排序一次拷贝一次(内部拷贝)都可。

还有另外一种方法,如果遇到越界了,我们就break直接退出,不排序了,这样的结果也不必担心,最终都会排序成功,但是break有一个问题,选择break的话外部拷贝就难以控制了,因为里面存在越界的时候,外部拷贝有可能就把随机值给拷贝回去了。

这里我们采用break的办法,使用修正区间的话后面两个越界问题的处理也会有相应的变化,下面再写。

begin2越界

这个比较简单,第二个区间越界了那就不归并了。直接break。

end2越界

end2越界,那我们直接修正即可。

这样最终的代码就是这样

    //归并每组数据个数,从1开始,因为1个认为是有序的,可以直接归并
    int range = 1;
    while (range < n)
    {
        for (int i = 0; i < n; i += (range * 2))
        {
            //[begin1, end1] [begin2, end2]
            int begin1 = i, end1 = i + range - 1;//从1个数据开始,所以这里i = 0,整个区间就是[0, 0]
            int begin2 = i + range, end2 = i + 2 * range - 1;
            printf("[%d,%d][%d,%d]\n", begin1, end1, begin2, end2);
            int j = i;
            //主要问题是防止end1,begin2,end2越界
            //修正区间 - 拷贝数据,归并完了整体拷贝 or 归并每组拷贝
            //end2越界对拷贝方式无影响;begin2和end1越界,修正就可以整体拷贝,break就只能每组拷贝
            if (end1 >= n)
            {
                //修正
                //end1 = n - 1;
                //不存在区间
                //begin2 = n;
                //end2 = n - 1;
                break;
            }
            else if (begin2 >= n)
            {
                //不存在区间
                //begin2 = n;
                //end2 = n - 1;
                
                break;
            }
            else if (end2 >= n)
            {
                end2 = n - 1;
            }
            while (begin1 <= end1 && begin2 <= end2)
            {
                if (a[begin1] <= a[begin2])
                {
                    tmp[j++] = a[begin1++];
                }
                else
                {
                    tmp[j++] = a[begin2++];
                }
            }
            while (begin1 <= end1)
            {
                tmp[j++] = a[begin1++];
            }
            while (begin2 <= end2)
            {
                tmp[j++] = a[begin2++];
            }
            //归并一部分,拷贝一部分
            memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));
        }
        range *= 2;
    }

那么如果end1越界 用修正方法,begin2越界也还是用修正,end2越界的话就不需要归并,那么修正一下,让这个区间进不去循环即可。

    int range = 1;
    while (range < n)
    {
        for (int i = 0; i < n; i += (range * 2))
        {
            //[begin1, end1] [begin2, end2]
            int begin1 = i, end1 = i + range - 1;//从1个数据开始,所以这里i = 0,整个区间就是[0, 0]
            int begin2 = i + range, end2 = i + 2 * range - 1;
            printf("[%d,%d][%d,%d]\n", begin1, end1, begin2, end2);
            int j = i;
            //主要问题是防止end1,begin2,end2越界
            //修正区间 - 拷贝数据,归并完了整体拷贝 or 归并每组拷贝
            //end2越界对拷贝方式无影响;begin2和end1越界,修正就可以整体拷贝,break就只能每组拷贝
            if (end1 >= n)
            {
                //修正
                end1 = n - 1;
                //不存在区间
                begin2 = n;
                end2 = n - 1;
                //break;
            }
            else if (begin2 >= n)
            {
                //不存在区间
                begin2 = n;
                end2 = n - 1;
                //break;
            }
            else if (end2 >= n)
            {
                end2 = n - 1;
            }
            while (begin1 <= end1 && begin2 <= end2)
            {
                if (a[begin1] <= a[begin2])
                {
                    tmp[j++] = a[begin1++];
                }
                else
                {
                    tmp[j++] = a[begin2++];
                }
            }
            while (begin1 <= end1)
            {
                tmp[j++] = a[begin1++];
            }
            while (begin2 <= end2)
            {
                tmp[j++] = a[begin2++];
            }
            //归并一部分,拷贝一部分
            //memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));
        }
        //整体拷贝
        memcpy(a, tmp, sizeof(int) * n);
        range *= 2;
    }

所有测试性能代码

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

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));
}

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

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

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

void TestMergeSort()
{
    //int a[] = { 6,1,2,7,9,3,4,5,8,10,8 };
    int a[] = { 6, 1, 2, 7, 9, 3, 4, 5, 6, 8 };
    //int a[] = {10,6,7,1,3,9,4,2 };
    MergeSortNonR(a, sizeof(a) / sizeof(int));
    PrintArray(a, sizeof(a) / sizeof(int));
}

void Test()
{
    srand(time(0));
    const int N = 100000;
    int* a1 = (int*)malloc(sizeof(int) * N);
    int* a2 = (int*)malloc(sizeof(int) * N);
    int* a3 = (int*)malloc(sizeof(int) * N);
    int* a4 = (int*)malloc(sizeof(int) * N);
    int* a5 = (int*)malloc(sizeof(int) * N);
    int* a6 = (int*)malloc(sizeof(int) * N);
    int* a7 = (int*)malloc(sizeof(int) * N);

    int y = 0, z = 0;
    for (int x = 0; x < N; ++x)
    {
        y = rand();
        if (y % 4 == 0 || y % 7 == 0 || y % 47 == 0)
        {
            y += z;
            ++z;
        }
        a1[x] = y;
        a2[x] = a1[x];
        a3[x] = a1[x];
        a4[x] = a1[x];
        a5[x] = a1[x];
        a6[x] = a1[x];
        a7[x] = a1[x];
    }

    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();

    int begin4 = clock();
    SelectSort(a4, N);
    int end4 = clock();

    int begin5 = clock();
    BubbleSort(a5, N);
    int end5 = clock();

    int begin6 = clock();
    QuickSort(a6, 0, N - 1);
    //QuickSortNonR(a6, 0, N - 1);
    int end6 = clock();

    int begin7 = clock();
    //MergeSort(a6, N);
    MergeSortNonR(a6, N);
    int end7 = clock();

    printf("InsertSort:%d\n", end1 - begin1);
    printf("ShellSort:%d\n", end2 - begin2);
    printf("HeapSort:%d\n", end3 - begin3);
    printf("SelectSort:%d\n", end4 - begin4);
    printf("BubbleSort:%d\n", end5 - begin5);
    printf("QuickSort:%d\n", end6 - begin6);
    printf("MergeSort:%d\n", end7 - begin7);

    free(a1);
    free(a2);
    free(a3);
    free(a4);
    free(a5);
    free(a6);
    free(a7);
}

三、排序稳定性

这里会联合之前的两个复杂度,总结每个排序.

稳定性是指原始数组里同样的值的前后顺序在排序后的顺序是否正确。排序前什么顺序,排序后相对顺序仍然不变,那就是稳定,反之不稳定。

插入排序(希尔和插入)

插入排序:时O(N^2), 空O(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;
    }
}

 插入排序稳定,插入算法里如果后面的值小于前面的,那就互换,如果相等那就跳过,继续往后走,所以相同值的相对顺序是不变的

希尔排序:时O(N^1.3), 空O(1)。它稳定吗?

//希尔排序
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;
        }
    }
}

希尔顺序不稳定。 因为预排序时相同的数据就不保证顺序还是和之前一样了,因为会分到不同的组。

选择排序(选择和堆排)

选择排序:时O(N^2), 空O(1)。它稳定吗?

//选择排序
// 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;
    }
}

选择排序不稳定。举一个特例,总共4个数7744。代入进函数就会发现两个4和两个7的顺序无法保证,所以选择排序其实是不稳定的。

堆排序:时O(N*logN), 空O(1), 它稳定吗?

//堆排序
void Swap(int* s1, int* s2)
{
    int tmp = *s1;
    *s1 = *s2;
    *s2 = tmp;
}

void AdjustDown(int* a, int n, int parent)
{
    int child = parent * 2 + 1;
    while (child < n)
    {
        if (child + 1 < n && a[child + 1] < a[child])
        {
            ++child;
        }
        if (a[child] < a[parent])
        {
            Swap(&a[child], &a[parent]);
            parent = child;
            child = parent * 2 + 1;
        }
        else
        {
            break;
        }
    }
}

void HeapSort(int* a, int n)//O(N * logN)
{
    //0(N)
    for (int i = (n - 1 - 1) / 2; i >= 0; i--)
    {
        AdjustDown(a, n, i);
    }
    int end = n - 1;
    //O(N * logN)
    while (end > 0)
    {
        Swap(&a[0], &a[end]);
        AdjustDown(a, end, 0);
        end--;
    }
}

 堆排序不稳定,即使放入堆的时候顺序对,但是堆排需要向下调整,这时候就无法保证了

交换排序(快排和冒泡)

冒泡排序:时O(N^2),空O(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;
    }
}

 冒泡排序稳定。这个好理解。可以看到,两个数相等的时候就不换,所以能够稳定。

快排:时O(N*logN), 空O(logN),它稳定吗?

//快排
static int GetMidIndex(int* a, int begin, int end)
{
    //int mid = (begin + end) / 2;
    int mid = begin + rand() % (end - begin);
    if (a[begin] < a[mid])
    {
        if (a[mid] < a[end])
            return mid;
        else if (a[begin] > a[end])
            return begin;
        else return end;
    }
    else
    {
        if (a[mid] > a[end])
            return mid;
        else if (a[begin] < a[end])
            return begin;
        else return end;
    }
}

//Hoare
int PartSort1(int* a, int begin, int end)
{
    int mid = GetMidIndex(a, begin, end);
    Swap(&a[begin], &a[mid]);
    int left = begin, right = end;
    int key = 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;
    return key;
}

// 挖坑法
int PartSort2(int* a, int begin, int end)
{
    int mid = GetMidIndex(a, begin, end);
    Swap(&a[begin], &a[mid]);
    int left = begin, right = end;
    int key = a[left];
    int hole = left;
    while (left < right)
    {
        // 右边找小,填到左边坑里面
        while (left < right && a[right] >= key)
        {
            --right;
        }

        a[hole] = a[right];
        hole = right;

        // 左边找大,填到右边坑里面
        while (left < right && a[left] <= key)
        {
            ++left;
        }

        a[hole] = a[left];
        hole = left;
    }

    a[hole] = key;
    return hole;
}

//双指针
int PartSort3(int* a, int begin, int end)
{
    int mid = GetMidIndex(a, begin, end);
    Swap(&a[begin], &a[mid]);
    int key = begin;
    int prev = begin, cur = begin + 1;
    while (cur <= end)
    {
        if (a[cur] < a[key] && ++prev != cur)
            Swap(&a[prev], &a[cur]);
        ++cur;
    }
    Swap(&a[prev], &a[key]);
    key = prev;
    return key;
}

void QuickSort(int* a, int begin, int end)
{
    if (begin >= end)
        return;
    if ((end - begin + 1) < 15)
    {
        // 小区间用直接插入替代,减少递归调用次数
        InsertSort(a + begin, end - begin + 1);
    }
    else
    {
        int keyi = PartSort1(a, begin, end);
        QuickSort(a, begin, keyi - 1);
        QuickSort(a, keyi + 1, end);
    }
}

void QuickSortNonR(int* a, int begin, int end)
{
    ST st;
    StackInit(&st);
    StackPush(&st, begin);
    StackPush(&st, end);
    while (!StackEmpty(&st))
    {
        int right = StackTop(&st);
        StackPop(&st);
        int left = StackTop(&st);
        StackPop(&st);
        int keyi = PartSort3(a, left, right);
        //[left, keyi-1] keyi [keyi+1, right]
        if (keyi + 1 < right)
        {
            StackPush(&st, keyi + 1);
            StackPush(&st, right);
        }

        if (left < keyi - 1)
        {
            StackPush(&st, left);
            StackPush(&st, keyi - 1);
        }
    }
    StackDestroy(&st);
}

快排不稳定。快排中它不要求相对顺序,它需要一个个比较,然后交换,停下后再和key交换,所以无法保证稳定。

归并排序

时O(N*logN), 空O(N),它稳定吗? 

归并排序稳定。归并排序是在tmp中相当于取小的尾插进tmp中,那么相对顺序也就可以保证。

//归并排序
void _MergeSort(int* a, int begin, int end, int* tmp)//时间复杂度:O(N*logN),空间复杂度:O(N)
{
    if (begin >= end)
        return;
    int mid = (begin + end) / 2;
    // [begin, mid] [mid+1, end] 递归让子区间有序
    _MergeSort(a, begin, mid, tmp);
    _MergeSort(a, mid + 1, end, tmp);
    // 归并[begin, mid] [mid+1, end]
    int begin1 = begin, end1 = mid;
    int begin2 = mid + 1, end2 = end;
    int i = begin;//区间左右端都记录下来,然后i从左端开始走
    while (begin1 <= end1 && begin2 <= end2)
    {
        if (a[begin1] <= a[begin2])
        {
            tmp[i++] = a[begin1++];
        }
        else
        {
            tmp[i++] = a[begin2++];
        }
    }
    while (begin1 <= end1)
    {
        tmp[i++] = a[begin1++];
    }
    while (begin2 <= end2)
    {
        tmp[i++] = a[begin2++];
    }
    memcpy(a + begin, tmp + begin, sizeof(int) * (end - begin + 1));
}

void MergeSort(int* a, int n)
{
    int* tmp = (int*)malloc(sizeof(int) * n);
    if (tmp == NULL)
    {
        perror("malloc fail");
        exit(-1);
    }
    _MergeSort(a, 0, n - 1, tmp);
    free(tmp);
    tmp = NULL;
}

void MergeSortNonR(int* a, int n)
{
    int* tmp = (int*)malloc(sizeof(int) * n);
    if (tmp == NULL)
    {
        perror("malloc fail");
        exit(-1);
    }
    //归并每组数据个数,从1开始,因为1个认为是有序的,可以直接归并
    int range = 1;
    while (range < n)
    {
        for (int i = 0; i < n; i += (range * 2))
        {
            //[begin1, end1] [begin2, end2]
            int begin1 = i, end1 = i + range - 1;//从1个数据开始,所以这里i = 0,整个区间就是[0, 0]
            int begin2 = i + range, end2 = i + 2 * range - 1;
            printf("[%d,%d][%d,%d]\n", begin1, end1, begin2, end2);
            int j = i;
            //主要问题是防止end1,begin2,end2越界
            //修正区间 - 拷贝数据,归并完了整体拷贝 or 归并每组拷贝
            //end2越界对拷贝方式无影响;begin2和end1越界,修正就可以整体拷贝,break就只能每组拷贝
            if (end1 >= n)
            {
                //修正
                end1 = n - 1;
                //不存在区间
                begin2 = n;
                end2 = n - 1;
                //break;
            }
            else if (begin2 >= n)
            {
                //不存在区间
                begin2 = n;
                end2 = n - 1;
                //break;
            }
            else if (end2 >= n)
            {
                end2 = n - 1;
            }
            while (begin1 <= end1 && begin2 <= end2)
            {
                if (a[begin1] <= a[begin2])
                {
                    tmp[j++] = a[begin1++];
                }
                else
                {
                    tmp[j++] = a[begin2++];
                }
            }
            while (begin1 <= end1)
            {
                tmp[j++] = a[begin1++];
            }
            while (begin2 <= end2)
            {
                tmp[j++] = a[begin2++];
            }
            //归并一部分,拷贝一部分
            //memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));
        }
        //整体拷贝
        memcpy(a, tmp, sizeof(int) * n);
        range *= 2;
    }
    free(tmp);
    tmp = NULL;
}

当然稳定的排序也可以不稳定,比如归并排序中a[begin1] < a[begin2]就不稳定了。

四、特殊的例子(快排)

对于现在我们写的快排,如果数组里全是同一个数字,那么这个程序就很难受

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

 这个问题的描述就是对于大量的重复数据,在key是这个重复数据时,存在性能下降的问题。

以往快排的结果是key在中间,左边是 <= key的, 右边是 >= key的,这是两路划分。针对重复数据这个问题,有三路划分的办法,三路划分即为把整个数据分成三部分,小于key的,等于key的,大于key的,如果没有和key相等的,其实就和二路划分一样,有就放到等于key这个区间里,这块区间一直不要动,只递归大于和小于key的区间。

建三个变量,left,right,cur。

 这是第一种情况,互换后cur指向第三个数据,left指向第二个数据

下面这个情况,演示了互换后,right原本指向的数据,也就是cur现在指向的数据并不确定是否大于小于key,所以cur还需要原地判断。现在是换过来的7大于6,right此时指向6,所以互换,就变成了下图这样。


继续往后走,当cur大于right时整个过程就结束了。这时候整个区间就出来结果了。145  66666  87三个区间。

代码实现 

void QuickSort(int* a, int begin, int end)
{
    if (begin >= end)
        return;
    //if ((end - begin + 1) < 15)
    //{
        // 小区间用直接插入替代,减少递归调用次数
        //InsertSort(a + begin, end - begin + 1);
    //}
    else
    {
        int mid = GetMidIndex(a, begin, end);
        Swap(&a[begin], &a[mid]);

        int left = begin, right = end;
        int key = a[left];
        int cur = begin + 1;
        while (cur <= right)
        {
            if (a[cur] < key)
            {
                Swap(&a[cur], &a[left]);
                cur++;
                left++;
            }
            else if (a[cur] > key)
            {
                Swap(&a[cur], &a[right]);
                --right;
            }
            else // a[cur] == key
            {
                cur++;
            }
        }
        // [begin, left-1][left, right][right+1,end]
        QuickSort(a, begin, left - 1);
        QuickSort(a, right + 1, end);
        //int keyi = PartSort1(a, begin, end);
        //QuickSort(a, begin, keyi - 1);
        //QuickSort(a, keyi + 1, end);
    }
}

把找key的代码放到快排函数里。 

这里还有一个问题,三数取中算法有点问题。

在力扣上会有很多特殊用例,如果按照之前的三数取中办法可能也会受到影响选到很小或很大的数字,所以改成随机数取key

static int GetMidIndex(int* a, int begin, int end)
{
    //int mid = (begin + end) / 2;
    int mid = begin + rand() % (end - begin);
    if (a[begin] < a[mid])
    {
        if (a[mid] < a[end])
            return mid;
        else if (a[begin] > a[end])
            return begin;
        else return end;
    }
    else
    {
        if (a[mid] > a[end])
            return mid;
        else if (a[begin] < a[end])
            return begin;
        else return end;
    }
}

结束。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值