1. 程式人生 > >常用演算法總結

常用演算法總結

排序演算法幾種分類方式:

1,穩定排序和不穩定排序

      如果a==b, 當排序之前a在b的前面,排序後,a仍然在b的前面,則該排序演算法為穩定排序演算法。否則為不穩定排序演算法。

2,非線性時間比較類排序和線性時間非比較類排序演算法

      非線性時間比較類排序:通過比較來決定元素間的相對位置,由於比較次數,使其時間複雜度不能

突破O(nlogn)。

      線性時間非比較類排序:不通過比較來決定元素間的相對位置,它可以突破比較排序的時間下限,以線性時間執行。

幾種常見的排序演算法介紹:

1,選擇排序

演算法原理:依次在元素間比較,從集合中找出最小的元素,放到集合最前面,再從剩下的集合中找出次小的元素,再放到當前集合最前面;依次迴圈,把所有的元素排好序。

平均時間複雜度O(n*n),空間複雜度O(1)。

選擇排序是不穩定排序

  1. // 選擇排序:  
        public int[] selectSort(int[] nums) {  
            long start = System.currentTimeMillis();  
            for (int i = 0; i < nums.length; i++) {  
                int minIndex = i;  
                for (int j = i + 1; j < nums.length; j++) {  
                    if (nums[j] < nums[minIndex]) {  
                        minIndex = j;  
                    }  
                }  
                swap(nums, i, minIndex);  
            }  
            System.out.println("Select Sort, count time = " + (System.currentTimeMillis() - start) + "ms");  
            return nums;  
        }  
            private void swap(int[] nums, int x, int y) {  
            if (x == y)  
                return;  
            int temp = nums[x];  
            nums[x] = nums[y];  
            nums[y] = temp;  
        }  

2,快速排序

演算法原理:從元素集合中挑選出一個基準(Pivot),一次遍歷之後,把所有大於基準的元素放在基準值的右邊,所有小於基準的元素放在基準值的左邊。然後遞迴分別對左邊和右邊執行同樣的操作。

遍歷過程如下:首先選定基準,然後分別從左邊和右邊開始遍歷,直至左右相遇則遍歷完成。左邊開始往右邊遍歷時,遇到比基準值大的元素,則停下來,右邊開始往左邊遍歷時,遇到比基準值小的元素,則停下來,然後把左右兩個元素交換。然後繼續遍歷,直至相遇。

注意:如果選定的基準是左邊第一個元素,則先從右邊開始往左遍歷,這樣能保證停下來時的元素是不大於基準的元素。反之,則從左邊開始遍歷。

平均時間複雜度O(nlogn),空間複雜度O(logn)。

快速排序是不穩定排序

 public int[] quickSort(int[] nums) {  
        long start = System.currentTimeMillis();  
        if (nums == null || nums.length == 0)  
            return nums;  
        quickSortByPivot(nums, 0, nums.length - 1);  
        System.out.println("Quick Sort, count time = " + (System.currentTimeMillis() - start) + "ms");  
        return nums;  
    }  
  
    private void quickSortByPivot(int[] nums, int left, int right) {  
        int l = left, r = right;  
        int pivot = nums[left];  
  
        while (l < r) {  
            while (r > l && nums[r] >= pivot)  
                r--;  
  
            while (r > l && nums[l] <= pivot)  
                l++;  
  
            if (l < r)  
                swap(nums, l, r);  
        }  
        swap(nums, left, l);  
  
        if (l - 1 > left)  
            quickSortByPivot(nums, left, l - 1);  
        if (right > l + 1)  
            quickSortByPivot(nums, l + 1, right);  
    }  

3,簡單插入排序

演算法原理:把當前元素插入到已排好序的元素集合的對應位置。把第一個元素當成已經排好序的元素,從第二個元素(新元素)開始,從排好序的元素(待比較元素)中後向前逐一掃描比較,如果待比較的元素比新元素大,則把新元素與待比較元素交換,然後新元素繼續往前比較,直至結束。當每個元素都與排好序的元素完成比較,則排序完成。

平均時間複雜度O(n*n),空間複雜度O(1)。

簡單插入排序是穩定排序

// 簡單插入排序:  
    public int[] simpleInsertSort(int[] nums) {  
        long start = System.currentTimeMillis();  
        for (int i = 1; i < nums.length; i++) {  
            int pre = i - 1, cur = i;  
            while (pre >= 0 && nums[pre] > nums[cur]) {  
                swap(nums, pre, cur);  
                cur = pre;  
                pre--;  
            }  
        }  
        System.out.println("Simple Insert Sort,count time = " + (System.currentTimeMillis() - start) + "ms");  
        return nums;  
    }  

4,希爾(shell)排序(縮小增量排序)

演算法原理:是插入排序的改進版,考慮到插入排序時,有可能某個元素需要插入到比較遠的位置,導致不斷的重複插入。因此希爾排序會優先比較距離較遠的元素。希爾排序引入步長概念,先選定一個步長(可根據集合大小確定),然後使用插入排序思想比較相隔步長距離的各個元素。然後把步長減一,繼續比較,直至步長為1,此時相當於插入排序,但是目前的集合已經近似有序了。

平均時間複雜度O(n^1.3),空間複雜度O(1)。

希爾排序是不穩定排序。

// 希爾排序 ,  
    public int[] shellSort(int[] nums) {  
        long start = System.currentTimeMillis();  
        if (nums ==  || nums.length == 0)  
            return ;  
        int gap = nums.length / 3;// 步長  
  
        while (gap > 0) {  
            for (int k = 0; k < gap; k++) {  
                int cur = k;  
                int pre = k;  
                while (cur < nums.length) {  
                    if (nums[cur] < nums[pre]) {  
                        swap(nums, cur, pre);  
                    }  
                    pre = cur;  
                    cur += gap;  
                }  
            }  
            gap--;  
        }  
        System.out.println("Shell Sort,count time = " + (System.currentTimeMillis() - start) + "ms");  
        return nums;  
    }  

5,氣泡排序

演算法原理:每次遍歷一次集合,比較相鄰元素,把較大的元素移到後面,完成一次遍歷時,則最大的元素已經移到最後,然後繼續遍歷剩下的無序集合,把當前集合最大的移到最後面。經歷n次遍歷後,完成排序。

氣泡排序還可以稍微改進,當排序過程中,發現待排序集合已經是有序的,則可以不需要進一步遍歷了。

平均時間複雜度O(n*n),空間複雜度O(1)。

氣泡排序是穩定排序。

// 氣泡排序:  
    public int[] bubbleSort(int[] nums) {  
        long start = System.currentTimeMillis();  
        if (nums ==  || nums.length == 0)  
            return ;  
        for (int i = 0; i < nums.length; i++) {  
            boolean isSorted = true;// 假設當前已經有序,氣泡排序改進,如果當前迴圈發現已經有序,則不需要繼續遍歷。  
            for (int j = 1; j < nums.length - i; j++) {  
                if (nums[j] < nums[j - 1]) {  
                    swap(nums, j, j - 1);// 一次遍歷後,如果有交換動作,則不是有序的.  
                    isSorted = false;  
                }  
            }  
            System.out.println("bubble Sort current index:" + i);  
            if (isSorted)  
                return nums;  
        }  
        System.out.println("Bubble Sort,count time = " + (System.currentTimeMillis() - start) + "ms");  
        return nums;  
    }  

6,堆排序

演算法原理:堆排序是利用堆這種資料結構來設計的排序演算法。堆可以看做一個近似完全二叉樹的結構。堆分為大頂堆和小頂堆。大頂堆滿足如下性質,父節點的值總是大於(等於)子節點的值,小頂堆滿足如下性質,父節點的值總是小於(等於)子節點的值。

從0開始對堆中的節點進行編號,則其節點和其父節點以及子節點的編號關係如下,當前節點編號為i:

父節點編號 parent(i) = i / 2;

左子節點編號 left(i) = 2 * i + 1;

右子節點編號 right(i) = 2 * i + 2;

堆排序過程為首先根據集合元素大小建立一個大頂堆,當前的堆是無序的,然後對每個元素進行調整,使其符合大頂堆的規則,當前節點值大於等於子節點值。完成一次堆的調整後,堆的第一個元素,即堆頂元素,則為集合中的最大元素。此時堆頂元素為有序元素,把它與集合中的最後一個位置的元素互換,然後繼續為剩下的其他元素建立大頂堆。直至最後,所有的元素都為有序元素。

平均時間複雜度O(nlogn),空間複雜度O(1)。

堆排序是不穩定排序。

// 最大堆排序:  
    public int[] heapSort(int[] nums) {  
        long start = System.currentTimeMillis();  
        if (nums ==  || nums.length == 0)  
            return ;  
  
        buildMaxHeap(nums);  
  
        System.out.println("Heap Sort,count time = " + (System.currentTimeMillis() - start) + "ms");  
        return nums;  
    }  
  
    private void adjustHeapOfIndex(int[] nums, int index, int heapSize) {  
        // System.out.println("index|heapSize:" + index + "|" + heapSize);  
        int left = index * 2 + 1;  
        int right = index * 2 + 2;  
        int largest = index;  
        if (left < heapSize && nums[left] > nums[largest])  
            largest = left;  
        if (right < heapSize && nums[right] > nums[largest])  
            largest = right;  
        if (index != largest) {  
            swap(nums, index, largest);  
            adjustHeapOfIndex(nums, largest, heapSize);  
        }  
        // this.printArrays("adjust heap", nums);  
    }  
  
    private int[] buildMaxHeap(int[] nums) {  
  
        int heapSize = nums.length;  
  
        while (heapSize > 0) {// 從nums.length到1,不斷構建最大堆;  
  
            // 構建最大堆;  
            for (int i = heapSize - 1; i >= 0; i--) { // 針對每個index,不斷調整堆;  
                adjustHeapOfIndex(nums, i, heapSize);  
            }  
            // this.printArrays("build max heap:",nums);  
            swap(nums, 0, heapSize - 1); // 堆構建完成後 最大的值為nums[0],完後交換放到最後  
  
            heapSize--;  
        }  
  
        return nums;  
    }  

7,歸併排序

 演算法原理:歸併排序主要是採用分治法,把兩個已排好序的子集合合併成一個有序的集合。子集的排序則是採用遞迴方法,如果子集只包含一個元素,則為一個有序的集合。

平均時間複雜度O(nlogn),空間複雜度O(n)。

歸併排序是穩定排序。

// 歸併排序  
    public int[] mergeSort(int[] nums) {  
        long start = System.currentTimeMillis();  
        if (nums ==  || nums.length == 0)  
            return ;  
        // System.out.println("start to merge sort:");  
        int[] temp = new int[nums.length];  
        mergeSort(nums, 0, nums.length - 1, temp);  
        System.out.println("Merge Sort,count time = " + (System.currentTimeMillis() - start) + "ms");  
        return nums;  
    }  
  
    public void mergeSort(int[] nums, int start, int end, int[] temp) {  
        if (end - start > 0) {  
            int sublength = (end - start) / 2;  
            mergeSort(nums, start, start + sublength, temp);// 分別把兩個子串排好序 即一直劃分到只剩一個元素,則就是一個排好序的子串。  
            mergeSort(nums, start + sublength + 1, end, temp);  
            // System.out.println("Merge Sort:" + start + "|" + sublength + "|" + end);  
            mergeArrays(nums, start, start + sublength, start + sublength + 1, end, temp);// 把兩個排好序的子串合併  
        }  
    }  
  
    private int[] mergeArrays(int[] nums, int lstart, int lend, int rstart, int rend, int[] temp) {  
        if ((lend - lstart) < 0 && (rend - rstart) < 0)  
            return nums;  
  
        int tempindex = lstart;  
        int leftindex = lstart, rightindex = rstart;  
        // System.out.println("tempindex|leftindex " + tempindex + "|" + leftindex);  
        while (leftindex <= lend && rightindex <= rend) {  
            if (nums[leftindex] <= nums[rightindex]) {  
                temp[tempindex++] = nums[leftindex++];  
            } else {  
                temp[tempindex++] = nums[rightindex++];  
            }  
        }  
  
        while (leftindex <= lend) {  
            temp[tempindex++] = nums[leftindex++];  
        }  
        while (rightindex <= rend) {  
            temp[tempindex++] = nums[rightindex++];  
        }  
        // System.out.println("fininsh index " + tempindex + "|" + leftindex);  
        for (int k = lstart; k <= rend; k++) {// 把經過處理的數組裡的值全部更新到原陣列中  
            nums[k] = temp[k];  
        }  
        // this.printArrays("Merge Arrays:", nums);  
        return nums;  
    }  

8,基數排序

演算法原理:基數排序是把待排序的數字分為低位和高位,低位先排序,然後收集,再按高位排序,再收集,直至最高位,則排序完成。注意基數排序無法處理負數。

排序過程為,首先得到集合中的最大數,根據最大數,得到需要按位迴圈排序的次數。

在每次按位排序時,先初始化10個桶,分別存放餘數為0-9的元素個數,在一次遍歷之後,10個桶中已儲存各個餘數出現的次數,然後再根據餘數個數更新10個桶中的相應元素應該存放的位置,比如餘數為1的元素會排在餘數為0的元素的後面,因此如果餘數為0的元素個數為5,則餘數為1的元素至少會在第5個位置之後,由此統計元素應該在集合中出現的位置。

之後再次從大到小遍歷整個集合,根據元素的餘數,得到對應桶的值,即為該元素在新的集合中的下標值,存放好一個元素,就把對應桶中的值相應減一。遍歷結束,則這次按位排序結束,得到一個按低位排序的集合。

再取較高位,進行排序。直至按最高位排序結束,則此時得到的集合就是一個有序的集合。

平均時間複雜度O(2n*k),k為按位排序的次數;空間複雜度O(n+k),k為桶的數量;資料量大的話,k會遠遠小於n。 

基數排序是穩定排序。

// 基數排序 基數排序不能處理負數;
	public int[] radixSort(int[] nums) {
		long start = System.currentTimeMillis();
		if (nums == null || nums.length == 0)
			return null;

		int max = nums[0];
		for (int cur : nums) {// 先得到最大數
			if (cur > max)
				max = cur;
		}

		int[] temp = new int[nums.length];

		int exp = 1;
		do {
			int[] buckets = new int[10];// 針對餘數的桶 統計餘數個數
			for (int j = 0; j < nums.length; j++) {
				buckets[(nums[j] / exp) % 10]++;// 儲存每個餘數出現的個數
			}
			// this.printArrays("buckets nums of exp:" + exp, buckets);

			for (int l = 1; l < 10; l++) {
				buckets[l] += buckets[l - 1];// 統計餘數對應的數按順序出現在整個陣列中的位置 即餘數越小 則其在陣列中排在越前面
			}
			// this.printArrays("buckets:", buckets);

			// this.printArrays("before bucket:", nums);
			// 從陣列最後往前迴圈 因為buckets中資料的位置從大到小遞減.
			for (int m = nums.length - 1; m >= 0; m--) {
				// System.out.println("m, value:" + m + "|" + nums[m] + "|" +
				// buckets[(nums[m]/exp)%10]);
				// this.printArrays("nums:", nums);
				temp[buckets[(nums[m] / exp) % 10] - 1] = nums[m];// 每儲存一個數據後,則把buckets中對應的資料減一;
				buckets[(nums[m] / exp) % 10]--;
				// this.printArrays("buckets:", buckets);
			}

			for (int k = 0; k < nums.length; k++) {// 更新值
				nums[k] = temp[k];
			}

			// this.printArrays("After bucket:" + exp, temp);
			exp *= 10;
		} while (max / exp > 0);

		System.out.println("Radix Sort,count time = " + (System.currentTimeMillis() - start) + "ms");
		return nums;
	}

9,計數排序

演算法原理:計數排序不是基於比較,而是將待排序的資料轉換為鍵儲存在額外開闢的陣列空間中。作為一種線性時間複雜度的排序,計數排序要求待排序的資料必須是有確定範圍的整數。

排序過程為,先得到待排序資料中的最大值和最小值,然後根據最大值和最小值開闢一個新的陣列空間,然後遍歷整個資料集合,統計各個元素出現的次數,並且存入與元素值對應的陣列下標。

然後對陣列中的計數進行統計,計算出每個元素應該存放的位置,最後反向遍歷資料集合,根據每個元素值,在陣列中獲取元素應該存放的位置。

平均時間複雜度O(n+k),空間複雜度(n+k)。k為資料集合中最大值和最小值的範圍。如果待排序集合資料比較集中,計算排序是一個很有效的排序演算法。

計算排序是穩定排序。

// 計數排序
	public int[] countingSort(int[] nums) {
		long start = System.currentTimeMillis();
		if (nums == null || nums.length == 0)
			return null;
		// 先得到待排序資料的範圍
		int min = nums[0], max = nums[0];
		for (int curvalue : nums) {
			if (curvalue < min)
				min = curvalue;
			if (curvalue > max)
				max = curvalue;
		}

		// 根據資料範圍初始化陣列 進行計數
		int[] counts = new int[max - min + 1];

		// 遍歷陣列 進行計數
		for (int k : nums) {
			counts[k - min]++;
		}

		// 更新陣列中各元素的位置
		for (int k = 1; k < counts.length; k++) {
			counts[k] += counts[k - 1];
		}

		this.printArrays("Counting arr:", counts);

		int[] temp = new int[nums.length];
		for (int i = nums.length - 1; i >= 0; i--) {
			// System.out.println("counts index:" + (nums[i]-min) + ",count value:" +
			// counts[nums[i]-min]);
			temp[counts[nums[i] - min] - 1] = nums[i];
			counts[nums[i] - min]--;
		}

		for (int j = 0; j < nums.length; j++) {// 更新值
			nums[j] = temp[j];
		}

		System.out.println("Counting Sort,count time = " + (System.currentTimeMillis() - start) + "ms");
		return nums;
	}

10,桶排序

演算法原理:桶排序是計數排序的升級版,利用函式的對映關係,把資料對映到有限的桶中,然後對每個桶進行單獨排序,可使用快速排序等其他演算法。最後把幾個桶的元素合併,完成排序。

桶排序的時間複雜度取決於各個桶的排序演算法,其他部分的時間複雜度為O(n)。顯然,桶劃分得越多,各個桶中的資料越少,每個桶的排序時間越少,效率越高,但相應的,空間消耗也越大。

平均時間複雜度O(n+k),空間複雜度O(n+k)。

// 桶排序演算法
	public int[] bucketSort(int[] nums) {
		long start = System.currentTimeMillis();
		if (nums == null || nums.length == 0)
			return null;
		if (nums.length <= 1)
			return nums;

		//this.printArrays("bucket sort:", nums);
		// 先得到待排序資料的範圍
		int min = nums[0], max = nums[0];
		for (int curvalue : nums) {
			if (curvalue < min)
				min = curvalue;
			if (curvalue > max)
				max = curvalue;
		}

		// 根據數的範圍 確定桶的個數 以及桶的對映函式
		// 在此假設最多分十個桶 然後把資料分別放入對應桶內
		// 每個桶使用List
		int bucketNum = 10;
		int step = 0;
		if ((max - min) < 10)
			bucketNum = max - min + 1;
		step = (max - min + 1) / bucketNum;// 根據bucketNum和step分出bucketNum個桶,範圍 [min,min+step),最後一個桶還包含剩下的餘數;
		// 初始化桶
		List[] buckets = new ArrayList[bucketNum];
		for (int bk = 0; bk < buckets.length; bk++) {
			buckets[bk] = new ArrayList();
		}
		System.out.println("Bucketnum:" + bucketNum);
		// 遍歷陣列 把元素分別放入桶中
		for (int curnum : nums) {
			int curindex = (curnum - min) / step;
			if (curindex > buckets.length - 1)
				curindex = buckets.length - 1;
			System.out.println("curnum:" + curnum + ",curindex = " + curindex + ",min = " + min + ",step = " + step);
			buckets[curindex].add(curnum);
		}

		// 分別對各個桶進行排序 並且把每個桶中各元素合併
		int index = 0;
		for (List list : buckets) {
			int[] temp = new int[list.size()];
			for (int k = 0; k < list.size(); k++) {
				temp[k] = (Integer) list.get(k);
			}
			int[] tempresult = bucketSort(temp);
			if (tempresult != null)
				for (int value : tempresult)
					nums[index++] = value;
		}

		System.out.println("Bucket Sort,count time = " + (System.currentTimeMillis() - start) + "ms");
		return nums;
	}

排序演算法總結:

1,如果待排序的集合規模較小,如n<50,可用簡單插入排序或者選擇排序。

2,如果待排序的集合基本有序,可用簡單插入排序或者氣泡排序。

3,如果待排序的集合規模較大,則應採用時間複雜度O(nlogn)的排序方法,如快速排序,堆排序或歸併排序。

     當待排序的元素是隨機分佈的,快速排序是平均時間最短的;

     堆排序雖然時間複雜度跟快速排序一樣,但是實際上的時間消耗(建堆,調整堆)會比快速排序多,因此一般選擇快速排序。

     但是相比快速排序,堆排序的空間的優點是佔用更少,也不像快速排序,會出現最壞的情況。

     如果需要穩定的排序演算法,則可選歸併排序。

4,堆排序適用於在一個集合n中,快速找出前m個元素(m<n),每次建堆,調整堆,都找出一個元素,執行m次則找出前m個元素。

5,如果待排序的資料比較集中。則可以採用計數排序。

6,實際演算法使用時,可以多個演算法一起使用,以達到更好的效果。如歸併排序中,桶排序中,對子串的處理都可以選擇其他效率更好的演算法。