目錄
二分查找
//不使用遞歸實現: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中的裴波那契數組的最大值爲長度創建填充數組,將原待排序數組元素拷貝到填充數組中來, 如果有剩餘的未賦值元素, 用原待排序數組的最後一個元素值填充
- 針對填充數組進行關鍵字查找, 查找成功後記得判斷該元素是否來源於後來填充的那部分元素
參考鏈接:https://www.cnblogs.com/penghuwan/p/8021809.html#_label3
冒泡排序
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;
}
計數排序
https://mp.weixin.qq.com/s/WGqndkwLlzyVOHOdGK7X4Q(建議閱讀鏈接!優化穩定版計數排序不在下文中)
適用於數據比較集中的情況,有一定範圍,且範圍不是很大(此種情況性能比ologn快)
如:20個隨機整數,【0,10】,用最快的速度排序
先前的排序算法都是基於元素比較,而計數排序是利用數組下標來確定元素的正確位置
思路:根據整數的最大值max與最小值min之差dis,建立長度爲dis的數組,然後遍歷數組將每個數放在【數組-min】對應的位置
時間複雜度o(n+k=遍歷n查找最大最小值,創建計數數組(max-min=k),再遍歷n計數每個值出現的次數,再遍歷新數組k進行排序)
空間複雜度o(k=max-min,創建長度爲k的數組用於計數值出現的次數)
注意,以下情況不適用:
1.當數列最大最小值差距過大時,並不適用計數排序。
比如給定20個隨機整數,範圍在0到1億之間,這時候如果使用計數排序,需要創建長度1億的數組。不但嚴重浪費空間,而且時間複雜度也隨之升高。
2.當數列元素不是整數,並不適用計數排序。
如果數列中的元素都是小數,比如25.213,或是0.00000001這樣子,則無法創建對應的統計數組。這樣顯然無法進行計數排序。
對於這些侷限性,另一種線性時間排序算法(桶排序)做出了彌補
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)
參考鏈接:https://www.cnblogs.com/codeMedita/p/7425291.html
動態規劃要點:
將原問題拆解成若干子問題,同時保存子問題的答案,使得每個子問題只求解一次,最終獲得原問題的答案
大多數動態規劃問題本質都是遞歸問題——重疊子問題——記憶化搜索(自頂向下)
——動態規劃(自底向上)
- 求一個問題的最優解
- 整體問題的最優解依賴各個子問題的最優解
- 子問題之間有相互重疊的更小的子問題
- 從上往下分析問題,從下往上求解問題
三個重要概念:最優子結構,邊界,狀態轉移公式
遞歸:記憶化搜索——自上而下的解決問題
動態規劃——自下而上的解決問題
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