資料結構和演算法解析:排序問題簡易總結
直接插入排序
直接插入排序(Straight Insertion Sorting)的基本思想:在要排序的一組數中,假設前面(n-1) [n>=2] 個數已經是排好順序的,現在要把第n個數插到前面的有序數中,使得這n個數也是排好順序的。如此反覆迴圈,直到全部排好順序。 直接插入排序是一種穩定的排序方法,最好的時間複雜是O(n)也就是已經排好的序列,最差的時間複雜度是O(n^2)逆序
//插入排序是一種穩定的排序方法,最好的時間複雜是O(n)也就是已經排好的序列,最差的時間複雜度是O(n^2)逆序
public void insertSort(int nums[]){
int tmp;
for(int i=1;i<nums.length;i++){
int j=i;
while (j>0 && nums[j]<nums[j-1]){
tmp=nums[j];
nums[j]=nums[j-1];
nums[j-1]=tmp;
j--;
}
}
}
希爾排序
- 希爾排序(Shell’s Sort)是插入排序的一種又稱“縮小增量排序”(Diminishing Increment Sort),是直接插入排序演算法的一種更高效的改進版本。
- 希爾排序是把記錄按下標的一定增量分組,對每組使用直接插入排序演算法排序;隨著增量逐漸減少,每組包含的關鍵詞越來越多,當增量減至1時,整個檔案恰被分成一組,演算法便終止。
演算法思想
對於直接插入排序問題,資料量巨大時。將數的個數設為n,取奇數k=n/2,將下標差值為k的數分為一組,構成有序序列。再取k=k/2 ,將下標差值為k的書分為一組,構成有序序列。重複第二步,直到k=1執行簡單插入排序。
希爾排序是一種非常不穩定的排序方法,它的時間複雜度為O(n^(1.3—2))
//希爾排序是一個不穩定的排序演算法,它的時間複雜度為O(n^(1.3—2))
public void shellSortHelper (int nums[],int incr){
int tmp;
for(int i=incr;i<nums.length;i+=incr){
int j=i;
while (j>0 && nums[j]<nums[j-incr]){
tmp=nums[j];
nums[j]=nums[j-incr];
nums[j-incr]=tmp;
j-=incr;
}
}
}
//希爾排序
public void shellSort(int nums[]){
for(int i=nums.length/2;i > 0;i/=2){
shellSortHelper(nums,i);
}
}
選擇排序
-
常用於取序列中最大最小的幾個數時。 (如果每次比較都交換,那麼就是交換排序;如果每次比較完一個迴圈再交換,就是簡單選擇排序。)
-
遍歷整個序列,將最小的數放在最前面。
-
遍歷剩下的序列,將最小的數放在最前面。
-
重複第二步,直到只剩下一個數。
選擇排序的時間複雜度為O(n^2) 選擇排序不是一個穩定的排序
//交換排序是一種穩定的排序方法,最好最壞的時間複雜度都是O(n^2)
public void swapSort(int nums[])
{
int tmp;
for(int i=0;i<nums.length;i++){
for(int j=i+1;j<nums.length;j++){
if(nums[i]>nums[j]){
tmp=nums[j];
nums[j]=nums[i];
nums[i]=tmp;
}
}
}
}
//選擇排序是一種不穩定的排序方法,最好最壞的時間複雜度都是O(n^2)
public void selectSort(int nums[]){
for(int i=0;i<nums.length;i++){
int values=nums[i];
int position=i;
for(int j=i+1;j<nums.length;j++){
if(nums[j]<values){
values=nums[j];
position=j;
}
}
//swap
nums[position]=nums[i];
nums[i]=values;
}
}
氣泡排序
很簡單,用到的很少,據瞭解,面試的時候問的比較多!將序列中所有元素兩兩比較,將最大的放在最後面。將剩餘序列中所有元素兩兩比較,將最大的放在最後面。重複第二步,直到只剩下一個數。
氣泡排序的時間複雜度為O(n^2) 氣泡排序是一個穩定的排序
//氣泡排序 一種穩定的排序演算法,最好最壞的時間複雜度都是O(n^2)
public void bubbleSort(int nums[]){
int tmp;
for(int i=0;i<nums.length;i++){
for (int j=0;j<nums.length-i-1;j++){
if(nums[j]>nums[j+1]){
tmp=nums[j+1];
nums[j+1]=nums[j];
nums[j]=tmp;
}
}
}
}
快速排序
要求時間最快時。
- 選擇第一個數為p,小於p的數放在左邊,大於p的數放在右邊。
- 遞迴的將p左邊和右邊的數都按照第一步進行,直到不能遞迴。
快速排序是由一種不穩定的排序演算法,最好的時間複雜度是O(nLogN),最差時間複雜度是O(n^2)
//快速排序是由一種不穩定的排序演算法,最好的時間複雜度是O(nLogN),最差時間複雜度是O(n^2)
private void quickSortHelper(int nums[],int start,int end){
if(start>end) return;
int high=end;
int low=start;
int target=nums[start];
while (low<high){
while (low<high && nums[high]>=target){
high--;
}
nums[low]=nums[high];
while (low<high && nums[low]<=target){
low++;
}
nums[high]=nums[low];
}
nums[low]=target;
quickSortHelper(nums,start,low-1);
quickSortHelper(nums,low+1,end);
}
public void quickSort(int nums[])
{
quickSortHelper(nums,0,nums.length-1);
}
歸併排序
速度僅次於快速排序,記憶體少的時候使用,可以進行平行計算的時候使用。
- 選擇相鄰兩個陣列成一個有序序列。
- 選擇相鄰的兩個有序序列組成一個有序序列。重複第二步,直到全部組成一個有序序列。
歸併排序,是一種不穩定的排序是演算法這個演算法的時間複復雜度都是O(NlogN)
//歸併排序,是一種不穩定的排序是演算法這個演算法的時間複復雜度都是O(NlogN)
public void mergeSrot(int nums[],int start,int end){
if(start>=end) return;
int mid=(start+end)/2;
mergeSrot(nums,start,mid);
mergeSrot(nums,mid+1,end);
mergeSrotHelper(nums,start,mid,end);
}
// 將兩個有序序列歸併為一個有序序列(二路歸併)
private void mergeSrotHelper(int[] nums, int start, int mid, int end) {
int[] arr=new int[end+1];
int low=start;
int left=start;
int center=mid+1;
while (left<=mid && center<=end){
arr[low++] = nums[left] <= nums[center] ? nums[left++] : nums[center++];
}
while (left<=mid){
arr[low++]=nums[left++];
}
while (center<=end){
arr[low++]=nums[center++];
}
for(int i=start;i<=end;i++){
nums[i]=arr[i];
}
}
堆排序
對簡單選擇排序的優化。
- 將序列構建成大頂堆。
- 將根節點與最後一個節點交換,然後斷開最後一個節點。
- 重複第一、二步,直到所有節點斷開。
//堆排序
private boolean isLeaf(int nums[],int pos)
{
//沒有葉子節點
return pos*2+1>=nums.length;
}
private void swap(int[] nums,int pos1,int pos2)
{
int tmp;
tmp=nums[pos2];
nums[pos2]=nums[pos1];
nums[pos1]=tmp;
}
private void shiftdown(int[] nums,int pos)
{
while(!isLeaf(nums,pos))
{
int left=pos*2+1;
int right=pos*2+2;
if(right<nums.length)
{
left=nums[left]>nums[right]?left:right;
}
//是否需要調整堆
if(nums[pos]>=nums[left]) return;
swap(nums,pos,left);
pos=left;
}
}
public void buildHeap(int nums[])
{
for(int i=nums.length/2-1;i>=0;i--)
{
shiftdown(nums,i);
}
}
public void heapSort(int nums[])
{
for(int i=nums.length-1;i>=0;i--)
{
swap(nums,0,i);
shiftdown(nums,i);
}
}
總結
一、穩定性:
- 穩定:氣泡排序、插入排序、歸併排序和基數排序
- 不穩定:選擇排序、快速排序、希爾排序、堆排序
二、平均時間複雜度
-
O(n^2):直接插入排序,簡單選擇排序,氣泡排序。
-
在資料規模較小時(9W內),直接插入排序,簡單選擇排序差不多。當資料較大時,氣泡排序演算法的時間代價最高。效能為O(n^2)的演算法基本上是相鄰元素進行比較,基本上都是穩定的。
-
O(nlogn):快速排序,歸併排序,希爾排序,堆排序。
其中,快排是最好的,其次是歸併和希爾,堆排序在資料量很大時效果明顯。
三、排序演算法的選擇
- 資料規模較小
-
(1)待排序列基本序的情況下,可以選擇直接插入排序;
-
(2)對穩定性不作要求宜用簡單選擇排序,對穩定性有要求宜用插入或冒泡
- 資料規模不是很大
-
(1)完全可以用記憶體空間,序列雜亂無序,對穩定性沒有要求,快速排序,此時要付出log(N)的額外空間。
-
(2)序列本身可能有序,對穩定性有要求,空間允許下,宜用歸併排序
- 資料規模很大
-
(1)對穩定性有求,則可考慮歸併排序。
-
(2)對穩定性沒要求,宜用堆排序
- 序列初始基本有序(正序),宜用直接插入,冒泡