1. 程式人生 > >時間複雜度為O(N*logN)的常用排序演算法總結與Java實現

時間複雜度為O(N*logN)的常用排序演算法總結與Java實現

時間複雜度為O(N*logN)的常用排序演算法主要有四個——快速排序、歸併排序、堆排序、希爾排序

1.快速排序

·基本思想

    隨機的在待排序陣列arr中選取一個元素作為標記記為arr[index](有時也直接選擇起始位置),然後在arr中從後至前以下標j尋找比arr[index]小的數,然後從前至後以下標i尋找比arr[index]大的數,如果i<j則交換二者的值;重複以上操作,直到i>=j,之後交換arr[index]與arr[j]的值,同時在i>begin中遞迴排序標記值的左側陣列,在j<end中遞迴標記值的右側陣列,直到結束,排序完成。

·程式碼實現

這裡只提供一種實現,快速排序有多種實現。

public void quickSort(int[] arr,int begin,int end) {
    int i = begin,j = end;
    //以開始下標的值為標記
    int index = begin;
    while(i<j) {
        //從後到前尋找比標記小的值
        /*
        * 這裡需要注意,只能先從後開始找,否則如果先從前找的話,
        * 以後交換arr[j]與arr[index]的值會使比arr[index]大的值在左邊
        */
        while (arr[j]>=arr[index]&&i<j) {
            j--;
        }
        //從前到後尋找比標記大的值
        while (arr[i]<=arr[index]&&i<j) {
            i++;
        }
        //如果i<j才交換
        if (i<j) {
            int temp = arr[i];
            arr[i] = arr[j];
            arr[j] = temp;
        }
    }
    //將標記的值與arr[j]交換
    int temp = arr[j];
    arr[j] = arr[index];
    arr[index] = temp;
    //遞迴標記值的左右陣列
    if (i>begin) {
        quickSort(arr,begin,i-1);
    }
    if (j<end) {
        quickSort(arr,i+1, end);
    }
}
·效能分析

    快速排序其實是氣泡排序的一種改進,每次遞迴,都將整個陣列拆分成兩部分分別遞迴,每一次劃分過程的時間複雜度為O(N),平均情況下,快速排序的時間複雜度為O(N*logN),快速排序的交換次數是不確定的,主要取決於標記值的選取,因此額外空間複雜度是O(logN)~O(N),同時快速排序是不穩定的排序演算法,相同的值如果都比標記值大或小,則會進行相對次序上的改變。

2.歸併排序

·基本思想

    歸併排序總的大概流程如下:


    也就是說,將陣列拆分成規模最小的,然後將陣列逐漸的兩兩合併,最終合併成最初的陣列。在程式碼實現上,則是利用遞迴的思想,如果發現當前陣列的開始下標小於結束下標,則將陣列遞迴拆分,直到拆分成只有一個元素的時候,開始逐層向上合併。

·程式碼實現
//合併函式
public void mergeCombine(int[] arr,int begin,int mid,int end,int[] temp) {
    int i=begin,j=mid+1,k=0;
    //將兩段陣列按大小順序合併並排列
    while(i<=mid&&j<=end) {
        if (arr[i] <= arr[j]) {
            temp[k++] = arr[i++];
        }else {
            temp[k++] = arr[j++];
        }
    }
    //合併剩餘的陣列
    //i>mid說明前半段已經合併完畢
    while(i<=mid) {
        temp[k++] = arr[i++];
    }
    while(j<=end) {
        temp[k++] = arr[j++];
    }
    for (i = 0; i < k; i++) {
        arr[begin + i] = temp[i];
    }  
}
//歸併排序函式
//先傳入一個temp輔助陣列,否則每次遞迴的時候都要建立
public void mergeSort(int[] arr,int begin,int end,int[] temp) {
    //防止範圍過大而越界
    int mid = begin + (end - begin)/2;
    if (begin<end) {
        mergeSort(arr, begin, mid,temp);
        mergeSort(arr, mid+1, end,temp);
        mergeCombine(arr, begin, mid, end,temp);
    }
}
·效能分析

    歸併排序的劃分過程的時間複雜度是O(logN),每一個合併過程的時間複雜度是O(N),總體上的時間複雜度是O(N*logN),歸併排序的效率屬於比較高的。歸併排序的額外空間複雜度是O(N)(注意,只有當直接傳入輔助陣列而不是每次遞迴都定義輔助陣列的時候才是O(N)),同時,歸併排序是穩定的排序演算法,當元素相等的時候,不會進行交換。

3.堆排序

·基本思想
    堆有大根堆和小根堆之分,大根堆表示父節點大於或等於左右孩子節點(這裡採用大根堆的方法),同時調整根堆的方法也有遞迴的和非遞迴的,但是基本思想不變。先建造大根堆(建造的過程中就是用到了調整根堆的函式),然後將堆頭和堆尾的元素進行交換,並且使根堆大小減一,同時調整根堆,每次調整完畢都交換一次,總共交換陣列大小-1次,最終按序輸出陣列即可。
·程式碼實現

    ①使用遞迴版本的大根堆排序

/**
 * 遞迴版本的堆排序
 */
public class MaxHeap {
	//記錄堆
	int[] heap;
	//堆的大小(節點數目)
	int heapSize;
	
	public MaxHeap(int[] heap, int heapSize) {
		this.heap = heap;
		this.heapSize = heapSize;
	}
	
	public void buildMaxHeapify() {
		//從最後一個非葉子節點開始向上遞迴
		for(int i = heapSize/2-1 ; i >=0  ; i --) {
			maxHeapify(i);
		}
	}
	
	//堆排序演算法
	public void heapSort() {
		for(int i = 0 ; i < heap.length-1 ; i ++) {
			int temp = heap[0];
			heap[0] = heap[heapSize-1];
			heap[heapSize-1] = temp;
			heapSize--;
			maxHeapify(0);
		}
	}
	
	//調整大根堆
	public void maxHeapify(int index) {
		int left = getLeft(index);
		int right = getRight(index);
		int max = 0;
		if (left<heapSize&&heap[left]>heap[index]) {
			max = left;
		}else {
			max = index;
		}
		if (right<heapSize&&heap[right]>heap[max]) {
			max = right;
		}
		//max==index說明頭節點是最大的元素
		if (max==index||max>heapSize) {
			return ;
		}
		//否則交換頭節點與最大值,繼續調整以這個最大值為下標的堆
		int temp = heap[index];
		heap[index] = heap[max];
		heap[max] = temp;
		maxHeapify(max);
	}
	
	//得到左兒子節點的座標
	private int getLeft(int index) {
		return 2*index+1;
	}
	
	public int getRight(int index) {
		return 2*(index+1);
	}
}

    ②使用非遞迴版本的大根堆

/**
 * 非遞迴版本的堆排序
 */
public class MaxHeap2 {

	int[] heap;
	int heapSize;
	
	public MaxHeap2(int[] heap, int heapSize) {
		super();
		this.heap = heap;
		this.heapSize = heapSize;
	}

	public void heapSort() {
		for(int i = 0 ; i <heap.length-1 ; i ++) {
			int temp = heap[0];
			heap[0] = heap[heapSize-1];
			heap[heapSize-1] = temp;
			heapSize--;
			maxHeapify(0);
		}
	}
	
	public void buildMaxHeapify() {
		for(int i = heapSize/2-1 ; i >= 0 ; i --) {
			maxHeapify(i);
		}
	}
	
	public void maxHeapify(int index) {
		int left = getLeft(index);
		int right = getRight(index);
		int max = 0;
		//left>=heapSize的話,說明當前index是最後一個非葉節點
		while(left<heapSize) {
			if (left<heapSize&&heap[left]>heap[max]) {
				max = left;
			}else {
				max = index;
			}
			if (right<heapSize&&heap[right]>heap[max]) {
				max = right;
			}
			if (max!=index) {
				int temp = heap[index];
				heap[index] = heap[max];
				heap[max] = temp;
			}else {
				break;
			}
			//如果進行了交換,則可能頭節點小於子節點,需要再次調整根堆
			index = max;
			left = getLeft(max);
			right = getRight(max);
		}
	}
	
	//得到左兒子節點的座標
	private int getLeft(int index) {
		return 2*index+1;
	}
		
	public int getRight(int index) {
		return 2*(index+1);
	}
}
·效能分析

    堆排序的時間複雜度是O(N*logN),不需要額外的輔助陣列,屬於原地演算法的一種,額外複雜度是O(1),同時,堆排序因為每次都要把堆首和堆尾值進行交換,所以它是不穩定的排序演算法。

·希爾排序

·基本思想

    希爾排序是插入排序的改進,插入排序是無序區與有序區相鄰的比較,然後找到其合適的位置。而希爾排序則是先選定一個步長,按照這個步長,將其與之前的值(i與i-步長)進行比較,考慮是否改變二者的次序,逐漸縮小步長,直至步長為1則排序完畢。

·程式碼實現
/**
* @param arr 待排序陣列
* @param index	初始的步長
*/
public void shellSort(int[] arr,int index) {
    while(index>=1) {
        for(int i = 0 ; i < arr.length ; i ++) {
            for(int j = i ; j > 0+index-1 ; j --) {
                if (arr[j]<arr[j-index]) {
                    int temp = arr[j];
                    arr[j] = arr[j-index];
                    arr[j-index] = temp;
                }
            }
        }
        index--;
    }
}
·效能分析

    希爾排序的平均時間複雜度為O(N*logN),只需要交換元素位置時的額外空間,所以額外空間複雜度為O(1);另外,一步的插入排序是穩定的,但當步長>1的時候,相同的值可能與不同的值做比較,則會改變其相對位置,因此是不穩定的。