時間複雜度
什麼是時間複雜度?
答:算法中基本操作的執行次數。
異或運算
異或運算,即無進位相加,滿足交換律和結合律。(不適應於浮點型!)
兩個二進制數進行異或運算,對應位置的數,相同爲0,不同爲1。
異或運算語法:a^a =0; a^0=a;
小技巧:swap函數可以用"異或運算"實現
Code:
//侷限性在於:使用異或運算來交換兩個數時,需要的是不同的引用,否則無效。且不適用於浮點型。
public static void swap(int[] arr,int i,int j){
//此處用到了結合律和交換律
//a^a=0; a^0=a;
arr[i] = arr[i] ^ arr[j];
arr[j] = arr[i] ^ arr[j];
arr[i] = arr[i] ^ arr[j];
}
冒泡排序
時間複雜度爲:O(N^2),額外空間複雜度爲:O(1),可以實現穩定性!
穩定性:數組在排序前後值相同的數的相對順序不變。
Code:
public static void bubbleSort(int[] arr){
if(arr == null || arr.length < 2){
return;
}
for(int e = arr.length - 1; e>0; e--){//最外層的for循環,用來表示,我們要將數放到最後一個位置,倒數第二個位置,...
for(int i = 0; i<e; i++){
/*
是否會越界?
答:必然不會越界!
首先 e = arr.length - 1;,其次i<e,也就是說,i小於n-1,那麼i+1就必然不越界。
*/
if(arr[i]>arr[i+1]){
swap(arr,i,i+1);
}
}
}
}
public static void swap(int[] arr,int i,int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
插入排序
時間複雜度爲:O(N^2),額外空間複雜度爲O(1),可以實現穩定性;
因爲只有待插入元素的前一個元素大於待插入元素時,二者才進行交換,小於等於都不交換,因此相對位置不會改變,可以實現穩定性。
我們默認數組0位置上的元素是有序的,之後將數組剩餘的(n-1)個元素插入到有序序列中,最終使得數組有序。
Code:
public static void insertSort(int[] arr){
if(arr == null || arr.length < 2){
return;
}
for(int i = 1; i<arr.length; i++){//i從1開始,代表待插入元素的索引;
for(int j = i-1; j>=0 && arr[j]>arr[j+1]; j--){
swap(arr,j,j+1);
}
}
}
選擇排序
時間複雜度爲:O(N^2),額外空間複雜度爲O(1),不可以實現穩定性
"快些選一堆",都是不穩定的排序.
選擇數組中最小的元素與數組0位置的元素進行交換,選擇數組中第二小的數與數組1位置上的元素進行交換,如此往復直到整個數組有序。
Code:
public static void selectSort(int[] arr){
if(arr == null || arr.length < 2){
return;
}
for(int i = 0; i<arr.length-1; i++){
//之所以,這裏的i<arr.length-1;,是爲了避免數組越界,因爲下面的j = i+1;
int minIndex = i;
for(int j = i+1; j<arr.length; j++){
minIndex = arr[j] > arr[minIndex] ? minIndex : j;
}
swap(arr,minIndex,i);
}
}
隨機快排
常數項小,時間複雜度爲O(N*logN)
分析:
①選取劃分值:p; 花費O(1)
②劃分 <p(小於區) =p(等於區) >p(大於區) ,也就是熟悉的partition過程,O(N)
③左側 <p 和 右側 >p,分別去遞歸,當劃分值選的不好時,時間複雜度會升高,最差爲O(N^2)
隨機選擇劃分值可以在概率上避免數組本身的規律,也就儘可能避免了時間複雜度過高的情況。
空間複雜度爲:O(logN),需要記錄斷點的位置。當額外空間複雜度爲O(logN)時,快排做不到穩定性!
Code:
public static void quickSort(int[] arr){
if(arr == null || arr.length < 2){
return;
}
quickSort(arr,0,arr.length - 1);
}
public static void quickSort(int[] arr,int l,int r){
if(l < r){
//首先我們隨機選擇劃分值,之後將劃分值與數組r位置的元素進行交換;
/*
此處,Math.random()函數返回的是浮點型的[0,1)的數;
假設,l = 5,r = 10;
則(r-l+1) = 6;
於是 6 * [0,1) = [0,5];
之後再用 l + [0,5] 就生成了 l到 r中的隨機數;
*/
swap(arr,l+(int)((r-l+1)*Math.random()),r);
//得到劃分值以後,在整個範圍內進行partition過程,
//返回等於區的兩個邊界;
int[] p = partition(arr,l,r);
//對左右兩端的數據進行遞歸。
quickSort(arr,l,p[0]-1);
quickSort(arr,p[1]+1,r);
}
}
public static int[] partition(int[] arr,int l,int r){
int less = l-1;//初始化小於區的邊界
int more = r;//初始化大於區的邊界
while(l < more){
if(arr[l] < arr[r]){//若當前數小於劃分值時,則將小於區的下一個數與當前數進行交換,同時l++;
swap(arr,++less,l++);
}else if(arr[l] > arr[r]){//若當前數大於劃分值時,則將大於區的前一個數與當前數進行交換,同時l不變,繼續判斷l位置上的數是大於or小於or等於劃分值。
swap(arr,--more,l);
}else{//若當前數與劃分值相等,則l++;
l++;
}
}
//當執行到此處時,l==more,此時數組中爲 <p =p >p p,需要將r處的p與大於區的第一個元素進行交換;
swap(arr,more,r);
return new int[]{less+1,more};//最後返回等於區的左右邊界。
}
荷蘭國旗問題
Code:
public class Code_NetherlandsFlag{
public static int[] partition(int[] arr,int l,int r,int p){
int less = l-1;
int more = r+1;
while(l<more){
if(arr[l]<p){
swap(arr,++less,l++);
}else if(arr[l]>p){
swap(arr,--more,l);
}else{
l++;
}
}
return new int[]{less+1,more-1};
}
}
二分查找
二分查找的前提是:數組有序
題目:獲取存在於數組B中但不存在於數組A中的元素
右移(位運算):相當於除2操作,且比除快,但有侷限性。
Code:
public static List<Integer> getAllNotIncluded(int[] A,int[] B){
List<Integer> res = new ArrayList<Integer>();
for(int i=0; i<B.length; i++){
int l = 0;
int r = A.length - 1;
boolean contains = false;
while(l<=r){
int mid = l + ((r-l)>>1); //右移相當於除2,這樣取中間值時,不會溢出。
if(A[mid] > B[i]){
r = mid - 1;
}else if(A[mid] < B[i]){
l = mid + 1;
}else{
contains = true;
break;
}
}
if(!contains){
res.add(B[i]);
}
}
return res;
}
歸併排序
任何遞歸行爲都可以改爲非遞歸。
遞歸是系統幫你壓棧保存了函數的信息,我們可以自己建立棧來保存信息。
時間複雜度爲:O(N*logN),額外空間複雜度爲:O(N)(因爲merge的過程需要數組的輔助),可以實現穩定性
Code:
public staic void mergeSort(int[] arr){
if(arr == null || arr.length < 2){
return;
}
mergeSort(arr,0,arr.length-1);
}
public static void mergeSort(int[] arr,int l,int r){
if(l==r){//當拆分到只剩一個元素時,停止拆分。
return;
}
int mid = l + ((r-l)>>1);
mergeSort(arr,l,mid);
mergeSort(arr,mid+1,r);
merge(arr,l,mid,r);//合併操作
}
public static void merge(int[] arr,int l,int mid,int r){
//合併過程需要一個輔助數組
int[] help = new int[r-l+1];
int p1 = l;//指針1
int p2 = mid + 1;//指針2
int i = 0;
while(p1<=mid && p2<=r){
//兩個指針分別指向兩個合併組的初始位置,
//二者相比較,小的先放入數組中
help[i++] = arr[p1]<arr[p2] ? arr[p1++] : arr[p2++];
}
while(p1<=mid){
help[i++] = arr[p1++];
}
while(p2<=r){
help[i++] = arr[p2++];
}
//最後將help數組中的元素拷貝到原數組中。
for(i=0; i<help.length;i++){
arr[l+i] = help[i];
}
}
歸併排序的擴展–小和問題、逆序對
什麼是小和問題?
每個位置的左邊比它小的數都累加起來的和稱爲–小和。
什麼是逆序對?
逆序:若前面的數大於後面的數,則稱爲逆序;
小和問題Code:
public static int mergeSort(int[] arr){
if(arr == null || arr.length < 2){
return 0;
}
return mergeSort(arr,0,arr.length - 1);
}
public static int mergeSort(int[] arr){
if(l == r){
return 0;
}
int mid = l + ((r - l)>>1);
return mergeSort(arr,l,mid) + mergeSort(arr,mid+1,r) + merge(arr,l,mid,r);
}
public static int merge(int[] arr,int l,int mid,int r){
int[] help = new int[r - l + 1];
int res = 0;
int p1 = l;
int p2 = mid + 1;
int i = 0;
while(p1<=mid && p2<=r){
//此處,在合併時,比較左右兩組的數據,若arr[p1]小於arr[p2],則就存在(r-p2+1)個arr[p1]這樣的小和。
res += arr[p1]<arr[p2] ? (r - p2 +1) * arr[p1] : 0;
help[i++] = arr[p1]<arr[p2] ? arr[p1++] : arr[p2];
}
while(p1<=mid){
help[i++] = arr[p1++];
}
while(p2<=r){
help[i++] = arr[p2++];
}
//最後將help數組中的元素拷貝到原數組中。
for(i=0; i<help.length;i++){
arr[l+i] = help[i];
}
}
逆序對Code:
public static int mergeSort(int[] arr){
if(arr == null || arr.length < 2){
return 0;
}
return mergeSort(arr,0,arr.length-1);
}
public static int mergeSort(int[] arr,int l,int r){
if(l == r){
return 0;
}
int mid = l + ((r - l)>>1);
return mergeSort(arr,l,mid) + mergeSort(arr,mid + 1,r) + merge(arr,l,mid,r);
}
public static int merge(int[] arr,int l,int mid,int r){
int[] help = new int[r-l+1];
int count = 0;
int p1 = l;
int p2 = mid + 1;
int i = 0;
while(p1<=mid && p2<=r){
//此處,在合併時,比較兩組的數據,由於都是升序排列,因此逆序對需要如下這樣計算。
count += arr[p1] > arr[p2] ? (mid - p1 + 1) : 0;
help[i++] = arr[p1]<arr[p2]?arr[p1++]:arr[p2++];
}
while(p1<=mid){
help[i++] = arr[p1++];
}
while(p2<=r){
help[i++] = arr[p2++];
}
//最後將help數組中的元素拷貝到原數組中。
for(i=0; i<help.length;i++){
arr[l+i] = help[i];
}
}
堆排序
時間複雜度爲:O(N*logN),額外空間複雜度爲:O(1),不可以實現穩定性!
常數項大,堆在邏輯上是一棵完全二叉樹,有如下公式:
index位置的父節點索引爲:(index - 1)/2
index位置的左孩子索引爲:(2 * index) + 1
index位置的右孩子索引爲:(2 * index) + 2
大根堆:任何一個節點都是其下面那棵樹中值最大的點。
堆排序的流程:首先將數組的所有位置建立大根堆(heapInsert過程),之後將大根堆的堆頂元素和數組最後一個位置的元素進行交換,在交換之後的堆不能保證依舊是大根堆,則需要重新調整爲大根堆(heapify過程),之後繼續交換堆頂元素和數組中倒數第二個元素,如此往復,直到數組有序爲止。
Code:
public static void heapSort(int[] arr){
if(arr == null || arr.length < 2){
return;
}
//首先將數組建立爲大根堆
for(int i = 0; i<arr.length; i++){
heapInsert(arr,i);
}
//藉助變量size來確定堆的大小
int size = arr.length;
//之後交換堆頂元素和數組最末元素;
swap(arr,0,--size);
//在交換之後,重新調整爲大根堆,如此往復直到排序完成。
while(size > 0){
//調整爲大根堆,此時index爲0,進行heapify下沉過程.
heapify(arr,0,size);
//調整完之後,交換
swap(arr,0,--size);
}
}
public static void heapify(int[] arr,int index,int size){
int left = (2 * index) + 1;
while(left < size){//若index位置存在左孩子
//largest變量用來標識index位置的元素和其左右孩子中最大的那個元素的索引;
int largest = left + 1 < size && arr[left+1] > arr[left] ? left +1 : left;
largest = arr[largest] > arr[index] ? largest : index;
//若比較來比較去,發現最大的元素的索引依舊是index,則不用調整;
if(largest == index){
break;
}
//若比較完之後,最大元素的索引不是index位置,則二者交換
swap(arr,index,largest);
//之後,更新index位置,繼續進行下沉操作。
index = largest;
//同時更新left,進行下一輪循環。
left = (2 * index) + 1;
}
}
public static void heapInsert(int[] arr,int index){
/*
從index位置開始,請你往上瞅,與其父比較,若比其父大,則二者交換,之後繼續往上瞅,與其父比較,若依舊大於其父,則繼續交換,直到不比其父大 或者 index爲0時,停止;
*/
while(arr[index] > arr[(index - 1)/2]){
swap(arr,index,(index - 1)/2);
index = (index - 1)/2;
}
}
基於桶排序思想的題目
非基於比較的排序,對數據的位數和範圍有限制,可以優化,但是解決不了。
題目:在一個無序數組中,求如果有序之後,相鄰元素之間最大差值。
思路:假設數組有N個數,我們需要準備N+1個桶,之後遍歷數組,將數組中最小值min放在第一個桶中,數組中最大值max放在最後一個桶,之後將range = (max - min)/(N+1)均分,使得每個桶存放一定範圍的數值;由於我們有N+1個桶,而只有N個數,再加上第一個和最後一個桶都有元素,則必然存在一個空桶位於第一個桶和最後一個桶之間,此時我們考慮,空桶左邊第一個非空桶的最大值與空桶右邊第一個非空桶的最小值,在排序之後一定是相鄰的元素,且二者的差值必然 >= range,(其實每個桶內也有相鄰的數據,但是它們之間的差值 <= range,由於我們要求最大差值,所以就不考慮這種情況),因此我們不用管桶內部的相鄰數據,我們只用考慮桶與桶之間的相鄰數,因此我們只用獲取每個桶的最大值max與最小值min;每個桶只找它前面離它最近的非空桶,最大差值不一定來自於空桶兩側的非空桶,有可能來自於兩個相鄰的非空桶.
爲什麼要加入空桶?爲什麼要用N+1個桶?
答:加入空桶,只是爲了說明,我們不用考慮桶內相鄰數據的差值,只是爲了排除這種情況。
Code:
public static int maxGap(int[] nums){
if(nums == null || nums.length < 2){
return 0;
}
int len = nums.length;
int min = Integer.MAX_VALUE;
int max = Integer.MIN_VALUE;
//首選獲取數組中最大值和最小值
for(int i = 0; i<len; i++){
min = Math.min(min,nums[i]);
max = Math.max(max,nums[i]);
}
//若最大值 == 最小值,則表示數組中爲同一個數,則直接返回
if(min == max){
return 0;
}
//之後,創建三個數組
//1.表明每一個桶是否爲空?
boolean[] hasNum = new boolean[len + 1];
//2.
int[] maxs = new int[len + 1];
//3.
int[] mins = new int[len + 1];
int bid = 0;//用來標識桶號
for(int i=0; i<len; i++){
//拿到數組中的每一個數,通過max與min等參數算出這個數應該進幾號桶。
//同時更新對應桶的最大值、最小值以及桶的狀態。
bid = bucket(nums[i],len,min,max);
mins[bid] = hasNum[bid]?Math.min(mins[bid],nums[i]):nums[i];
maxs[bid] = hasNum[bid]?Math.max(maxs[bid],nums[i]):nums[i];
//此時該桶不爲空.
hasNum[bid] = true;
}
int res = 0;
//上一個桶的最大值,初始值默認爲0號桶的最大值
int lastMax = maxs[0];
int i = 1;
for(;i<=len;i++){
if(hasNum[i]){
//後一個桶的最小值減去前一個桶的最大值,即爲所求相鄰元素的差值。
res = Math.max(res,mins[i] - lastMax);
lastMax = maxs[i];
}
}
return res;
}
//根據給定的min和max,算出每個數值num對應的桶號;
public static int bucket(long num,long len,long min,long max){
return (int) ((num - min)*len/(max-min));
}
目前就更新到這裏,若覺得文章存在問題,歡迎斧正~~