天天看点

排序(下)---快排、归并排序(下)—快排、归并

排序(下)—快排、归并

快速排序

采用分治的思想:每次选区一个数作为基准,让整个数据中凡是大于此数的放在此数的右边,小于此数的放在此数的左边,然后进行对此数的两边进行选基准,如上方式的分法,递归下去,直到分出来的数据只有一个元素,或者无元素,停止分支,因为每次分支都会进行调整,所以当分到每个分支中只剩一个数的时候,整组数据就有序了。
  1. 首先得定义一个选选基准,并进行一次的分支的方法。
  2. 每分一次将最后的基准数的下标返回,以此为界限,把两边分别看作两个数组,并分别进行分治
  3. 直到所分后的数组元素个数为1或者0,就停止分治,此时,整个数组就已经有序。
流程图:
排序(下)---快排、归并排序(下)—快排、归并
  • 快排的时间复杂度平均和最好情况都为O(n*logn),但是最坏情况是O(n^2),主要看key值的选取规则

选基准的进行单次调整的方法由很多种:

左右下标寻找法:

  • 定一个左下标,定一个右下标,分别指向当前数组的最左和最右。
  • 将key的值定位在边上(这里定位在最右边)
  • 左下标先向右走,如果发现所指的值大于key,说明需要被交换到右边去,这时在让右下标向左走,在和左下标重合之前如果发现所指的值小于key的值,那就将两个下标所指的值进行交换,然后继续向对方的方向走,直到两个下标重合。
  • 两个下标重合的时候将key值(数组的最右值与重合的地方进行交换)一次调整完成。
  • 视图:
    排序(下)---快排、归并排序(下)—快排、归并
  • 代码:
    int PartSort(int left, int right, int arr[])
        {
            int key = arr[right];
            int start = left;
            int end = right;  //反例:1, 2, 3, 4, 5, 6, 7, 8, 9,如果是end= right-1,end会一直指向8,而start会向后移动到8停止,然后进行交换8和9会发现是错的。
            while (start < end)
            {
                while (start < end && arr[start] <= key) //反例:5, 5, 5, 5, 5, 5, 5,自己可以推一下
                {
                    start++;
                }
                while (start < end && arr[end] >= key)
                {
                    end--;
                }
                //此时start有可能和end是相等的
                if (start < end)//此时说明start指向了一个比key大的数,end指向了一个比key小的数,应该将这两个数进行交换
                {
                    int tmp = arr[start];
                    arr[start] = arr[end];
                    arr[end] = tmp;
                }
            }
            //说明整个数组已经交换完毕,就差将最后一个与当前的数进行交换
            int tmp = arr[right];
            arr[right] = arr[end];
            arr[end] = tmp;
            return end;
        }
               
  • 在编写的时候遇到好多坑需要注意

挖坑法

挖坑发其实还是由左右向中间遍历的方法,不过与左右指针寻找法稍有不同。
  • 首先key值我们先选最右边的值
  • 首先是start向右走,碰到比key值大的数的时候,直接将此值赋给end所指的值(第一次的时候end指向的是数组最右边的值,因为变量key的值就是此值,所以不用怕赋值过后将此值丢了)
  • 赋值完成之后,start停止,让end向左移动,当碰到比key值小的值的时候,将此值赋给start所指的值(此时原来的值已经在刚在赋给了上一个end下标,所以原来的值是丢不掉的)
  • 一次循环,直到start与end重合,此时start和end指向的位置的左边的数全都小于等于key值,end指向的数全都大于此值,并且此值在上一次赋值后已经有了一个备份,所以直接将key的值赋给此处的值,一次调整就完成了。
  • 图示:
排序(下)---快排、归并排序(下)—快排、归并
  • 代码:
    //挖坑法
    int PartSort(int left, int right, int arr[])
    {
        int start = left;
        int end = right;
        int key = arr[right];
        while (start < end)
        {
            while (start < end && arr[start] <= key)
            {
                start++;
            }
            //此时start所指的数大于key值,需要将其赋给end所指的空间
            arr[end] = arr[start];
            while (start < end && arr[end] >= key)
            {
                end--;
            }
            //此时end所指的值小于key值,需要将其赋给start所指的空间
            arr[start] = arr[end];
        }
        arr[start] = key;
        return start;
    }
               

前后下标法:

一个下标用来遍历数组,一个下标用来标记当前已经走过的比较大的数,刚开是都指向数组中第一个数。
  • 两个变量:cur用来遍历数组,firstBig用来指向第一个比较大的数,开始都为数组最左边的数,key的数还取数组最有变的数
  • 首先让cur所指的内容和key值进行比较,如果大于key值,就继续向前移动。
  • 如果cur所指的值小于key值,那就让firstBig所指的值与cur所指的值进行交换,并且让firstBig向后走动一步。
  • 直到cur移动到数组的最后一个元素的时候停止移动,让cur指向的值和firstBig所指的值进行交换,一次调整完成。
  • 图示:
排序(下)---快排、归并排序(下)—快排、归并
  • 代码:
    //前后下标法
    int PartSort(int left, int right, int arr[])
    {
        int cur = left;
        int firstBig = left;
        int key = arr[right];
        while (cur < right)
        {
            if (arr[cur] <= key)
            {
                int tmp = arr[firstBig];
                arr[firstBig] = arr[cur];
                arr[cur] = tmp;
                firstBig++;
            }
            cur++;
        }
        int tmp = arr[firstBig];
        arr[firstBig] = arr[cur];
        arr[cur] = tmp;
        return firstBig;
    }
               

快排主函数

以上我们只是介绍了一次的调整方法,而再我们的快速排序中,要频繁的使用这些调整的方法,因为快排的思想是分治,也就是每分治一次,都要使用一次调整:
  1. 首先将整体的数组使用我们刚才的方法进行一次调整,这样就会分治一次,我们每次调整都会返回最后放在中间的key,也就是说我们只是把比key小的放在了key的左边,比key大的放在了key的右边
  2. 然后我们将key的左边(不包含key)左右看成一个数组,进行调整,将key的右边(不包含key)看成一个数组进行调整,然后照样会返回一个key,然后再继续这样递归下去。
  3. 当最后分出来的数组中元素只有一个或者0个的时候,我们已经将整个数组的工作完成了,到此结束。

代码:

//递归
void QuickSortRecursion(int arr[], int left, int right)
{
    if (left >= right)//取间内就剩最多一个元素
    {
        return;
    }
    int div = PartSort(left, right, arr);
    QuickSortRecursion(arr, left, div-1);
    QuickSortRecursion(arr, div + 1, right);
}
void QuickSort(int arr[], int size)
{
    QuickSortRecursion(arr, 0, size-1);
}
           
这里我使用的是递归的方式,当然也可以使用非递归的方式,但是得用到栈。
//快速排序循环版
void QuickSortLoop(int arr[], int size)
{
    int stack[100];
    int top = 0;
    stack[top++] = 0;
    stack[top++] = size - 1;
    while (top > 0)
    {
        int right = stack[--top];
        int left = stack[--top];
        int div = PartSort(left, right, arr);
        if (right-left > 1)
        {
            stack[top++] = left;
            stack[top++] = div - 1;
            stack[top++] = div + 1;
            stack[top++] = right;
        }
    }
}
           
注意每次都是先入左边数组,再入右边数组,但是每次都是先调整右边数组,再调整左边数组的。

关于key的选取

以上的介绍中我们默认选的key为最右边的数,但是这样是有缺点的:

假如我们的数组(假设右9个元素)本来就有序,不管是升序还是降序,假设是升序可以想象一下,当我们选最右边的数作为key的时候:

- 第一次切分的后,左边还有8个元素,右边没有元素,第二次切分之后左边还有7个元素,右边还没有元素……

- 这样下去就得切分8次,每次切分都要遍历一遍,时间复杂度已经达到了O(n^2)

- 如果是降序的话还是一样的。

再次我们右两种方法进行选择key的值:

  1. 随机选:生成随机数选取数组的一个下标
  2. 三选一:选出最左边、中间和最右边,三者进行比较,选出值处于三者中间的,然后将其与最右的进行交换,然后就可以使用我们的方法,当成选作最右的方法使用了。
代码:略(哈哈,其实不是很难)

归并排序

    将对一个无序数组的排序转化为对两个有序数组的排序

    使用一以上的思想将一个数组分为两个数组,对于这两个数组也分别使用此方法,这样切分下去就最后结果就是,一个数组就只有一个元素或者没有0个元素了

    然后从切分到极致的情况开始排序,每次排序就是将两个数组合并为一个数组,最后将原来的数组又重新组合回来

  1. 首先将一个数组切分成两个数组,然后再随两个数组进行切分,直到最后切分出来的数组元素个数为1或者0
  2. 因为一个数可以认为式天生有序,所以可以从最后切分后的数组开始进行合并
  3. 合并的算法就是将两个有序的数组合并为一个有序数组
  4. 由最后一步开始合并,直到最后合并后只剩一个数组,说明排序已经完成。

流程图:

排序(下)---快排、归并排序(下)—快排、归并

代码:

//首先是合并两个有序数组
void Merge(int arr[], int mid, int left, int right, int extra[])
{
    int indexLeft = left, indexRight = mid+1;//indexLeft用来遍历左半个数组,indexRight用来遍历右半个数组,把中间的值归为左半边
    int indexExtra = left;//用来遍历extra合并后的数组,为了保持下标相等,所以也从left开始
    while (indexLeft <= mid && indexRight <= right)
    {
        if (arr[indexLeft] <= arr[indexRight])
        {
            extra[indexExtra++] = arr[indexLeft++];
        }
        else
        {
            extra[indexExtra++] = arr[indexRight++];
        }
    }
    //最后出来的时候可能是左边走完了,右边没有走完,
    //也可能是右边走完了,左边没走完
    while (indexLeft <= mid)
    {
        extra[indexExtra++] = arr[indexLeft++];
    }
    while (indexRight <= right)
    {
        extra[indexExtra++] = arr[indexRight++];
    }
    //最后将extra放回到原来的数组中,并且是原来的区间
    int i = 0;
    for (i=left; i<=right; i++)
    {
        arr[i] = extra[i];
    }
}

//然后是归并排序的递归版
void MergeSortRecursion(int arr[], int left, int right,int extra[])
{
    if (left >= right)
    {
        return;
    }
    int mid = left + (right - left) / 2;
    MergeSortRecursion(arr, left, mid, extra);
    MergeSortRecursion(arr, mid+1, right, extra);
    Merge(arr, mid, left, right, extra);
}
//归并排序的入口
void MergeSort(int arr[], int size)
{
    int *p = (int*)malloc(sizeof(int) * size);
    MergeSortLoop(arr, 0, size-1, p);
    free(p);
}
//当然也有循环版(非递归)
void MergeSortLoop(int arr[], int left, int right, int extra[])
{
    int i = 0;
    int gap = 1;
    for (gap = 1; gap <= right; gap *= 2)
    {
        for (i=left; i<=right; i+=(2*gap))
        {
            int indexLeft = i;
            int mid = i + gap;
            int indexRight = i+2*gap-1;
            if (mid > right)
            {
                break;
            }
            if (indexRight > right)
            {
                indexRight = right;
            }
            Merge(arr, i + gap - 1, i, i + gap, extra);
        }
    }
}
           
  • 归并排序是非常稳定的,它的任何情况下的时间复复杂度都是O(n*logn)

继续阅读