最小斯坦納樹 學習筆記
一、排序演算法介紹
排序也稱排序演算法(Sort Algorithm),排序是將一組資料,依指定的順序進行排列的過程。
排序的分類:
●內部排序:
➢指將需要處理的所有資料都載入到內部儲存器中進行排序。
●外部排序法:
➢資料量過大,無法全部載入到記憶體中,需要藉助外部儲存進行排序。
二、演算法的時間複雜度
度量一個程式執行時間的兩種方法
事後統計的方法
●這種方法可行,但是有兩個問題:一是要想對設計的演算法的執行效能進行評測,需要實際執行該程式;二是所得時間的統計量依賴於計算機的硬體、軟體等環境因素,這種方式,要在同一臺計算機的相同狀態下執行,才能比較那個演算法速度更快。
事前估算的方法
●通過分析某個演算法的時間複雜度
三、演算法的時間複雜度
1.時間頻度
基本介紹
●時間頻度:一個演算法花費的時間與演算法中語句的執行次數成正比例,哪個演算法中語句執行次數多,它花費時間就多。一個演算法中的語句執行次數稱為語句頻度或時間頻度。記為T(n)。
使用說明
➢忽略常數項
➢忽略低次項
2.時間複雜度
●一般情況下,演算法中的基本操作語句的重複執行次數是問題規模n的某個函式,用【T(n)】表示,若有某個輔助函式【f(n)】,使得當n趨近於無窮大時,【T(n) / f(n)】的極限值為不等於零的常數,則稱【f(n)】是【T(n)】的同數量級函式。記作【T(n)=O(f(n)】,稱【O(f(n))】為演算法的漸進時間複雜度,簡稱時間複雜度。
●【T(n)】不同,但時間複雜度可能相同。如: 【T(n)=n2+7n+6】 與 【T(n)=3n2+2n+2】它們的【T(n)】不同,但時間複雜度相同,都為【O(n2)】。
●計算時間複雜度的方法:
➢用常數1代替執行時間中的所有加法常數
➢修改後的執行次數函式中,只保留最高階項
➢去除最高階項的係數
3.常見的時間複雜度
➢常數階O(1)
➢對數階O(log2 n)
➢線性階O(n)
➢線性對數階O(nlog2n)
➢平方階O(n^2)
➢立方階O(n^3)
➢k次方階O(n^k)
➢指數階O(2^n)
➢階乘階O(n!)
4.平均時間複雜度和最壞時間複雜度
➢平均時間複雜度是指所有可能的輸入例項均以等概率出現的情況下,該演算法的執行時間。
➢最壞情況下的時間複雜度稱最壞時間複雜度。一般討論的時間複雜度均是最壞情況下的時間複雜度。這樣做的原因是:最壞情況下的時間複雜度是演算法在任何輸入例項上執行時間的界限,這就保證了演算法的執行時間不會比最壞情況更長。
➢平均時間複雜度和最壞時間複雜度是否一致,和演算法有關(如圖:)。
相關術語解釋:
➢穩定:如果a原本在b前面,而a=b,排序之後a仍然在b的前面;
➢不穩定: 如果a原本在b的前面,而a=b,排序之後a可能會出現在b的後面;
➢內排序: 所有排序操作都在記憶體中完成;
➢外排序:由於資料太大,因此把資料放在磁碟中,而排序通過磁碟和記憶體的資料傳輸才能進行;
➢時間複雜度: 一個演算法執行所耗費的時間。
➢空間複雜度:執行完一個程式所需記憶體的大小。
➢n:資料規模
➢k:"桶”的個數
➢In-place: 不佔用額外記憶體
➢Out-place: 佔用額外記憶體
四、演算法的空間複雜度
●類似於時間複雜度的討論,一個演算法的空間複雜度(Space Complexity)定義為該演算法所耗費的儲存空間,它也是問題規模n的函式。
●空間複雜度(Space Complexity)是對一個演算法在執行過程中臨時佔用儲存空間大小的量度。有的演算法需要佔用的臨時工作單元數與解決問題的規模n有關,它隨著n的增大而增大,當n較大時,將佔用較多的儲存單元,例如快速排序和歸併排序演算法就屬於這種情況
●在做演算法分析時,主要討論的是時間複雜度。從使用者使用體驗上看,更看重的程式執行的速度。一些快取產品(redis, memcache)和演算法(基數排序)本質就是用空間換時間.
氣泡排序
氣泡排序介紹
●氣泡排序(Bubble Sorting) 的基本思想是:通過對待排序序列從前向後(從下標較小的元素開始),依次比較相鄰元素的值,若發現逆序則交換,使值較大的元素逐漸從前移向後部,就象水底下的氣泡一樣逐漸向上冒。
●因為排序的過程中,各元素不斷接近自己的位置,如果一趟比較下來沒有進行過交換,就說明序列有序,因此要在排序過程中設定一個標誌flag判斷元素是否進行過交換。從而減少不必要的比較。
氣泡排序及優化
package com.xudong.DataStructures;
import java.text.SimpleDateFormat;
import java.util.Date;
public class BubbleSortDemo {
public static void main(String[] args) {
//int arr[] = {3,9,-1,10,-2};
//建立80000個隨機數的陣列
int[] arr = new int[80000];
for (int i = 0; i < 80000; i++) {
arr[i] = (int) (Math.random() * 8888888);//生成一個[0,8888888)的隨機數
}
//測試時間
Date date1 = new Date();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String date1Str = simpleDateFormat.format(date1);
System.out.println("排序前的時間是:" + date1Str);
Bubblesort(arr);
Date date2 = new Date();
String date2Str = simpleDateFormat.format(date2);
System.out.println("排序後的時間是:" + date2Str);
//System.out.println(Arrays.toString(arr));
}
public static void Bubblesort(int[] arr){
//第一趟排序,就是將最大的數排在最後
int temp = 0;
boolean flag = false;//標識變數,表示是否進行過交換
for (int i = 0; i < arr.length - 1; i++) {
for (int j = 0; j < arr.length - 1; j++) {
//如果前面的數比後面的數大,則交換
if (arr[j] > arr[j + 1]){
flag = true;
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
// System.out.println("第" + (i + 1) +"趟排序後的陣列:");
// System.out.println(Arrays.toString(arr));
//演算法優化
if (!flag){//在一趟排序中,一次交換都沒有發生過
break;
}else {
flag = false;//重置flag,進行下次判斷
}
}
}
}
選擇排序
1.基本介紹
●選擇式排序也屬於內部排序法,是從欲排序的資料中,按指定的規財選出某一元素,再依規定交換位置後達到排序的目的。
選擇排序要比氣泡排序快。
2.選擇排序思想:
●選擇排序(select sorting) 也是一種簡單的排序方法。它的基本思想是:第一次從arr[0] ~ arr[n-1]中選取最小值,與arr[0]交換, 第二次從arr[1] ~ arr[n-1]中選取最小值,與arr[1]交換, 第三次從arr[2] ~ arr[n-1]中選取最小值,與arr[2]交換,...,第i次從arr[i-1] ~ arr[n-1]中選取最小值,與arr[i-1]交換,... ,第n-1次從arr[n-2] ~ arr[n-1]中選取最小值,與arr[n-2]交換,總共通過n-1次,得到一個按排序碼從小到大排列的有序序列。
package com.xudong.DataStructures;
import java.util.Arrays;
public class SelectSortDemo {
public static void main(String[] args) {
int[] arr = {101,34,119,1};
selectSort(arr);
}
public static void selectSort(int[] arr){
for (int i = 0; i < arr.length - 1; i++) {
int minIndex = i;
int min = arr[i];
for (int j = i + 1; j < arr.length; j++) {
if (min > arr[j]){//假定的最小值並不是最小。升序降序改這裡符號
min = arr[j];//重置min
minIndex = j;//重置minIndex
}
}
//將最小值放在arr[i],即交換
if (minIndex != i){//優化
arr[minIndex] = arr[i];
arr[i] = min;
}
System.out.println("第" + (i + 1) + "輪後:");
System.out.println(Arrays.toString(arr));
}
}
}
插入排序
●插入式排序屬於內部排序法,是對於欲排序的元素以插入的方式找尋該元素的適當位置,以達到排序的目的。
插入排序效率稍比選擇排序低
插入排序法思想:
●插入排序(Insertion Sorting)的基本思想是:把n個待排序的元素看成為一個有序表和一個無序表,開始時有序表中只包含一個元素,無序表中包含有n-1個元素,排序過程中每次從無序表中取出第一個元素,把它的排序碼依次與有序表元素的排序碼進行比較,將它插入到有序表中的適當位置,使之成為新的有序表。
package com.xudong.DataStructures;
import java.util.Arrays;
public class InsertSortDemo {
public static void main(String[] args) {
int[] arr = {101,34,119,1,-1,89};
intsertSort(arr);
}
public static void intsertSort(int[] arr){
for (int i = 1; i < arr.length; i++) {
//定義待插入的數
int insertVal = arr[i];
int insertIndex = i - 1;//即arr[i]前面這個數的下標
while (insertIndex >= 0 && insertVal < arr[insertIndex]){//升序降序改這裡符號
arr[insertIndex + 1] = arr[insertIndex];
insertIndex--;
}
if (insertIndex + 1 != i){
//當退出while迴圈時,說明插入的位置找到,insertIndex + 1
arr[insertIndex + 1] = insertVal;
}
System.out.println("第" + i + "輪插入:");
System.out.println(Arrays.toString(arr));
}
}
}
希爾排序
●希爾排序是希爾(Donald Shell)於1959年提出的一種排序演算法。希爾排序也是一種插入排序,它是簡單插入排序經過改進之後的一個更高效的版本,也稱為縮小增量排序。
希爾排序法基本思想
●希爾排序是把記錄按下標的一定增量分組,對每組使用直接插入排序演算法排序;隨著增量逐漸減少,每組包含的關鍵詞越來越多,當增量減至1時,整個檔案恰被分成一組,演算法便終止
package com.xudong.DataStructures;
import java.util.Arrays;
public class ShellSortDemo {
public static void main(String[] args) {
int[] arr = {8,9,1,7,2,3,5,4,6,0};
shellSort2(arr);
}
//插入時採用交換法,效率慢,與冒泡差不多
public static void shellSort(int[] arr){
int temp;
int count = 0;
for (int gap = arr.length / 2; gap > 0; gap /= 2) {
for (int i = 0; i < arr.length; i++) {
//遍歷各組中所有的元素。共gap組,步長gap
for (int j = i - gap; j >= 0 ; j -= gap) {
//如果當前元素大於加上步長後的那個元素,說明交換
if (arr[j] > arr[j + gap]){
temp = arr[j];
arr[j] = arr[j + gap];
arr[j + gap] = temp;
}
}
}
System.out.println("希爾排序第" + (++count) + "輪=" + Arrays.toString(arr));
}
}
//插入時採用移位法:效率比插入排序高
public static void shellSort2(int[] arr){
int temp;
int count = 0;
//增量gap,並逐步縮小增量
for (int gap = arr.length / 2; gap > 0; gap /= 2){
//從第gap個元素,逐個對其所在的組進行直接插入排序
for (int i = gap; i < arr.length; i++) {
int j = i;
temp = arr[j];
if (arr[j] < arr[j - gap]){
while (j - gap >= 0 && temp < arr[j - gap]){
//移動
arr[j] = arr[j - gap];
j -= gap;
}
//當退出while後,temp就找到了插入的位置
arr[j] = temp;
}
}
System.out.println("希爾排序第" + (++count) + "輪=" + Arrays.toString(arr));
}
}
}
快速排序
●快速排序(Quicksort)是對氣泡排序的一種改進。
快速排序稍比希爾排序快。
快速排序法基本思想
基本思想是:通過一趟排序將要排序的資料分割成獨立的兩部分,其中一部分的所有資料都比另外一部分的所有資料都要小,然後再按此方法對這兩部分資料分別進行快速排序,整個排序過程可以遞迴進行,以此達到整個資料變成有序序列.
package com.xudong.DataStructures;
import java.util.Arrays;
public class QuickSortDemo {
public static void main(String[] args) {
int[] arr = {-9,78,0,23,-567,70};
quickSort(arr,0,arr.length - 1);
System.out.println("arr = " + Arrays.toString(arr));
}
public static void quickSort(int[] arr,int left,int right){
int l = left;//左下標
int r = right;//右下標
int pivot = arr[(left + right) / 2];//pivot中軸值
int temp;
//讓比pivot值小的放左邊,比pivot值大的放右邊
while (l < r){
//在pivot的左邊一直找,找到大於等於pivot值,才退出
while (arr[l] < pivot){
l += 1;
}
//在pivot的右邊一直找,找到小於等於pivot值,才退出
while (arr[r] > pivot){
r -= 1;
}
if (l >= r){//左邊全小於等於pivot,右邊全大於pivot
break;
}
//交換
temp = arr[l];
arr[l] = arr[r];
arr[r] = temp;
//如果交換完後,arr[l] == pivot值,r--,後移
if (arr[l] == pivot){
r -= 1;
}
//如果交換完後,arr[r] == pivot值,l++,後移
if (arr[r] == pivot){
l += 1;
}
}
//如果 l == r ,則 l++,r--,否則會棧溢位
if (l == r){
l += 1;
r -= 1;
}
//向左遞迴
if (left < r){
quickSort(arr,left,r);
}
//向右遞迴
if (right > l){
quickSort(arr,l,right);
}
}
}
歸併排序
與快速排序的效率差不多。
●歸併排序(MERGE-SORT)是利用歸併的思想實現的排序方法,該演算法採用經典的分治(divide-and-conquer)策略(分治法將問題分(divide)成一些小的問題然後遞迴求解,而治(conquer)的階段則將分的階段得到的各答案”修補”在一起,即分而治之)。
說明:
●可以看到這種結構很像一棵完全二叉樹,本文的歸併排序我們採用遞迴去實現(也可採用迭代的方式去實現)。分階段可以理解為就是遞迴拆分子序列的過程。
package com.xudong.DataStructures;
import java.util.Arrays;
public class MergeSortDemo {
public static void main(String[] args) {
int arr[] = {8,4,5,7,1,3,6,2};
int temp[] = new int[arr.length];
mergeSort(arr,0,arr.length -1,temp);//歸併arr.length-1次
System.out.println("歸併排序後:" + Arrays.toString(arr));
}
//分+合算法
public static void mergeSort(int[] arr,int left,int right,int[] temp){
if (left < right){
int mid = (left + right) / 2;
//向左遞迴分解
mergeSort(arr,left,mid,temp);
//向右遞迴分解
mergeSort(arr,mid + 1,right,temp);
//合併
merge(arr,left,mid,right,temp);
}
}
//合併的方法
/**
* @param arr 排序的原始陣列
* @param left 左邊有序序列的初始索引
* @param mid 中間索引
* @param right 右邊索引
* @param temp 做中轉的陣列
*/
public static void merge(int[] arr,int left,int mid,int right,int[] temp){
int i = left;
int j = mid + 1;
int t = 0;
//(一)先把左右兩邊(有序)的資料按照規則填充到temp陣列
//直到左右兩邊的有序序列有一邊處理完為止
while (i <= mid && j <= right){
//如果左邊的有序序列的當前元素,小於等於右邊有序序列的當前元素
//那麼將左邊的當前元素拷貝到temp陣列
if (arr[i] <= arr[j]){
temp[t] = arr[i];
t += 1;
i += 1;
}else {//反之,將右邊有序序列的當前元素填充到temp陣列
temp[t] = arr[j];
t += 1;
j += 1;
}
}
//(二)把剩餘一邊的資料依次全部填充到temp
while (i <= mid){
temp[t] = arr[i];
t += 1;
i += 1;
}
while (j <= right){
temp[t] = arr[j];
t += 1;
j += 1;
}
//(三)將temp陣列的元素拷貝到arr。並不是拷貝所有
t = 0;
int tempLeft = left;
while (tempLeft <= right){
arr[tempLeft] = temp[t];
t += 1;
tempLeft += 1;
}
}
}
基數排序
基數排序(桶排序)介紹:
●基數排序(radixsort) 屬於“分配式排序”(distributionsort),又稱“桶子法”(bucket sort)或binsort,顧名思義,它是通過鍵值的各個位的值,將要排序的元素分配至某些“桶”中,達到排序的作用
●基數排序法是屬於穩定性的排序,基數排序法的是效率高的穩定性排序法
●基數排序(Radix Sort)是桶排序的擴充套件
●基數排序是1887年赫爾曼何樂禮發明的。它是這樣實現的:將整數按位數切割成不同的數字,然後按每個位數分別比較。
基數排序的說明:
●基數排序是對傳統桶排序的擴充套件,速度很快.
●基數排序是經典的空間換時間的方式,佔用記憶體很大,當對海量資料排序時,容易造成OutOfMemoryError 。
●基數排序時穩定的。[注:假定在待排序的記錄序列中,存在多個具有相同的關鍵字的記錄,若經過排序,這些記錄的相對次序保持不變,即在原序列中,r[i]=r[j], 且r[i]在r[j]之前, 而在排序後的序列中,r[i]仍在r[j]之前,則稱這種排序演算法是穩定的;否則稱為不穩定的]
●有負數的陣列,我們不適用基數排序來進行排序。
基數排序基本思想
●將所有待比較數值統一為同樣的數位長度,數位較短的數前面補零。然後,從最低位開始,依次進行一次排序。這樣從最低位排序一直到最高位排序完成以後,數列就變成一個有序序列。
package com.xudong;
import java.util.Arrays;
public class RadixSortDemo {
public static void main(String[] args) {
int arr[] = {53,3,542,748,14,214};
radixSort(arr);
}
//基數排序
public static void radixSort(int[] arr){
//得到陣列中最大的數的位數
int max = arr[0];
for (int i = 1; i < arr.length; i++) {
if (arr[i] > max){
max = arr[i];
}
}
//得到最大位數是幾位數
int maxLength = (max + "").length();
//定義一個二維陣列,表示十個桶,每個桶就是一個數組
int[][] bucket = new int[10][arr.length];
//為了記錄每個桶中,實際存放了多少個數據,這裡定義一個一維陣列記錄各個桶每次放入資料的個數
int[] bucketElementCounts = new int[10];
for (int i = 0 , n = 1; i < maxLength; i++,n *= 10) {
//針對每個元素的對應的位數進行排序處理。個十百千
for (int j = 0; j < arr.length; j++) {
//取出每個元素的對應位的值
int digitOfElement = arr[j] / n % 10;
//放到對應的桶中
bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j];
bucketElementCounts[digitOfElement]++;
}
//按照桶的順序(一維陣列的下標依次取出資料,放入原來陣列)
int index = 0;
//遍歷每一個桶,並將桶中的資料放入到原陣列
for (int k = 0; k < bucketElementCounts.length; k++) {
if (bucketElementCounts[k] != 0){//如果桶中有資料
//迴圈該桶即,即第k個桶,放入資料
for (int l = 0; l < bucketElementCounts[k]; l++) {
arr[index++] = bucket[k][l];
}
}
//每輪處理後,須將桶清空
bucketElementCounts[k] = 0;
}
System.out.println("第" + (i + 1) + "輪,排序處理的結果:" + Arrays.toString(arr));
}
}
}