各種排序演算法
目錄
二分查詢
//不使用遞迴實現:while迴圈,時間O(log2 N),空間O(1) public static int commonBinarySearch(int[] arr,int key){ int low = 0; int high = arr.length - 1; int middle = 0; //定義middle if(key < arr[low] || key > arr[high] || low > high){ return -1; } while(low <= high){ middle = (low + high) / 2; if(arr[middle] > key){ //比關鍵字大則關鍵字在左區域 high = middle - 1; }else if(arr[middle] < key){ //比關鍵字小則關鍵字在右區域 low = middle + 1; }else{ return middle; } } return -1; //最後仍然沒有找到,則返回-1 } //使用遞迴實現,時間O(log2 N),空間O(log2N ) public static int recursionBinarySearch(int[] arr,int key,int low,int high){ if(key < arr[low] || key > arr[high] || low > high){ return -1; } int middle = (low + high) / 2; //初始中間位置 if(arr[middle] > key){ //比關鍵字大則關鍵字在左區域 return recursionBinarySearch(arr, key, low, middle - 1); }else if(arr[middle] < key){ //比關鍵字小則關鍵字在右區域 return recursionBinarySearch(arr, key, middle + 1, high); }else { return middle; } }
二分查詢優化:
1、插值查詢演算法 將mid=left + (right-left)/2 的計算更改為 mid = left + ((target-min)/(max-target))*(right-left),即更換1/2係數
2、斐波那契查詢演算法
- 根據待查詢陣列長度確定裴波那契陣列的長度(或最大元素值)
- 根據1中長度建立該長度的裴波那契陣列,再通過F(0)=1,F(1)=1, F(n)=F(n-1)+F(n-2)生成裴波那契數列為陣列賦值
- 以2中的裴波那契陣列的最大值為長度建立填充陣列,將原待排序陣列元素拷貝到填充陣列中來, 如果有剩餘的未賦值元素, 用原待排序陣列的最後一個元素值填充
- 針對填充陣列進行關鍵字查詢, 查詢成功後記得判斷該元素是否來源於後來填充的那部分元素
氣泡排序
public static void bubbleSort(int[] data){ if(data == null) return; for (int i = 0; i < data.length; i++) { for (int j = 1; j < data.length-i; j++) { if (data[j-1]>data[j]) { int temp = data[j]; data[j]=data[j-1]; data[j-1]=temp; } } } return; }
優化版:解釋連結:https://mp.weixin.qq.com/s/wO11PDZSM5pQ0DfbQjKRQA
public static void bubbleSortUpdate(int[] data){
if(data == null) return;
//記錄最後一次交換的位置
int lastExchangeIndex =0; //解決原陣列中後部分都為有序情況下的比較浪費
//無序數列的邊界,每次比較只需要比到這裡為止
int sortBorder = data.length;
for (int i = 0; i < data.length; i++) {
//有序標記,每一輪的初始是true,當有一輪比較沒有找到需要更換位置的資料時,可以直接退出整個迴圈了
boolean isSorted = true;
for (int j = 1; j < sortBorder; j++) {
if (data[j-1]>data[j]) {
int temp = data[j];
data[j]=data[j-1];
data[j-1]=temp;
//有元素交換,所以不是有序,標記變為false
isSorted = false;
//把無序數列的邊界更新為最後一次交換元素的位置
lastExchangeIndex = j;
}
}
sortBorder = lastExchangeIndex;
if (isSorted)
break;
}
return;
}
選擇排序
public static void selectionSort(int[] data){
if(data == null) return;
int curMinIndex = 0;
for (int i = 0; i < data.length; i++) {
curMinIndex=i;
for (int j = i; j < data.length; j++) {
if (data[curMinIndex]>data[j]) {
curMinIndex = j;
}
}
int temp = data[i];
data[i]=data[curMinIndex];
data[curMinIndex]=temp;
}
}
插入排序
public static void insertionSort(int[] data){
if(data == null) return;
int now = 0;
int index = 0;
for (int i = 1; i < data.length; i++) {
index = i;
now = data[i];
while (index>0&&data[index-1]>now) {
data[index]=data[index-1];
index--;
}
data[index] = now;
}
}
希爾排序
public static void shellSort(int[] data){
if(data==null || data.length<=1)
return;
//陣列長12 d=6 d=3
for(int gap=data.length/2; gap>0; gap=gap/2){
//i=6 7 / 3 4 5
for(int i=gap;i<data.length;i++){
int cur = i;
int temp = data[i];
//這個步驟類似於直接插入排序
while (cur-gap>=0 && data[cur-gap]>temp) {
data[cur] = data[cur-gap];
cur = cur-gap;
}
data[cur]=temp;
}
}
}
歸併排序
public static void Merge(int[] data){
if (data==null) return;
//在排序前,先建好一個長度等於原陣列長度的臨時陣列,避免遞迴中頻繁開闢空間
int[] temp = new int[data.length];
sort(data,0,data.length-1,temp);
}
private static void sort(int[] data, int start, int end, int[] temp) {
if(start<end){
int mid = start + (end - start)/2;
sort(data,start,mid,temp);//左邊歸併排序,使得左子序列有序
sort(data,mid+1,end,temp);//右邊歸併排序,使得右子序列有序
merge(data,start,mid,end,temp);//將兩個有序子數組合並操作
}
}
private static void merge(int[] data, int start, int mid, int end, int[] temp) {
int left = start;//左序列指標
int right = mid+1;//右序列指標
int tempIndex = 0;//臨時陣列指標
while (left<=mid && right<=end){
if(data[left]<=data[right]){
temp[tempIndex++] = data[left++];
}else {
temp[tempIndex++] = data[right++];
}
}
while(left<=mid){//將左邊剩餘元素填充進temp中
temp[tempIndex++] = data[left++];
}
while(right<=end){//將右序列剩餘元素填充進temp中
temp[tempIndex++] = data[right++];
}
tempIndex = 0;
//將temp中的元素全部拷貝到原陣列中
while(start <= end){
data[start++] = temp[tempIndex++];
}
}
快速排序
public static void quickSort(int[] data,int start,int end) {
if (data==null) return;
if (start>=end) return;
//獲得start元素在原陣列中排序後的準確的位置索引
int index = partition3(data,start,end);
quickSort(data,start,index-1);
quickSort(data,index+1,end);
}
//作用:根據輸入data【】,start與end,返回data[start]在排序陣列中準確的位置
private static int partition(int[] data, int start, int end) {
if(start>=end)
return end;
//儲存目標值
int target=data[start];
//start是前面的哨兵,end是後面的哨兵
while(end>start){
//右哨兵從當前位置迴圈找到一個小於目標值的index
while (end>start&&data[end]>target)
end--;
//執行與左哨兵更換,並讓左哨兵走一步
if (end>start)
data[start++] = data[end];
//左哨兵迴圈找到一個大於目標值的index
while(end>start&&data[start]<target)
start++;
//左哨兵與右哨兵交換,並讓右哨兵向左走一步
if (end>start)
data[end--] = data[start];
}
//當執行到這裡,start=end
data[start]=target;
//System.out.println(start);
return start;
}
堆排序
private static void heapSort(int[] data){
if (data==null) return;
//1.構建初始大頂堆
//data.length/2-1定位到倒數第一個非葉子結點
for (int i = data.length/2-1; i >= 0; i--) {
adjustHeap(data,i,data.length);
}
//2.交換堆頂元素和末尾元素並重建堆
for (int j = data.length-1; j >0; j--) {
swapUtil.swap(data, 0, j);
adjustHeap(data,0,j);
}
}
//調整堆為最大堆,第二個引數i為需要考慮調整的節點,此處需要傳入第三個引數長度,因為最後搭建排序陣列的時候參加運算的陣列長度會減小
private static void adjustHeap(int[] data, int i, int length) {
int temp = data[i];
for (int j = 2*i+1; j < length; j=2*j+1) {
//若當前節點的右子節點的值大於左子節點的值,則定位到右子節點
if (j+1 < length && data[j+1]>data[j]) {//若為最小堆,則第二個>換為<號
j++;
}
//若當前考慮的節點(子節點)大於其父節點,則將其賦值給父節點,不用進行交換,到退出迴圈時再交換
if (data[j]>temp) {//若為最小堆,則這裡換為<號
data[i] = data[j];
i = j;
}else {
break;
}
}
data[i]=temp;
}
計數排序
適用於資料比較集中的情況
時間複雜度o(n+k=遍歷n查詢最大最小值,建立計數陣列(max-min=k),再遍歷n計數每個值出現的次數,再遍歷新陣列k進行排序)
空間複雜度o(k=max-min,建立長度為k的陣列用於計數值出現的次數)
private static void CountSort(int[] data){
if (data==null) return;
int min = data[0];
int max = data[0];
for (int i = 0; i < data.length; i++) {
if (data[i]>max) {
max = data[i];
}else if (data[i]<min) {
min = data[i];
}
}
//以上步驟只是為了找出最大最小值以便於建立臨時陣列,非計數排序必須,這裡只是因為輸入的陣列沒有規定陣列大小範圍
int[] bucket = new int[max-min+1];
for (int i = 0; i < data.length; i++) {
bucket[data[i]-min]++;
}
for (int i = 0; i < bucket.length; i++) {
if (bucket[i]!=0) {
for (int j = 0; j < bucket[i]; j++) {
System.out.print(i+min);
System.out.print(" ");
}
}
}
}
桶排序
適用於最大最小值相差較大的情況,但是值的分佈要夠均勻
時間複雜度o(n+k=遍歷原陣列n找到最大最小值,建立桶陣列,遍歷原陣列n將資料放入桶,排序每個桶內元素後遍歷桶取出資料k)
空間複雜度o(n+k=需要一個長度為n的陣列作為桶,每個桶裡面儲存一個數組List,陣列的每個位置區間大小為k,k=(max-min)/n+1(經過驗證,這個k最好要加1,使得程式魯棒性得以提升,即區間算出來後要加1))
public static void bucketSort(int[] arr){
//新建一個大小為原陣列長度的陣列
ArrayList<ArrayList<Integer>> bucketArr = new ArrayList<>(arr.length);
int max = Integer.MIN_VALUE;
int min = Integer.MAX_VALUE;
for(int i = 0; i < arr.length; i++){
max = Math.max(max, arr[i]);
min = Math.min(min, arr[i]);
bucketArr.add(new ArrayList());
}
//每個桶的區間大小
int bucketNum = (max - min) / arr.length+1;
//將每個元素放入桶
for(int i = 0; i < arr.length; i++){
int num = (arr[i] - min) / bucketNum ;
bucketArr.get(num).add(arr[i]);
}
//對每個桶進行排序
for(int i = 0; i < bucketArr.size(); i++){
Collections.sort(bucketArr.get(i));
}
System.out.println(bucketArr.toString());
}
基數排序
時間複雜度o(n*k=遍歷原陣列n得到陣列的最大值的位數k,再遍歷k遍原陣列n)
public static void radixSort(int[] a) {
int exp; // 指數。當對陣列按個位進行排序時,exp=1;按十位進行排序時,exp=10;...
int max = getMax(a); // 陣列a中的最大值
// 從個位開始,對陣列a按"指數"進行排序
for (exp = 1; max/exp > 0; exp *= 10)
countSort2(a, exp);
}
private static void countSort2(int[] a, int exp) {
int[] output = new int[a.length]; // 儲存"被排序資料"的臨時陣列
LinkedList[] buckets = new LinkedList[10];
for (int i = 0; i < buckets.length; i++) {
buckets[i]=new LinkedList();
}
// 將資料儲存在buckets[]中
for (int i = 0; i < a.length; i++){
//int temp = (a[i]/exp)%10;
buckets[(a[i]/exp)%10].offer(a[i]);
}
int temp = 0;
// 將資料儲存到臨時陣列output[]中
for (int j = 0; j < 10; j++) {
while (buckets[j].peek()!=null) {
output[temp++]=(int) buckets[j].poll();
}
}
// 將排序好的資料賦值給a[]
for (int i = 0; i < a.length; i++)
a[i] = output[i];
output = null;
buckets = null;
}
有時,待排序的檔案很大,計算機記憶體不能容納整個檔案,這時候對檔案就不能使用內部排序了(這裡做一下說明,其實所有的排序都是在記憶體中做的,這裡說的內部排序是指待排序的內容在記憶體中就可以完成,而外部排序是指待排序的內容不能在記憶體中一下子完成,它需要做內外存的內容交換),外部排序常採用的排序方法也是歸併排序,這種歸併方法由兩個不同的階段組成:
1、採用適當的內部排序方法對輸入檔案的每個片段進行排序,將排好序的片段(成為歸併段)寫到外部儲存器中(通常由一個可用的磁碟作為臨時緩衝區),這樣臨時緩衝區中的每個歸併段的內容是有序的。
2、利用歸併演算法,歸併第一階段生成的歸併段,直到只剩下一個歸併段為止。
例如要對外存中4500個記錄進行歸併,而記憶體大小隻能容納750個記錄,在第一階段,我們可以每次讀取750個記錄進行排序,這樣可以分六次讀取,進行排序,可以得到六個有序的歸併段,如下圖:
每個歸併段的大小是750個記錄,記住,這些歸併段已經全部寫到臨時緩衝區(由一個可用的磁碟充當)內了,這是第一步的排序結果。
完成第二步該怎麼做呢?這時候歸併演算法就有用處了,演算法描述如下:
1、將記憶體空間劃分為三份,每份大小250個記錄,其中兩個用作輸入緩衝區,另外一個用作輸出緩衝區。首先對Segment_1和Segment_2進行歸併,先從每個歸併段中讀取250個記錄到輸入緩衝區,對其歸併,歸併結果放到輸出緩衝區,當輸出緩衝區滿後,將其寫到臨時緩衝區內,如果某個輸入緩衝區空了,則從相應的歸併段中再讀取250個記錄進行繼續歸併,反覆以上步驟,直至Segment_1和Segment_2全都排好序,形成一個大小為1500的記錄,然後對Segment_3和Segment_4、Segment_5和Segment_6進行同樣的操作。
2、對歸併好的大小為1500的記錄進行如同步驟1一樣的操作,進行繼續排序,直至最後形成大小為4500的歸併段,至此,排序結束。
以上對外部排序如何使用歸併演算法進行排序進行了簡要總結,提高外部排序需要考慮以下問題:
1、如何減少排序所需的歸併趟數。
2、如果高效利用程式緩衝區,使得輸入、輸出和CPU執行儘可能地重疊。
3、如何生成初始歸併段(Segment)和如何對歸併段進行歸併。
此演算法適用於用小記憶體排序大資料量的問題
假設要對1000G資料用2G記憶體進行排序
方法:每次把2G資料從檔案傳入記憶體,用一個“記憶體排序”演算法排好序後,再寫入外部磁碟的一個2G的檔案中,之後再從1000G中載入第二個2G資料。迴圈500遍。就得到500個檔案,每個檔案2G,檔案內部都是有序的。
然後進行歸併排序,比較第1/500和2/500的檔案,分別讀入750MB進入記憶體,記憶體剩下的500MB用來臨時儲存生成的資料,直到將兩個2G檔案合併成4G,再進行後面兩個2G檔案的歸併……。另外,也可以用歸併排序的思想同時對500個2G的檔案直接進行歸併
優化思路:
- 增設一個緩衝buffer,加速從檔案到記憶體的轉儲
假設這個buffer已經由系統幫我們優化了
- 使用流水線的工作方式,假設從磁碟讀資料到記憶體為L,記憶體排序為S,排完寫磁碟為T,因為L和S都是IO操作,比較耗時間,所以可以用流水線,在IO操作的同時記憶體也在進行排序
- 以上流水線可能會出現記憶體溢位的問題,所以需要把記憶體分為3部分。即每個流水線持有2G/3的記憶體。
- 在歸併排序上進行優化,最後得到的500個2G檔案,每次掃描檔案頭找最小值,最差情況要比較500次,平均時間複雜度是O(n),n為最後得到的有序陣列的個數,優化思路是:維護一個大小為n的“最小堆”,每次返回堆頂元素(當前檔案頭數值最小的那個值),判斷彈出的最小值是屬於哪個檔案的,將哪個檔案此時的標頭檔案所指向的數再插入最小堆中,檔案指標自動後移,插入過程為logn,最小堆返回最小值為o(1),執行時空間複雜度為o(n)
將原問題拆解成若干子問題,同時儲存子問題的答案,使得每個子問題只求解一次,最終獲得原問題的答案
大多數動態規劃問題本質都是遞迴問題——重疊子問題——記憶化搜尋(自頂向下)
——動態規劃(自底向上)
- 求一個問題的最優解
- 整體問題的最優解依賴各個子問題的最優解
- 子問題之間有相互重疊的更小的子問題
- 從上往下分析問題,從下往上求解問題
三個重要概念:最優子結構,邊界,狀態轉移公式
遞迴:記憶化搜尋——自上而下的解決問題
動態規劃——自下而上的解決問題
0-1揹包問題示例:
public int SingleArray() {
int[] weight = {3,5,2,6,4}; //物品重量
int[] val = {4,4,3,5,3}; //物品價值
int length = weight.length;
int w = 12;
//如果是不需要裝滿,則初始化0,要裝滿則初始化Integer.MIN_VALUE
int[] dp = new int[w+1];//+1的目的使得i位置代表體積為i
for (int i = 0; i < length; i++) {
//for(int j=weight[i];j<dp.length;j++)完全揹包問題(無限使用)使用此迴圈
for (int j = dp.length-1; j >= weight[i] ; j--) {
dp[j] = Math.max(dp[j],dp[j-weight[i]]+val[i]);
}
}
return dp[w];
}
0-1揹包問題更詳細的參考連結:https://blog.csdn.net/ls5718/article/details/52227908