算法要考試了,復習到了排序的知識,所以對復習的內容進行以下總結,參考書目《算法導論》。排序問題是算法與數據結構中常講到的問題了,有次面試問到了具體的快速排序的原理以及實現,頓時當時就愣了,平時各種語言提供的類庫中都有實現好的快速排序算法,針對這個算法原理也就沒有在意;不過這次算法課程結束了感覺算法內容還是挺重要的,不過參考算法導論的話真的學了好多數學知識,也被許多數學知識給嚇住了,算法導論一書重點不在於算法的實踐,經典的算法只是給出了偽代碼,然后大量的篇幅進行正確性證明,復雜度分析,胡言亂語一番,接下來具體算法的介紹啦。
1、插入排序
插入排序的原理是訪問過的部分是有序的,不過只記住原理有時候是寫不出代碼的,或者有了代碼看不出它的這個原理,說這句是因為我記得以前考軟考的時候一個變換了一下的插入排序的偽代碼放在我的面前,我竟然不認識它...話不多說,直接上代碼
bool insertSort(unsigned int* array,int length)
{
int i,j;
unsigned int k;
for (i=1;i<length;i++)
{
j = i - 1;
//把第i個元素先拿出來,比它大的依次向后挪動,然后把它插到j+1的位置上,剛好j+1的元素挪動到j上了
key = array[i];
//比key大的都向后挪動
while (j>=0&&key < array[j])
{
array[j+1] = array[j];
j--;
}
array[j+1] = key;
}
return true;
}
插入排序實際應用中也不經常用到,不過有些還是會用到的,比如數據量比較小的情況下,插入排序是最快的,所以有的算法的在小的規模會利用這種暴力法,我對插入排序比較敬畏就是因為那次軟考中看不出插入排序,當時真是太年輕了,時間復雜度O(n2),插入排序一般情況下也是穩定的排序算法。
2、歸並排序
好像說外排序的原理和歸並排序差不多,不過我還是沒有實現過,這里的排序算法都是內存排序算法了,歸並排序需要開辟一個額外的內容空間,但是時間復雜度為Θ(nlgn),雖說這是一個Θ,但是后面會看到,實際應用中它不如快速排序快速,后面會分析下原因的,歸並排序一般情況屬於穩定的排序算法,這個算法思想比較簡單,而且上篇日志分治算法中介紹了,主要工作在於merge中,直接貼代碼了
bool PreMergeSort(unsigned int* array,int begin,int end)
{
unsigned int* arrayAssit = new unsigned int[end - begin + 1];
mergeSort(array,arrayAssit,begin,end);
delete [] arrayAssit;
return true;
}
bool mergeSort(unsigned int* array,unsigned int* arrayAssit,int begin,int end)
{
if (end == begin)
{
return true;
}
int mid = (begin+end)/2;
mergeSort(array,arrayAssit,begin,mid);
mergeSort(array,arrayAssit,mid+1,end);
merge(array,arrayAssit,begin,mid,end);
return true;
}
bool merge(unsigned int* array,unsigned int* arrayAssit,int begin,int mid,int end)
{
int i,j,k;
i = begin;
j = mid + 1;
k = begin;
while(i <= mid&&j<= end)
{
if (array[i] <= array[j])
{
arrayAssit[k++] = array[i++];
}
else
{
arrayAssit[k++] = array[j++];
}
}
while (i <= mid)
{
arrayAssit[k++] = array[i++];
}
while (j <= end)
{
arrayAssit[k++] = array[j++];
}
memcpy(array+begin,arrayAssit+begin,(end-begin+1)*sizeof(array[0]));
return true;
}
分治算法設計的思想很重要的,接下來的快速排序算法也是基於分治的思想...
3、快速排序
快速排序是面試中經常出現的問題呀,所以了解其原理,熟練寫出偽代碼還是必備技能呀,會寫一個冒泡和插入是不行的,記憶快速排序的方法是其是一個不需要額外空間的排序算法,又稱原地排序,不需要歸並排序中那樣數組的復制什么的。主要原理就是找一個分割元素,把數組分成左邊和右邊,然后遞歸,快速排序一般情況下是不穩定的排序算法,直接貼3種快速排序的算法,哪一種容易記憶挑哪一種呀~不過性能最好的是三數取中是性能最好的啦,不過復雜了一點,
(1)算法導論中每次取最后元素作為分割元素
這個分割數組的原理保留兩個指示器元素,i,j,其中一個,假如為i是遍歷元素的指示器,另外一個指示器保留的位置是其前面的元素均小於分割元
步驟是i和j初始化相同的位置,起始坐標,i向后遍歷,遇到小於分割元素的時候,此時更換當前元素和j元素指示位置,j++,這時候j前面就是小於分割元素的元素,最后結束的時候將分割元素和j的元素更換,則分割元素放到中間位置




bool swap(unsigned int& i,unsigned int& j)
{
unsigned int tmp;
tmp = i;
i = j;
j = tmp;
return true;
}
unsigned int partitionLast(unsigned int* array,int begin,int end)
{
unsigned int divide = array[end];
int i = begin - 1;
int j;
for (j = begin;j < end;j++)
{
if (array[j]<divide)
{
i++;
swap(array[i],array[j]);
}
}
swap(array[i+1],array[end]);
return i+1;
}
bool quickSortLast(unsigned int* array,int begin,int end)
{
unsigned int divide;
if (begin<end)
{
divide = partitionLast(array,begin,end);
quickSortLast(array,begin,divide-1);
quickSortLast(array,divide+1,end);
}
return true;
}
(2)每次取第一個元素作為分割元素,好像是叫霍爾(Hore)排序
這個方法不同於第一個方法,沒有分割出來具體的分割部分,原理如下:
首先拿出第一個元素作為分割元素。此時第一個元素是可復寫的狀態,所以此時從后面遍歷,找到第一個小於分割元素的元素,復寫第一個元素,此時它是可復寫狀態,j保留了它的位置,所以這時候利用i從前面遍歷,找到第一個大於分割元素的元素,復寫j的狀態,然后在從j進行,如此交替直到i=j的時候,這時候此位置的元素為可復寫狀態,分割元素填入及分割完畢,接下來遞歸調用

bool quickSortFirst(unsigned int* array,int begin,int end)
{
int i,j;
unsigned int divide = array[begin];
i = begin;
j = end;
while (i<j)
{
while (i<j&&array[j]>=divide)
j--;
array[i] = array[j];
while (i<j&&array[i]<=divide)
i++;
array[j] = array[i];
}
array[i]=divide;
if (i-1 > begin)
quickSortFirst(array,begin,i-1);
if (i+1 < end)
quickSortFirst(array,i+1,end);
return true;
}
(3)三數取中快速排序實現
這個方法更加仔細的選擇分割元素,這個方法在選分割元素的時候是比較開始元素,末尾元素,中間元素的大小,然后選取中間大小的元素,並且將其和倒數第二個元素交換,這樣末尾元素和開始元素已經在兩邊了,提升了一些效率,避免了最壞情況,這種方法效率比較高。
unsigned int selectThreeDivide(unsigned int* array,int begin,int end)
{
int mid = (end+begin)/2;
if (array[begin]>array[mid])
swap(array[begin],array[mid]);
if (array[begin]>array[end])
swap(array[begin],array[end]);
if (array[mid]>array[end])
swap(array[mid],array[end]);
swap(array[mid],array[end-1]);
return array[end-1];
}
bool quickSortThree(unsigned int* array,int begin,int end)
{
int i,j;
unsigned int divide = selectThreeDivide(array,begin,end);
i = begin;
j = end - 1;
while (i<j)
{
while (i<j&&array[i]<=divide)
i++;
array[j] = array[i];
while (i<j&&array[j]>=divide)
j--;
array[i] = array[j];
}
array[i] = divide;
if (i-1>begin)
quickSortThree(array,begin,i-1);
if (i+1<end)
quickSortThree(array,i+1,end);
return true;
}
快速排序時間復雜度分析:
快速排序的時間復雜度分析也比較復雜,不過針對有序的元素應用上面的固定選擇分割元素的時候會達到最壞的情況,最壞的情況就是每次分割的時候有一邊沒有元素,這樣時間復雜度就是O(n2),不過實際應用中很少針對已經有序的數組進行排序,

不過快速排序針對這個方法有隨機化的方法,每次隨機選取分割元素。這樣即可存在最壞情況交叉,也總能夠得到好的情況。


仍然屬於Θ(nlgn)的范圍。
隨機化快速排序的分析需要利用隨機化的分析方法,引入隨機指示器變量,針對時間復雜度求期望,數學證明比較復雜,這里略去。了解隨機化快速排序的時間復雜度為Θ(nlgn)即可。
快速排序效率較高的原因其中之一是緩存命中率較高,不需要頻換的調換緩存,這個因素還會影響后面基數排序效率的測試~
基於比較排序的下限:
基於比較排序下限的證明是通過決策樹證明的,決策樹的高度Ω(nlgn),這樣就得出了比較排序的下限。

證明方法是每次比較排序均需要至葉節點算排序結束,然而n個元素共有n!個葉節點,根據二叉樹的性質可知高度h的葉節點個數最多等於2h,h>=lg(n!),且n!改寫為(n/e)n,所以h>=nlgn-nlge,所以下限是nlgn
由於基於比較的下限是nlgn,所以針對快速排序等一些排序算法已經能夠達到好的結果,時間復雜度明顯提高不太可能了,所以下面介紹一些非比較排序,有些能夠達到線性的時間。
1、計數排序
下面的代碼是基數排序中計數排序的部分
bool countSort(radixSortNum* array,radixSortNum* arrayAssit,int* arrayCount,int length,int k)
{
int i;
for (i = 0;i<k;i++)
{
arrayCount[i] = 0;
}
for (i = 0;i<length;i++)
{
arrayCount[array[i].forCountSort]++;
}
for (i = 0;i<k-1;i++)
{
arrayCount[i+1]+=arrayCount[i];
}
for (i = length-1;i>=0;i--)
{
arrayAssit[arrayCount[array[i].forCountSort]-1].forCountSort = array[i].forCountSort;
arrayAssit[arrayCount[array[i].forCountSort]-1].src = array[i].src;
arrayCount[array[i].forCountSort]--;
}
memcpy(array,arrayAssit,(length)*sizeof(array[0]));
return true;
}
這種排序方法一般情況是穩定的排序方法,所以能夠利用在基數排序中,基數排序要求的子排序部分是穩定的排序方法。

計數排序預先處理成如下的結構,C‘中保存的元素及為每個元素的位置,直接可輸出。如下,看了圖應該就知道原理了吧,具體C中就是保留了B中元素的應該輸出的位置信息。該排序方法適合排序的范圍k比較小




2、基數排序
基數排序中穩定的排序算法使用的是計數排序,關於基數算法的實現的討論主要是r 值取多少才能保證效率的最高。書上給出了排序的時間復雜度公式
,因此書上給出了理論上的r 的最合理的值為lg(n)。但是實驗對這個r 值的選取進行了測試,發現實際情況並不是lg(n)使得算法達到最優,具體r 值的選取參見性能比較的結論。 這里取的每段r 值的大小的時候用位操作,相比%更加有效率。
bool PrecountSort(radixSortNum* array,int length,int k)
{
radixSortNum* arrayAssit = new radixSortNum[length];
int* arrayCount = new int[k];
countSort(array,arrayAssit,arrayCount,length,k);
delete [] arrayAssit;
delete [] arrayCount;
return true;
}
bool countSort(radixSortNum* array,radixSortNum* arrayAssit,int* arrayCount,int length,int k)
{
int i;
for (i = 0;i<k;i++)
{
arrayCount[i] = 0;
}
for (i = 0;i<length;i++)
{
arrayCount[array[i].forCountSort]++;
}
for (i = 0;i<k-1;i++)
{
arrayCount[i+1]+=arrayCount[i];
}
for (i = length-1;i>=0;i--)
{
arrayAssit[arrayCount[array[i].forCountSort]-1].forCountSort = array[i].forCountSort;
arrayAssit[arrayCount[array[i].forCountSort]-1].src = array[i].src;
arrayCount[array[i].forCountSort]--;
}
memcpy(array,arrayAssit,(length)*sizeof(array[0]));
return true;
}
bool radixSort(radixSortNum* array,int length,int numlen,int r)
{
bitset<32> bitmode(0x0000);
int i;
if (r == 0)
r = (int)(log(double(length))/log((double)2));
cout<<"r:"<<r<<endl;
for (i = 0;i < r;i++)
{
bitmode[i] = 1;
}
unsigned int mode = bitmode.to_ulong();
//cout<<mode<<endl;
//cout<<bitmode<<endl;
numlen = 32;
int leftLen = (int)ceil((double)numlen/(double)r),j=0;
leftLen +=1;//盡量多挪動一次,后面超過31就退出循環;
//cout<<"需要挪動這么多次:"<<leftLen<<endl;
//cout<<"mode:"<<bitmode<<endl;
while (leftLen > 0)
{
if (j*r>31)
{
break;
}
for (i = 0;i<length;i++)
{
unsigned int tmp = array[i].src>>(j*r);
//bitset<32> bittmp(tmp);
//cout<<"time:"<<j+1<<"bitset:"<<bittmp<<endl;
array[i].forCountSort = (tmp)&mode;
}
PrecountSort(array,length,(int)pow((double)2,r));
j++;
leftLen--;
}
return true;
}



根據實驗表明,這里的R 與理論值有差別,R 值取12 的時候達到最高的效率,為什么跟理論值有偏差呢?這里考慮可能與CACHE 有關,因為R 取12 的時候,剛好基數排序里面的計數排序部分能夠在CACHE 中完成,所以效率最高。
3、桶排序

最好的元素一個元素對應一個坑位。
最后貼一個一些排序算法效率的比較


基數排序效率雖然較高,但是有一定的局限性,實現相對快速排序難度大一些,需要額外的空間,所以實際選擇中快速排序選擇比較多。
文章本意做復習筆記和分享用途,轉載請標明出處,謝謝~
