左神算法課基礎班知識點總結(一)

時間複雜度

什麼是時間複雜度?
       答:算法中基本操作的執行次數。

異或運算

       異或運算,即無進位相加,滿足交換律和結合律。(不適應於浮點型!)
       兩個二進制數進行異或運算,對應位置的數,相同爲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));
}

目前就更新到這裏,若覺得文章存在問題,歡迎斧正~~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章