三種快速排序演算法以及快速排序的優化

叫我霍啊啊啊發表於2015-03-15

一.  快速排序的基本思想

快速排序使用分治的思想,通過一趟排序將待排序列分割成兩部分,其中一部分記錄的關鍵字均比另一部分記錄的關鍵字小。之後分別對這兩部分記錄繼續進行排序,以達到整個序列有序的目的。

二.  快速排序的三個步驟

1) 選擇基準:在待排序列中,按照某種方式挑出一個元素,作為 “基準”(pivot);

2) 分割操作:以該基準在序列中的實際位置,把序列分成兩個子序列。此時,在基準左邊的元素都比該基準小,在基準右邊的元素都比基準大;

3) 遞迴地對兩個序列進行快速排序,直到序列為空或者只有一個元素;

三.  選擇基準元的方式

對於分治演算法,當每次劃分時,演算法若都能分成兩個等長的子序列時,那麼分治演算法效率會達到最大。也就是說,基準的選擇是很重要的。選擇基準的方式決定了兩個分割後兩個子序列的長度,進而對整個演算法的效率產生決定性影響。

最理想的方法是,選擇的基準恰好能把待排序序列分成兩個等長的子序列。

方法一:固定基準元(基本的快速排序)

思想:取序列的第一個或最後一個元素作為基準元。

/// <summary>
        /// 1.0 固定基準元(基本的快速排序)
        /// </summary>
        public static void QsortCommon(int[] arr, int low, int high)
        {
            if (low >= high) return;                        //遞迴出口
            int partition = Partition(arr, low, high);      //將 >= x 的元素交換到右邊區域,將 <= x 的元素交換到左邊區域
            QsortCommon(arr, low, partition - 1);
            QsortCommon(arr, partition + 1, high);
        }

/// <summary>
        /// 固定基準元,預設陣列第一個數為基準元,左右分組,返回基準元的下標
        /// </summary>
        public static int Partition(int[] arr, int low, int high)
        {
            int first = low;
            int last = high;
            int key = arr[low];                             //取第一個元素作為基準元
            while (first < last)
            {
                while (first < last && arr[last] >= key)
                    last--;
                arr[first] = arr[last];
                while (first < last && arr[first] <= key)
                    first++;
                arr[last] = arr[first];
            }
            arr[first] = key;                               //基準元居中
            return first;
        }

注意:基本的快速排序選取第一個或最後一個元素作為基準。但是,這是一直很不好的處理方法。

測試資料:

測試資料分析:如果輸入序列是隨機的,處理時間可以接受的。如果陣列已經有序時,此時的分割就是一個非常不好的分割。因為每次劃分只能使待排序序列減一,此時為最壞情況,快速排序淪為氣泡排序,時間複雜度為Θ(n^2)。而且,輸入的資料是有序或部分有序的情況是相當常見的。因此,使用第一個元素作為基準元是非常糟糕的,為了避免這個情況,就引入了下面兩個獲取基準的方法。

方法二:隨機基準元

思想:取待排序列中任意一個元素作為基準元。

引入的原因:在待排序列是部分有序時,固定選取基準元使快排效率底下,要緩解這種情況,就引入了隨機選取基準元。

/// <summary>
        /// 2.0 隨機基準元
        /// </summary>
        public static void QsortRandom(int[] arr, int low, int high)
        {
            if (low >= high) return;                        //遞迴出口
            PartitionRandom(arr, low, high);                //隨機基準元
            int partition = Partition(arr, low, high);      //將 >= x 的元素交換到右邊區域,將 <= x 的元素交換到左邊區域
            QsortRandom(arr, low, partition - 1);
            QsortRandom(arr, partition + 1, high);
        }

/// <summary>
        /// 隨機基準元,將確定好的基準元與第一個數交換,無返回值
        /// </summary>        
        public static void PartitionRandom(int[] arr, int low, int high)
        {
            Random rd = new Random();
            int randomIndex = rd.Next() % (high - low) + low;//取陣列中隨機下標
            Swap(arr, randomIndex, low);                     //與第一個數交換
        }

測試資料:

測試資料分析::這是一種相對安全的策略。由於基準元的位置是隨機的,那麼產生的分割也不會總是會出現劣質的分割。在整個陣列數字全相等時,仍然是最壞情況,時間複雜度是O(n^2)。實際上,隨機化快速排序得到理論最壞情況的可能性僅為1/(2^n)。所以隨機化快速排序可以對於絕大多數輸入資料達到O(nlogn)的期望時間複雜度。一位前輩做出了一個精闢的總結:“隨機化快速排序可以滿足一個人一輩子的人品需求。”

方法三:三數取中

引入的原因:雖然隨機選取基準時,減少出現不好分割的機率,但是還是最壞情況下還是O(n^2),要緩解這種情況,就引入了三數取中選取基準。

分析:最佳的劃分是將待排序的序列分成等長的子序列,最佳的狀態我們可以使用序列的中間的值,也就是第N/2個數。可是,這很難算出來,並且會明顯減慢快速排序的速度。這樣的中值的估計可以通過隨機選取三個元素並用它們的中值作為基準元而得到。事實上,隨機性並沒有多大的幫助,因此一般的做法是使用左端、右端和中心位置上的三個元素的中值作為基準元。顯然使用三數中值分割法消除了預排序輸入的不好情形,並且減少快排大約14%的比較次數。

舉例:待排序序列為:8 1 4 9 6 3 5 2 7 0

左邊為:8,右邊為0,中間為6

我們這裡取三個數排序後,中間那個數作為樞軸,則樞軸為6

注意:在選取中軸值時,可以從由左中右三個中選取擴大到五個元素中或者更多元素中選取,一般的,會有(2t+1)平均分割槽法(median-of-(2t+1),三平均分割槽法英文為median-of-three。

具體思想:對待排序序列中low、mid、high三個位置上資料進行排序,取他們中間的那個資料作為基準,並用0下標元素儲存基準。

即:採用三數取中,並用0下標元素儲存基準。

/// <summary>
        /// 3.0 三數取中
        /// </summary>
        public static void QsortMedianOfThree(int[] arr, int low, int high)
        {
            if (low >= high) return;                        //遞迴出口
            PartitionMedianOfThree(arr, low, high);         //三數取中
            int partition = Partition(arr, low, high);      //將 >= x 的元素交換到右邊區域,將 <= x 的元素交換到左邊區域
            QsortMedianOfThree(arr, low, partition - 1);
            QsortMedianOfThree(arr, partition + 1, high);
        }

/// <summary>
        /// 三數取中確定基準元,將確定好的基準元與第一個數交換,無返回值
        /// </summary>        
        public static void PartitionMedianOfThree(int[] arr, int low, int high)
        {
            int mid = low + (high + -low) / 2;
            if (arr[mid] > arr[high])
            {
                Swap(arr, mid, high);
            }
            if (arr[low] > arr[high])
            {
                Swap(arr, low, high);
            }
            if (arr[mid] > arr[low])
            {
                Swap(arr, mid, low);
            }                                                //將中間大小的數與第一個數交換
        }

測試資料:

測試資料分析:使用三數取中優勢還是很明顯的,但是還是處理不了重複陣列。

四.  兩種優化的方法

優化一:當待排序序列的長度分割到一定大小後,使用插入排序

原因:對於很小和部分有序的陣列,快排不如插排好。當待排序序列的長度分割到一定大小後,繼續分割的效率比插入排序要差,此時可以使用插排而不是快排。

截止範圍:待排序序列長度N = 10,雖然在5~20之間任一截止範圍都有可能產生類似的結果,這種做法也避免了一些有害的退化情形。

—-摘自《資料結構與演算法分析》Mark Allen Weiness 著

/// <summary>
        /// 4.0 三數取中+插排
        /// </summary>        
        public static void QsortThreeInsert(int[] arr, int low, int high)
        {
            if (high - low + 1 < 10)
            {
                InsertSort(arr, low, high);
                return;
            }                                               //插排,遞迴出口
            PartitionMedianOfThree(arr, low, high);         //三數取中
            int partition = Partition(arr, low, high);      //將 >= x 的元素交換到右邊區域,將 <= x 的元素交換到左邊區域
            QsortMedianOfThree(arr, low, partition - 1);
            QsortMedianOfThree(arr, partition + 1, high);
        }

測試資料:

測試資料分析:針對隨機陣列,使用三數取中選擇基準+插排,效率還是可以提高一點,真是針對已排序的陣列,是沒有任何用處的。因為待排序序列是已經有序的,那麼每次劃分只能使待排序序列減一。此時,插排是發揮不了作用的。所以這裡看不到時間的減少。另外,三數取中選擇基準+插排還是不能處理重複陣列。

優化二:在一次分割結束後,可以把與Key相等的元素聚在一起,繼續下次分割時,不用再對與key相等元素分割

舉例:

待排序序列 1 4 6 7 6 6 7 6 8 6

三數取中選取基準:下標為4的數6

轉換後,待分割序列:6 4 6 7 1 6 7 6 8 6

             基準key:6

本次劃分後,未對與key元素相等處理的結果:1 4 6 6 7 6 7 6 8 6

下次的兩個子序列為:1 4 6 和 7 6 7 6 8 6

本次劃分後,對與key元素相等處理的結果:1 4 6 6 6 6 6 7 8 7

下次的兩個子序列為:1 4 和 7 8 7

經過對比,我們可以看出,在一次劃分後,把與key相等的元素聚在一起,能減少迭代次數,效率會提高不少

具體過程:在處理過程中,會有兩個步驟

第一步,在劃分過程中,把與key相等元素放入陣列的兩端

第二步,劃分結束後,把與key相等的元素移到樞軸周圍

/// <summary>
        /// 5.0 三數取中+插排+聚集相同元素
        /// </summary>        
        public static void QsortThreeInsertGather(int[] arr, int low, int high)
        {
            if (high - low + 1 < 10)
            {
                InsertSort(arr, low, high);
                return;
            }                                               //插排,遞迴出口
            PartitionMedianOfThree(arr, low, high);         //三數取中

            //進行左右分組(處理相等元素)
            int first = low;
            int last = high;
            int left = low;
            int right = high;
            int leftLength = 0;
            int rightLength = 0;
            int key = arr[first];
            while (first < last)
            {
                while (first < last && arr[last] >= key)
                {
                    if (arr[last] == key)                   //處理相等元素,將相等的元素放置陣列兩端
                    {
                        Swap(arr, last, right);
                        right--;
                        rightLength++;
                    }
                    last--;
                }
                arr[first] = arr[last];
                while (first < last && arr[first] <= key)
                {
                    if (arr[first] == key)
                    {
                        Swap(arr, first, left);
                        left++;
                        leftLength++;
                    }
                    first++;
                }
                arr[last] = arr[first];
            }
            arr[first] = key;

            //一次快排結束
            //把與基準元key相同的元素移到最終位置周圍
            int i = first - 1;
            int j = low;
            while (j < left && arr[i] != key)
            {
                Swap(arr, i, j);
                i--;
                j++;
            }
            i = last + 1;
            j = high;
            while (j > right && arr[i] != key)
            {
                Swap(arr, i, j);
                i++;
                j--;
            }
            QsortThreeInsertGather(arr, low, first - leftLength - 1);
            QsortThreeInsertGather(arr, first + rightLength + 1, high);
        }

測試資料:

測試資料分析:三數取中+插排+聚集相等元素的組合,效果竟然好的出奇。

原因:在陣列中,如果有相等的元素,那麼就可以減少不少冗餘的劃分。這點在重複陣列中體現特別明顯啊。

其實這裡,插排的作用還是不怎麼大的。

以下是全部的測試程式原始碼:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Diagnostics;
using System.Threading;

namespace Sort
{
    class Program
    {
        static void Main(string[] args)
        {
            //開啟10M的堆疊空間的執行緒
            ThreadStart ts = new ThreadStart(Sort.DoQsort);
            Thread thread = new Thread(ts, 10000000);
            thread.Start();
        }
    }

    class Sort
    {
        public static void DoQsort()
        {
            int[] arr = new int[100000];                        //10W個空間大小的陣列

            //Random rd = new Random();
            //for (int i = 0; i < arr.Length; i++)          //隨機陣列
            //{
            //    arr[i] = rd.Next();
            //}

            //for (int i = 0; i < arr.Length; i++)          //升序陣列
            //{
            //    arr[i] = i;
            //}

            //for (int i = 0; i < arr.Length; i++)          //降序陣列
            //{
            //    arr[i] = arr.Length - 1 - i;
            //}

            for (int i = 0; i < arr.Length; i++)          //重複陣列
            {
                arr[i] = 5768461;
            }

            Stopwatch watch = new Stopwatch();
            watch.Start();                                  //開始計時

            //QsortCommon(arr, 0, arr.Length - 1);          //固定基準元
            //QsortRandom(arr, 0, arr.Length - 1);          //隨機基準元
            //QsortMedianOfThree(arr, 0, arr.Length - 1);   //三數取中
            //QsortThreeInsert(arr, 0, arr.Length - 1);     //三數取中+插排
            QsortThreeInsertGather(arr, 0, arr.Length - 1); //三數取中+插排+聚集相同元素

            watch.Stop();                                   //計時結束

            Console.WriteLine(watch.ElapsedMilliseconds.ToString());
        }

        /// <summary>
        /// 1.0 固定基準元(基本的快速排序)
        /// </summary>
        public static void QsortCommon(int[] arr, int low, int high)
        {
            if (low >= high) return;                        //遞迴出口
            int partition = Partition(arr, low, high);      //將 >= x 的元素交換到右邊區域,將 <= x 的元素交換到左邊區域
            QsortCommon(arr, low, partition - 1);
            QsortCommon(arr, partition + 1, high);
        }

        /// <summary>
        /// 2.0 隨機基準元
        /// </summary>
        public static void QsortRandom(int[] arr, int low, int high)
        {
            if (low >= high) return;                        //遞迴出口
            PartitionRandom(arr, low, high);                //隨機基準元
            int partition = Partition(arr, low, high);      //將 >= x 的元素交換到右邊區域,將 <= x 的元素交換到左邊區域
            QsortRandom(arr, low, partition - 1);
            QsortRandom(arr, partition + 1, high);
        }

        /// <summary>
        /// 3.0 三數取中
        /// </summary>
        public static void QsortMedianOfThree(int[] arr, int low, int high)
        {
            if (low >= high) return;                        //遞迴出口
            PartitionMedianOfThree(arr, low, high);         //三數取中
            int partition = Partition(arr, low, high);      //將 >= x 的元素交換到右邊區域,將 <= x 的元素交換到左邊區域
            QsortMedianOfThree(arr, low, partition - 1);
            QsortMedianOfThree(arr, partition + 1, high);
        }

        /// <summary>
        /// 4.0 三數取中+插排
        /// </summary>        
        public static void QsortThreeInsert(int[] arr, int low, int high)
        {
            if (high - low + 1 < 10)
            {
                InsertSort(arr, low, high);
                return;
            }                                               //插排,遞迴出口
            PartitionMedianOfThree(arr, low, high);         //三數取中
            int partition = Partition(arr, low, high);      //將 >= x 的元素交換到右邊區域,將 <= x 的元素交換到左邊區域
            QsortMedianOfThree(arr, low, partition - 1);
            QsortMedianOfThree(arr, partition + 1, high);
        }

        /// <summary>
        /// 5.0 三數取中+插排+聚集相同元素
        /// </summary>        
        public static void QsortThreeInsertGather(int[] arr, int low, int high)
        {
            if (high - low + 1 < 10)
            {
                InsertSort(arr, low, high);
                return;
            }                                               //插排,遞迴出口
            PartitionMedianOfThree(arr, low, high);         //三數取中

            //進行左右分組(處理相等元素)
            int first = low;
            int last = high;
            int left = low;
            int right = high;
            int leftLength = 0;
            int rightLength = 0;
            int key = arr[first];
            while (first < last)
            {
                while (first < last && arr[last] >= key)
                {
                    if (arr[last] == key)                   //處理相等元素
                    {
                        Swap(arr, last, right);
                        right--;
                        rightLength++;
                    }
                    last--;
                }
                arr[first] = arr[last];
                while (first < last && arr[first] <= key)
                {
                    if (arr[first] == key)
                    {
                        Swap(arr, first, left);
                        left++;
                        leftLength++;
                    }
                    first++;
                }
                arr[last] = arr[first];
            }
            arr[first] = key;

            //一次快排結束
            //把與基準元key相同的元素移到最終位置周圍
            int i = first - 1;
            int j = low;
            while (j < left && arr[i] != key)
            {
                Swap(arr, i, j);
                i--;
                j++;
            }
            i = last + 1;
            j = high;
            while (j > right && arr[i] != key)
            {
                Swap(arr, i, j);
                i++;
                j--;
            }
            QsortThreeInsertGather(arr, low, first - leftLength - 1);
            QsortThreeInsertGather(arr, first + rightLength + 1, high);
        }

        /// <summary>
        /// 固定基準元,預設陣列第一個數為基準元,左右分組,返回基準元的下標
        /// </summary>
        public static int Partition(int[] arr, int low, int high)
        {
            int first = low;
            int last = high;
            int key = arr[low];                             //取第一個元素作為基準元
            while (first < last)
            {
                while (first < last && arr[last] >= key)
                    last--;
                arr[first] = arr[last];
                while (first < last && arr[first] <= key)
                    first++;
                arr[last] = arr[first];
            }
            arr[first] = key;                               //基準元居中
            return first;
        }

        /// <summary>
        /// 隨機基準元,將確定好的基準元與第一個數交換,無返回值
        /// </summary>        
        public static void PartitionRandom(int[] arr, int low, int high)
        {
            Random rd = new Random();
            int randomIndex = rd.Next() % (high - low) + low;//取陣列中隨機下標
            Swap(arr, randomIndex, low);                     //與第一個數交換
        }

        /// <summary>
        /// 三數取中確定基準元,將確定好的基準元與第一個數交換,無返回值
        /// </summary>        
        public static void PartitionMedianOfThree(int[] arr, int low, int high)
        {
            int mid = low + (high + -low) / 2;
            if (arr[mid] > arr[high])
            {
                Swap(arr, mid, high);
            }
            if (arr[low] > arr[high])
            {
                Swap(arr, low, high);
            }
            if (arr[mid] > arr[low])
            {
                Swap(arr, mid, low);
            }                                                //將中間大小的數與第一個數交換
        }

        /// <summary>
        /// 插入排序
        /// </summary>
        public static void InsertSort(int[] arr, int low, int high)
        {
            for (int i = low + 1; i <= high; i++)
            {
                if (arr[i] < arr[i - 1])
                {
                    for (int j = low; j < i; j++)
                    {
                        if (arr[j] > arr[i])
                        {
                            Swap(arr, i, j);
                        }
                    }
                }
            }
        }

        /// <summary>
        /// 陣列交換
        /// </summary>
        public static void Swap(int[] arr, int index1, int index2)
        {
            int temp = arr[index1];
            arr[index1] = arr[index2];
            arr[index2] = temp;
        }
    }
}

相關文章