八大排序演算法實戰:思想與實現

書呆子Rico發表於2017-05-27

所謂排序,就是根據排序碼的遞增或者遞減順序把資料元素依次排列起來,使一組任意排列的元素變為一組按其排序碼線性有序的元素。本文將介紹八種最為經典常用的內部排序演算法的基本思想與實現,包括插入排序(直接插入排序,希爾排序)、選擇排序(直接選擇排序,堆排序)、交換排序(氣泡排序,快速排序)、歸併排序、分配排序(基數排序),並給出各種演算法的時間複雜度、空間複雜度和穩定性。

喜歡的朋友記得點點贊和關注,支援下哦

八大排序演算法實戰:思想與實現

一. 排序演算法概述

本文將介紹八種最為經典常用的內部排序演算法,包括插入排序(直接插入排序,希爾排序)、選擇排序(直接選擇排序,堆排序)、交換排序(氣泡排序,快速排序)、歸併排序、分配排序(基數排序)。實際上,無論是基本排序方法(直接插入排序,直接選擇排序,氣泡排序),還是高效排序方法(快速排序,堆排序,歸併排序)等,它們各有所長,都擁有特定的使用場景。因此,在實際應用中,我們必須根據實際任務的特點和各種排序演算法的特性來做出最合適的選擇。一般地,我們衡量一個演算法的指標包括:

  1. 時間複雜度 (在排序過程中需要比較和交換的次數)
  2. 空間複雜度 (在排序過程中需要的輔助儲存空間)
  3. 穩定性 (該演算法的實現是否可以保證排序後相等元素的初始順序,只要該演算法存在一種實現可以保證這種特徵,那麼該演算法就是穩定的)
  4. 內部排序/外部排序 (在排序過程中資料元素是否完全在記憶體)

筆者將在本文著重探討上述八種排序演算法的思想和實現,並就各演算法根據以上指標進行分析和歸類,以便進一步熟悉它們各自的應用場景。

二. 插入排序

插入排序的基本思想:每步將一個待排序元素,按其排序碼大小插入到前面已經排好序的一組元素中,直到元素全部插入為止。在這裡,我們介紹三種具體的插入排序演算法:直接插入排序,希爾排序與折半插入排序。

1、直接插入排序

直接插入排序的思想:當插入第i(i>=1)個元素時,前面的V[0],…,V[i-1]等i-1個 元素已經有序。這時,將第i個元素與前i-1個元素V[i-1],…,V[0]依次比較,找到插入位置即將V[i]插入,同時原來位置上的元素向後順移。在這裡,插入位置的查詢是順序查詢。

直接插入排序是一種穩定的排序演算法,其實現如下:

/**
 *
 * Title: 插入排序中的直接插入排序,依賴於初始序列
 *
 * Description: 在有序序列中不斷插入新的記錄以達到擴大有序區到整個陣列的目的
 *
 * 時間複雜度:最好情形O(n),平均情形O(n^2),最差情形O(n^2)
 *
 * 空間複雜度:O(1)
 *
 * 穩 定 性:穩定
 *
 * 內部排序(在排序過程中資料元素完全在記憶體)
 *
 */
public class StraightInsertionSort {
	public static int[] insertSort( int[] target )
	{
		if ( target != null && target.length != 1 ) /* 待排序陣列不為空且長度大於1 */
		{
			for ( int i = 1; i < target.length; i++ ) /* 不斷擴大有序序列,直到擴充套件到整個陣列 */
			{
				for ( int j = i; j > 0; j-- ) /* 向有序序列中插入新的元素 */
				{
					if ( target[j] < target[j - 1] ) /* 交換 */
					{
						int temp = target[j];
						target[j] = target[j - 1];
						target[j - 1] = temp;
					}
				}
			}
		}
		return(target);
	}
}
複製程式碼

2、希爾排序

希爾排序的思想:設待排序序列共n個元素,首先取一個整數gap<n作為間隔,將全部元素分為間隔為gap的gap個子序列並對每一個子序列進行直接插入排序。然後,縮小間隔gap,重複上述操作,直至gap縮小為1,此時所有元素位於同一個序列且有序。由於剛開始時,gap較大,每個子序列元素較少,排序速度較快;待到排序後期,gap變小,每個子序列元素較多,但大部分元素基本有序,所以排序速度仍較快。一般地,gap取 (gap/3 + 1)。

希爾排序是一種不穩定的排序方法,其實現如下:

/**
 *
 * Title: 插入排序中的希爾排序,依賴於初始序列
 *
 * Description: 分別對間隔為gap的gap個子序列進行直接插入排序,不斷縮小gap,直至為 1
 * 剛開始時,gap較大,每個子序列元素較少,排序速度較快;
 *
 * 待到排序後期,gap變小,每個子序列元素較多,但大部分元素基本有序,所以排序速度仍較快。
 *
 * 時間複雜度:O(n) ~ O(n^2)
 *
 * 空間複雜度:O(1)
 *
 * 穩 定 性:不穩定
 *
 * 內部排序(在排序過程中資料元素完全在記憶體)
 */
public class ShellSort {
	public static void shellSort( int[] target )
	{
		if ( target != null && target.length != 1 )
		{
			int gap = target.length;
			while ( gap > 1 ) /* gap為int型,自動取整 */
			{
				gap = gap / 3 + 1;
				for ( int i = gap; i < target.length; i++ )
				{
					int j = i - gap;
					while ( j >= 0 )
					{
						if ( target[j + gap] < target[j] )
						{
							swap( target, j, j + gap );
							j -= gap;
						}else{
							break;
						}
					}
				}
			}
		}
	}
	public static void swap( int[] target, int i, int j )
	{
		int temp = target[i];
		target[i] = target[j];
		target[j] = temp;
	}
}
複製程式碼

3、折半插入排序

折半插入排序的思想:當插入第i(i>=1)個元素時,前面的V[0],…,V[i-1]等i-1個 元素已經有序。這時,折半搜尋第i個元素在前i-1個元素V[i-1],…,V[0]中的插入位置,然後直接將V[i]插入,同時原來位置上的元素向後順移。與直接插入排序不同的是,折半插入排序比直接插入排序明顯減少了關鍵字之間的比較次數,但是移動次數是沒有改變。所以,折半插入排序和插入排序的時間複雜度相同都是O(N^2),但其減少了比較次數,所以該演算法仍然比直接插入排序好。折半插入排序是一種穩定的排序演算法,其實現如下:

/**
 *
 * Title: 插入排序中的折半插入排序,依賴於初始序列
 *
 * Description: 折半搜尋出插入位置,並直接插入;與直接插入搜尋的區別是,後者的搜尋要快於順序搜尋
 *
 * 時間複雜度:折半插入排序比直接插入排序明顯減少了關鍵字之間的比較次數,但是移動次數是沒有改變。所以,
 *
 * 折半插入排序和插入排序的時間複雜度相同都是O(N^2),在減少了比較次數方面它確實相當優秀,所以該演算法仍然比直接插入排序好。
 *
 * 空間複雜度:O(1)
 *
 * 穩 定 性:穩定
 *
 * 內部排序(在排序過程中資料元素完全在記憶體)
 *
 */
public class BinaryInsertSort {
	public static int[] binaryInsertSort( int[] target )
	{
		if ( target != null && target.length > 1 )
		{
			for ( int i = 1; i < target.length; i++ )
			{
				int left = 0;
				int right = i - 1;
				int mid;
				int temp = target[i];
				if ( temp < target[right] ) /* 當前值小於有序序列的最大值時,開始查詢插入位置 */
				{
					while ( left <= right )
					{
						mid = (left + right) / 2;
						if ( target[mid] < temp )
						{
							left = mid + 1; /* 縮小插入區間 */
						}else if ( target[mid] > temp )
						{
							right = mid - 1; /* 縮小插入區間 */
						}else{ /* 待插入值與有序序列中的target[mid]相等,保證穩定性的處理 */
							left = left + 1;
						}
					}
					/* left及其後面的資料順序向後移動,並在left位置插入 */
					for ( int j = i; j > left; j-- )
					{
						target[j] = target[j - 1];
					}
					target[left] = temp;
				}
			}
		}
		return(target);
	}
}
複製程式碼

三. 選擇排序

選擇排序的基本思想:每一趟 (例如第i趟,i = 0,1,…)在後面第n-i個待排序元素中選出最小元素作為有序序列的第i個元素,直到第n-1趟結束後,所有元素有序。在這裡,我們介紹兩種具體的選擇排序演算法:直接選擇排序與堆排序

1、直接選擇排序

直接選擇排序的思想:

第一次從R[0]~R[n-1]中選取最小值,與R[0]交換,第二次從R1~R[n-1]中選取最小值,與R1交換,….,第i次從R[i-1]~R[n-1]中選取最小值,與R[i-1]交換,…..,第n-1次從R[n-2]~R[n-1]中選取最小值,與R[n-2]交換,總共通過n-1次,得到一個按排序碼從小到大排列的有序序列

直接選擇排序是一種不穩定的排序演算法,其實現如下:

/**
 *
 * Title: 選擇排序中的直接選擇排序,依賴於初始序列
 *
 * Description: 每一趟 (例如第i趟,i = 0,1,...)在後面第n-i個待排序元素中選出最小元素作為有序序列的第i個元素
 *
 * 時間複雜度:最好情形O(n^2),平均情形O(n^2),最差情形O(n^2)
 *
 * 空間複雜度:O(1)
 *
 * 穩 定 性:不穩定
 *
 * 內部排序(在排序過程中資料元素完全在記憶體)
 *
 */
public class StraightSelectSort {
	public static int[] selectSort( int[] target )
	{
		if ( target != null && target.length != 1 )
		{
			for ( int i = 0; i < target.length; i++ )
			{
				int min_index = i;
				for ( int j = i + 1; j < target.length; j++ )
				{
					if ( target[min_index] > target[j] )
					{
						min_index = j;
					}
				}
				if ( target[min_index] != target[i] ) /* 導致不穩定的因素:交換 */
				{
					int min = target[min_index];
					target[min_index] = target[i];
					target[i] = min;
				}
			}
		}
		return(target);
	}
}
複製程式碼

2、堆排序

堆排序的核心是堆調整演算法。首先根據初始輸入資料,利用堆調整演算法shiftDown()形成初始堆;然後,將堆頂元素與堆尾元素交換,縮小堆的範圍並重新調整為堆,如此往復

堆排序是一種不穩定的排序演算法,其實現如下:

/**
 *
 * Title: 堆排序(選擇排序),升序排序(最大堆),依賴於初始序列
 *
 * Description: 現將給定序列調整為最大堆,然後每次將堆頂元素與堆尾元素交換並縮小堆的範圍,直到將堆縮小至1
 *
 * 時間複雜度:O(nlgn)
 *
 * 空間複雜度:O(1)
 *
 * 穩 定 性:不穩定
 *
 * 內部排序(在排序過程中資料元素完全在記憶體)
 *
 */
public class HeapSort {
	public static int[] heapSort( int[] target )
	{
		if ( target != null && target.length > 1 )
		{
			/* 調整為最大堆 */
			int pos = (target.length - 2) / 2;
			while ( pos >= 0 )
			{
				shiftDown( target, pos, target.length - 1 );
				pos--;
			}
			/* 堆排序 */
			for ( int i = target.length - 1; i > 0; i-- )
			{
				int temp = target[i];
				target[i] = target[0];
				target[0] = temp;
				shiftDown( target, 0, i - 1 );
			}
			return(target);
		}
		return(target);
	}
	/**
	 *
	 * @description 自上而下調整為最大堆
	 *
	 * @param target
	 *
	 * @param start
	 *
	 * @param end
	 *
	 */
	private static void shiftDown( int[] target, int start, int end )
	{
		int i = start;
		int j = 2 * start + 1;
		int temp = target[i];
		while ( j <= end ) /* 迭代條件 */
		{
			if ( j < end && target[j + 1] > target[j] ) /* 找出較大子女 */
			{
				j = j + 1;
			}
			if ( target[j] <= temp ) /* 父親大於子女 */
			{
				break;
			} else {
				target[i] = target[j];
				i = j;
				j = 2 * j + 1;
			}
		}
		target[i] = temp;
	}
}
複製程式碼

四. 交換排序

交換排序的基本思想:根據序列中兩個元素的比較結果來對換這兩個記錄在序列中的位置,也就是說,將鍵值較大的記錄向序列的尾部移動,鍵值較小的記錄向序列的前部移動。

1、氣泡排序

氣泡排序的思想:根據序列中兩個元素的比較結果來對換這兩個記錄在序列中的位置,將鍵值較大的記錄向序列的尾部移動,鍵值較小的記錄向序列的前部移動。因此,每一趟都將較小的元素移到前面,較大的元素自然就逐漸沉到最後面了,也就是說,最大的元素最後才能確定,這就是冒泡。氣泡排序是一種穩定的排序演算法,其實現如下:

/**
 *
 * Title: 交換排序中的氣泡排序 ,一般情形下指的是優化後的氣泡排序,最多進行n-1次比較,依賴於初始序列
 *
 * Description:因為越大的元素會經由交換慢慢"浮"到數列的頂端(最後位置),最大的數最後才確定下來,所以稱為氣泡排序
 *
 * 時間複雜度:最好情形O(n),平均情形O(n^2),最差情形O(n^2)
 *
 * 空間複雜度:O(1)
 *
 * 穩 定 性:穩定
 *
 * 內部排序(在排序過程中資料元素完全在記憶體)
 *
 */
public class BubbleSort {
	/**
	 *
	 * @description 樸素氣泡排序(共進行n-1次比較)
	 *
	 * @author rico
	 *
	 */
	public static int[] bubbleSort( int[] target )
	{
		int n = target.length;
		if ( target != null && n != 1 )
		{
			/* 最多需要進行n-1躺,每一趟將比較小的元素移到前面,比較大的元素自然就逐漸沉到最後面了,這就是冒泡 */
			for ( int i = 0; i < n - 1; i++ )
			{
				for ( int j = n - 1; j > i; j-- )
				{
					if ( target[j] < target[j - 1] )
					{
						int temp = target[j];
						target[j] = target[j - 1];
						target[j - 1] = temp;
					}
				}
				System.out.println( Arrays.toString( target ) );
			}
		}
		return(target);
	}
	/**
	 *
	 * @description 優化氣泡排序
	 */
	public static int[] optimizeBubbleSort( int[] target )
	{
		int n = target.length;
		if ( target != null && n != 1 )
		{
			/* 最多需要進行n-1躺,每一趟將比較小的元素移到前面,比較大的元素自然就逐漸沉到最後面了,這就是冒泡 */
			for ( int i = 0; i < n - 1; i++ )
			{
				boolean exchange = false;
				for ( int j = n - 1; j > i; j-- )
				{
					if ( target[j] < target[j - 1] )
					{
						int temp = target[j];
						target[j] = target[j - 1];
						target[j - 1] = temp;
						exchange = true;
					}
				}
				System.out.println( Arrays.toString( target ) );
				if ( !exchange ) /* 若 i 到 n-1 這部分元素已經有序,則直接返回 */
				{
					return(target);
				}
			}
		}
		return(target);
	}
}
複製程式碼

2、快速排序

快速排序的思想:

通過一趟排序將要排序的資料分割成獨立的兩部分,其中一部分的所有資料都比另外一部分的所有資料都要小(劃分過程),然後再按此方法對這兩部分資料分別進行快速排序(快速排序過程),整個排序過程可以遞迴進行,以此達到整個資料變成有序序列。

快速排序是一種不穩定的排序演算法。

/**
 *
 * Title: 交換排序中的快速排序,目前應用最為廣泛的排序演算法,是一個遞迴演算法,依賴於初始序列
 *
 * Description:快速排序包括兩個過程:劃分 和 快排
 *
 * "劃分"是指將原序列按基準元素劃分兩個子序列
 *
 * "快排"是指分別對子序列進行快排
 *
 * 就平均計算時間而言,快速排序是所有內部排序方法中最好的一個
 *
 * 對大規模資料排序時,快排是快的;對小規模資料排序時,快排是慢的,甚至慢於簡單選擇排序等簡單排序方法
 *
 * 快速排序依賴於原始序列,因此其時間複雜度從O(nlgn)到O(n^2)不等
 *
 * 時間複雜度:最好情形O(nlgn),平均情形O(nlgn),最差情形O(n^2)
 *
 * 遞迴所消耗的棧空間
 *
 * 空間複雜度:O(lgn)
 *
 * 可選任一元素作為基準元素
 *
 * 穩 定 性:不穩定
 *
 * 內部排序(在排序過程中資料元素完全在記憶體)
 *
 */
public class QuickSort {
	/**
	 *
	 * @description 快排演算法(遞迴演算法):在遞去過程中就把問題解決了
	 *
	 * @param target
	 *
	 * @param left
	 *
	 * @param right
	 *
	 * @return
	 *
	 */
	public static int[] quickSort( int[] target, int left, int right )
	{
		if ( right > left ) /* 遞迴終止條件 */
		{
			int base_index = partition( target, left, right ); /* 原序列劃分後基準元素的位置 */
			quickSort( target, left, base_index - 1 ); /* 對第一個子序列快速排序,不包含基準元素! */
			quickSort( target, base_index + 1, right ); /* 對第二個子序列快速排序,不包含基準元素! */
			return(target);
		}
		return(target);
	}
	/**
	 *
	 * @description 序列劃分,以第一個元素為基準元素
	 *
	 * @param target 序列
	 *
	 * @param left 序列左端
	 *
	 * @param right 序列右端
	 *
	 * @return
	 *
	 */
	public static int partition( int[] target, int left, int right )
	{
		int base = target[left]; /* 基準元素的值 */
		int base_index = left; /* 基準元素最終應該在的位置 */
		for ( int i = left + 1; i <= right; i++ ) /* 從基準元素的下一個元素開始 */
		{
			if ( target[i] < base ) /* 若其小於基準元素 */
			{
				base_index++; /* 若其小於基準元素,則基準元素最終位置後移;否則不用移動 */
				if ( base_index != i ) /* 相等情況意味著i之前的元素都小於base,只需要換一次即可,不需要次次都換 */
				{
					int temp = target[base_index];
					target[base_index] = target[i];
					target[i] = temp;
				}
			}
		}
		/* 將基準元素就位 */
		target[left] = target[base_index];
		target[base_index] = base;
		System.out.println( Arrays.toString( target ) );
		return(base_index); /* 返回劃分後基準元素的位置 */
	}
}
複製程式碼

五. 歸併排序

歸併排序包含兩個過程:”歸”和”並”。

  • ”歸”是指將原序列分成半子序列,分別對子序列進行遞迴排序;
  • ”並”是指將排好序的各子序列合併成原序列。

歸併排序演算法是一個典型的遞迴演算法,因此也是概念上最為簡單的排序演算法與快速排序演算法相比,歸併排序演算法不依賴於初始序列,並且是一種穩定的排序演算法,但需要與原序列一樣大小的輔助儲存空間。

/**
 *
 * Title: 歸併排序 ,概念上最為簡單的排序演算法,是一個遞迴演算法 Description:歸併排序包括兩個過程:歸 和 並
 *
 * "歸"是指將原序列分成半子序列,分別對子序列進行遞迴排序 "並"是指將排好序的各子序列合併成原序列
 *
 * 歸併排序的主要問題是:需要一個與原待排序陣列一樣大的輔助陣列空間
 *
 * 歸併排序不依賴於原始序列,因此其最好情形、平均情形和最差情形時間複雜度都一樣 時間複雜度:最好情形O(n),平均情形O(n^2),最差情形O(n^2)
 *
 * 空間複雜度:O(n) 穩 定 性:穩定 內部排序(在排序過程中資料元素完全在記憶體)
 *
 */
public class MergeSort {
	/**
	 *
	 * @description 歸併排序演算法(遞迴演算法):遞去分解,歸來合併
	 *
	 * @param target
	 *
	 * 待排序序列
	 *
	 * @param left
	 *
	 * 待排序序列起始位置
	 *
	 * @param right
	 *
	 * 待排序序列終止位置
	 *
	 * @return
	 *
	 */
	public static void mergeSort( int[] target )
	{
		int[] copy = Arrays.copyOf( target, target.length ); /* 空間複雜度O(n) */
		mergeSort( target, copy, 0, target.length - 1 );
	}
	public static void mergeSort( int[] target, int[] copy, int left, int right )
	{
		if ( right > left ) /* 遞迴終止條件 */
		{
			int mid = (left + right) / 2;
			mergeSort( target, copy, left, mid ); /* 歸併排序第一個子序列 */
			mergeSort( target, copy, mid + 1, right ); /* 歸併排序第二個子序列 */
			merge( target, copy, left, mid, right ); /* 合併子序列成原序列 */
		}
	}
	/**
	 *
	 * @description 兩路歸併演算法
	 *
	 * @param target
	 *
	 * 用於儲存歸併結果
	 *
	 * @param left
	 *
	 * 第一個有序表的第一個元素所在位置
	 *
	 * @param mid
	 *
	 * 第一個有序表的最後一個元素所在位置
	 *
	 * @param right
	 *
	 * 第二個有序表的最後一個元素所在位置
	 *
	 * @return
	 *
	 */
	public static void merge( int[] target, int[] copy, int left, int mid,
				 int right )
	{
		/* s1,s2是檢查指標,index 是存放指標 */
		int s1 = left;
		int s2 = mid + 1;
		int index = left;
		/* 兩個表都未檢查完,兩兩比較 */
		while ( s1 <= mid && s2 <= right )
		{
			if ( copy[s1] <= copy[s2] ) /* 穩定性 */
			{
				target[index++] = copy[s1++];
			} else {
				target[index++] = copy[s2++];
			}
		}
		/* 若第一個表未檢查完,複製 */
		while ( s1 <= mid )
		{
			target[index++] = copy[s1++];
		}
		/* 若第二個表未檢查完,複製 */
		while ( s2 <= right )
		{
			target[index++] = copy[s2++];
		}
		/* 更新輔助陣列 copy */
		for ( int i = left; i <= right; i++ )
		{
			copy[i] = target[i];
		}
	}
複製程式碼

六. 分配排序(基數排序)

分配排序的基本思想:用空間換時間。在整個排序過程中,無須比較關鍵字,而是通過用額外的空間來”分配”和”收集”來實現排序,它們的時間複雜度可達到線性階:O(n)。

基數排序包括兩個過程:

首先,將目標序列各元素分配到各個桶中(分配過程); 然後,將各個桶中的元素按先進先出的順序再放回去(收集過程),如此往復,一共需要進行d趟,d為元素的位數。

/**
 *
 * Title: 分配排序中的基數排序,不依賴於初始序列
 *
 * Description: 不是在對元素進行比較的基礎上進行排序,而是採用 "分配 + 收集" 的辦法
 *
 *
 *
 * 首先,將目標序列各元素分配到各個桶中;
 *
 * 其次,將各個桶中的元素按先進先出的順序再放回去
 *
 * 如此往復...
 *
 *
 *
 * 時間複雜度:O(d*(r+n))或者 O(dn),d 的大小一般會受到 n的影響
 *
 * 空間複雜度:O(rd + n)或者 O(n)
 *
 * 穩 定 性:穩定
 *
 * 內部排序(在排序過程中資料元素完全在記憶體)
 *
 */
public class RadixSort {
	/**
	 *
	 * @description 分配 + 收集
	 *
	 * @param target 待排序陣列
	 *
	 * @param r 基數
	 *
	 * @param d 元素的位數
	 *
	 * @param n 待排序元素個數
	 *
	 * @return
	 *
	 */
	public static int[] radixSort( int[] target, int r, int d, int n )
	{
		if ( target != null && target.length != 1 )
		{
			int[][] bucket = new int[r][n]; /* 一共有基數r個桶,每個桶最多放n個元素 */
			int digit; /* 獲取元素對應位上的數字,即裝入那個桶 */
			int divisor = 1; /* 定義每一輪的除數,1, 10, 100, ... */
			int[] count = new int[r]; /* 統計每個桶中實際存放元素的個數 */
			for ( int i = 0; i < d; i++ ) /* d 位的元素,需要經過分配、收集d次即可完成排序 */
			{ /* 分配 */
				for ( int ele : target )
				{
					digit = (ele / divisor) % 10; /* 獲取元素對應位上的數字(巧妙!!!) */
					bucket[digit][count[digit]++] = ele; /* 將元素放入對應桶,桶中元素數目加1 */
				}
				/* 收集 */
				int index = 0; /* 目標陣列的下標 */
				for ( int j = 0; j < r; j++ )
				{
					int k = 0; /* 用於按照先進先出順序獲取桶中元素 */
					while ( k < count[j] )
					{
						target[index++] = bucket[j][k++]; /* 按照先進先出依次取出桶中的元素 */
					}
					count[j] = 0; /* 計數器歸零 */
				}
				divisor *= 10; /* 用於獲取元素對應位數字 */
			}
		}
		return(target);
	}
}
複製程式碼

相關文章