1. 程式人生 > >快速排序及快速排序的優化(完整版)

快速排序及快速排序的優化(完整版)

快速排序的的基本思想:

      設陣列a中存放了n個數據元素,low為陣列的低端下標,high為陣列的高階下標,從陣列a中任取一個 元素(通常選取a[ow])作為標準,調整陣列a中各個元素的位置,使排在標準元素前面的元素的關鍵字均小於標準元素的關鍵字,排在標準元素後面元素的關鍵字均大於或等於標準元素的關鍵字。一次結束後,把標準元素放在適合的位置,並且把以標準元素為中心劃分了兩個子陣列,前面陣列的數都小於標準元素,後面的陣列都大於標準元素。然後分別對這兩個自陣列進行相同的遞迴快速排序。遞迴演算法的出口是high>low

快速排序相當與氣泡排序的升級版

       1.選擇基準:在待排序列中,按照某種方式挑出一個元素,作為

"基準"pivot

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

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

挖坑填數,進行快速排序:

1.i =L; j = R; 將基準數挖出形成第一個坑a[i]。

2.j--由後向前找比它小的數,找到後挖出此數填前一個坑a[i]中。

3.i++由向後找比它大的數,找到後也挖出此數填到前一個坑a[j]中。

4.再重複執行2,3二步,直到i==j,將基準數填入a[i]中


快速排序的過程圖:


快速排序的完整程式碼(遞迴):

#include<iostream>
using namespace std;
int partition(int *arr,int left,int right)
{
int temp=arr[left];
while(left<right)//直達left和right重合的時候,才找到合適的位置
{
//先從後往前找比基準小的
while(left<right  &&  arr[right]>=temp)//當right的值大於temp的值的時候才執行
        //等號一定得寫,因為可能會出現,儲存的temp元素和資料中的元素一樣的,不寫會出現死迴圈的現象
{
right--;
}
arr[left]=arr[right];//當right的值小於temp的值的時候執行
//從前往後找,找比基準大的
while(left<right  && arr[left] <=temp)//當left的值小於temp的值的時候執行
{
left++;
}
arr[right]=arr[left];//當left的值大於temp的時候執行
}
arr[left]=temp;//此時的left和right在同一個位置,此時為合適的位置,把temp的值給left
return left;//此時返回的值是temp合適的位置,即小於它的在它的左邊,大於它的在它的右邊
}
 void quick(int *arr,int left,int right)
{
if(left<right)
{
int pivot=partition(arr,left,right);
quick(arr,left,pivot-1);
quick(arr,pivot+1,right);
}
}
void quick_sort(int *arr,int len)
{
quick(arr,0,len-1);
}
int main()
{
int arr[]={9,5,7,10,45,12};
int len=sizeof(arr)/sizeof(arr[0]);
quick_sort(arr,len);
for(int k=0;k<len;++k)
{
cout<<arr[k]<<"  ";
}
cout<<endl;
}

快速排序的完整程式碼(非遞迴)

#include<stack>
#include<iostream>
using namespace std;

int partition(int *arr,int left,int right)
{
	int temp=arr[left];//基準
	while(left<right)
	{
		//先從後往前找比基準小的
		while(left<right && temp<=arr[right])
		{
			right--;
		}
		arr[left]=arr[right];
		//從前往後找比基準大的
		while(left<right  && temp>=arr[left])
		{
			left++;
		}
		arr[right]=arr[left];
	}
	arr[left]=temp;
	return left;
}
//其實就是用棧儲存每一個待排序子串的首尾元素的下標
void q_sort(int *arr,int left,int right )
{
	stack<int>  st;
	int pos=0;
    st.push (left);
	st.push (right);
	while(!st.empty())
	{
		right=st.top();
		st.pop();
		left=st.top();
		st.pop ();//5,6,8,2,9,4,1,3,45,89,65
		pos=partition(arr,left,right);
		if(pos+1<right)//先入基準右邊的,如果基準右邊只有一個元素的時候,就不用入了
		{
			st.push (pos+1);
			st.push (right);
		}
		if(pos-1>left)//再入基準左邊的,如果基準左邊只有一個元素的時候,就不用入了
		{   
			st.push (left);
			st.push (pos-1);
			
		}
	}
}

void quick_sort(int *arr,int len)
{
	q_sort(arr,0,len-1);
}
int main()
{
	int arr[]={5,6,8,2,9,4,1,3,45,89,65};
	int len=sizeof(arr)/sizeof(arr[0]);
	quick_sort(arr,len);
	for(int i=0;i<len;i++)
	{
		cout<<arr[i]<<"  ";
	}
	cout<<endl;
}

快速排序的各種優化:

優化一三數取中法,解決資料基本有序的(就是找到陣列中最小下標,最大下標,中間下標的數字,進行比較,把中間大的陣列放在最左邊)

程式碼:

//*************************************************************************
void swap(int *arr,int left,int right)
{
	int temp;
	temp=arr[left];
	arr[left]=arr[right];
	arr[right]=temp;	
}
//***************************************************************************
int partition(int *arr,int left,int right)
{
	
	//***************************
	//e.g:9,1,5,8,3,7,4,6,2
	int m=left+(right-left)/2;//找到中間的數字的下標
	if(arr[left]>arr[right])//最左大於最右的時候,交換左右
	{
		swap(arr,left,right);
	}
	if(arr[m]>arr[right])//如果中間的>right ,交換
	{
		swap(arr,m,right);
	}
	if(arr[m]>arr[left])//如果中間的>left,交換
	{
		swap(arr,m,right);
	}
	//經過交換之後low變為3
	//****************************
	int temp=arr[left];//基準
	while(left<right)//知道left和right重合的時候,才找到合適的位置
	{     //從後向前找到比小的數字
		while(left<right  &&  arr[right]>=temp)//當right的值大於temp的值的時候才執行
		{
			right--;
		}
		arr[left]=arr[right];//當right的值小於temp的值的時候執行
		 while(left<right  && arr[left] <= temp)//從前往後找到比基準大的數字
		{
			left++;
		}
		arr[right]=arr[left];//當left的值大於temp的時候執行
	}
	arr[left]=temp;//此時的left和right在同一個位置,此時為合適的位置,把temp的值給left
	return left;//此時返回的值是temp合適的位置,即小於它的在它的左邊,大於它的在它的右邊
}

優化二:

隨機選取基準

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

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

/*隨機選擇樞軸的位置,區間在low和high之間*/  
int SelectPivotRandom(int arr[],int low,int high)  
{  
  //產生樞軸的位置  
   srand((unsigned)time(NULL));  
  int pivotPos = rand()%(high - low) + low;   
  //把樞軸位置的元素和low位置元素互換,此時可以和普通的快排一樣呼叫劃分函式  
  swap(arr[pivotPos],arr[low]);  
   return arr[low];  
}  

優化三:優化小陣列的交換,就是為了解決大才小用問題

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

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

#define  max_len  10
void quick(int *arr,int left,int right)
{
int length=right-left;
if(length>max_len )
{
int pivot=partition(arr,left,right);
quick(arr,left,pivot-1);
quick(arr,pivot+1,right);
}
else
{
//用插入排序
}
}

優化四:

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

舉例:

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

三數取中選取樞軸:下標為4的數6

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

             樞軸key6

本次劃分後,未對與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相等的元素移到樞軸周圍

舉例:

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

三數取中選取樞軸:下標為4的數6

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

             樞軸key6

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

結果為:6 4 1 6(樞軸) 7 8 7 6 6 6

此時,與6相等的元素全放入在兩端了

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

結果為:1 4 66(樞軸 6 6 6 7 8 7

此時,與6相等的元素全移到樞軸周圍了

之後,在1 4  7 8 7兩個子序列進行快排

程式碼:

void QSort(int arr[],int low,int high)  
{  
    int first = low;  
   int last = high;  
  
   int left = low;  
    int right = high;  
 
   int leftLen = 0;  //用來統計左邊與key相等的元素的個數
   int rightLen = 0;  //統計右邊與key相等的元素的個數
  
    if (high - low + 1 < 10)  
    {  
        InsertSort(arr,low,high);  //資料量少,就用插入排序
        return;  
    }  
      
    //一次分割  
   int key = SelectPivotMedianOfThree(arr,low,high);//使用三數取中法選擇樞軸  
         
    while(low < high)  
    {  
       while(high > low && arr[high] >= key)  
        {  
         if (arr[high] == key)//處理相等元素  
           {  
               swap(arr[right],arr[high]); //把右邊與key元素相等的聚集的右端
                right--;  
                rightLen++;  
            }  
            high--;  
        }  
        arr[low] = arr[high];  
        while(high > low && arr[low] <= key)  
       {  
            if (arr[low] == key)  //把左邊與key元素相等的聚集陣列的左端
           {  
                swap(arr[left],arr[low]);  
                left++;  
                leftLen++;  
            }  
            low++;  
        }  
       arr[high] = arr[low];  
    }  
    arr[low] = key;  
 
    //一次快排結束  
    //把與樞軸key相同的元素移到樞軸最終位置周圍  
    int i = low - 1;  //軸的左邊
    int j = first;  
    while(j < left && arr[i] != key)  
   {  
        swap(arr[i],arr[j]);  //此時,把陣列左端與key相等的資料換到key的左邊
        i--;  
        j++;  
    }  
    i = low + 1;  //軸的右邊
    j = last;  
    while(j > right && arr[i] != key)  
    {  
       swap(arr[i],arr[j]);  //此時,把陣列右端與key相等的資料換到,key右邊
       i++;  
       j--;  
   }  
    partition(arr,first,low - 1 - leftLen);  
    partition(arr,low + 1 + rightLen,last);  
}