2018年10月8日
/*
本節主要內容:
1、 時間復雜度
2、冒泡排序
3、選擇排序
4、插入排序
5、對數器概念和使用
6、遞歸行為的時間復雜度
7、歸并排序
8、快速排序
9、堆排序
10、排序算法的總結和排序穩定性總結
11、工程中的綜合排序算法
12、比較器的作用
*/
時間復雜度:
常數時間的操作:
-------------------------------------------------------------------------
一個操作如果和數據量沒有關系,每次都是固定時間內完成的操作,叫做常數操作。
時間復雜度的表示:
-------------------------------------------------------------------------
時間復雜度為一個算法流程中,常數操作數量的指標。常用O (讀作big O)來表示。
具體來說,在常數操作數量的表達式中
只要高階項,不要低階項,也不要高階項的系數,剩下的部分 如果記為f(N),那么時間復雜度為O(f(N))。
評價一個算法流程的好壞
-------------------------------------------------------------------------
評價一個算法流程的好壞,先看時間復雜度的指標,然后再分析不同數據樣本下的實際運行時間,也就是常數項時間
理解額外空間復雜度:
要排序的數組不算做額外的空間,因為我是在提供的數組上進行操作的,就不算作額外的空間
但是像歸并排序:需要借助額外的數組,因此歸并排序的額外空間復雜度為O(N),選擇排序,冒泡排序,插入排序的額外空間復雜度為O(1)
二分查找
在排好序的數組中查詢一個數,時間復雜度為O(log2(N)),也可以寫作O(logN)
時間復雜度例子:
一個有序數組A,另一個無序數組B,請打印B中的所有不在A中的數,A數組長度為N,B數組長度為M。
算法流程1:對于數組B中的每一個數,都在A中通過遍歷的方式找一下;
--------------------------------
時間復雜度為:O(M * N)
算法流程2:對于數組B中的每一個數,都在A中通過二分的方式找一下;
--------------------------------
時間復雜度為:O(M * logN)
因為二分查找的時間復雜度為O(logN)
算法流程3:先把數組B排序,然后用類似外排的方式打印所有不在A中出現的數;
-------------------------------
時間復雜度分析:
①B數組進行排序,假設時間復雜度為O(M * logM)
②類似外排的方式檢查,時間復雜度為:O(M + N)
指針a最多滑動N次,指針b最多滑動M次,且每次只滑動a和b中的一個,最差是a和b都走完了這兩個數組
即時間復雜度為:O(M + N)
③故而時間復雜度為:O(M * logM) + O(M + N)
如何分析好壞?
如果A數組長,B數組短,則算法3更好,因為此時N更大
如果A數組短,B數組長,則算法2更好,因為此時N小,M大
冒泡排序:
public static void sort(Comparable[] a){
if(a == null || a.length < 2){
return;
}
for(int end = a.length - 1;end > 0;end--){
for(int i = 0;i < end;i++){
if(less(a,i+1,i))
exch(a,i,i+1);
}
}
}
冒泡排序時間復雜度:
N + (N-1) + (N-2)+ ...... + 1 = O(N2)
冒泡排序額外空間復雜度O(1)
選擇排序
public static void sort(Comparable[] a){
if(a == null || a.length < 2){
return;
}
for(int i = 0;i < a.length;i++){
int min = i;
for(int j = i+1;j < a.length;j++){
if(less(a,j,min))
min = j;
}
exch(a,i,min);
}
}
選擇排序時間復雜度:
N + (N-1) + (N-2)+ ...... + 1 = O(N2)
選擇排序額外空間復雜度O(1)
插入排序
-
首先不同于前面冒泡和選擇排序的是:冒泡和選擇的時間復雜度是與數據狀況無關的
冒泡:總是兩兩比較,最大的交換到右邊
選擇:每次都選出最小的,交換到最前面
這兩個都和數據狀況無關,時間復雜度都是O(N2)
- 插入排序則不同:
①當數據有序時:1,2,3,4,5
此時時間復雜度為0(N),因為此時對于每一個i,不需要j的移動,因此為O(N)
②當數據完全倒序時:5,4,3,2,1
此時時間復雜度為O(N2),因為此時對于從第二數開始的每一個數,都需要走到最開始才能回到正確位置,
因此要移動1 + 2 + 3 + 4 + 5 + ...... N 也就是O(N2)
③因此插入排序的時間復雜度為:O(N2)
- 當數據狀況不同產生的算法流程不同時,一律按照最差的來算
因此插入排序的時間復雜度為:O(N2)
插入排序額外空間復雜度O(1)
對數器:
驗證算法正確性:小樣本驗證大樣本
貪心策略:驗證貪心策略的正確性
對數器的概念和使用
0,有一個你想要測的方法a,
1,實現一個絕對正確但是復雜度不好的方法b
2,實現一個隨機樣本產生器
3,實現比對的方法
4,把方法a和方法b比對很多次來驗證方法a是否正確。
5,如果有一個樣本使得比對出錯,打印樣本分析是哪個方法出錯
6,當樣本數量很多時比對測試依然正確,可以確定方法a已經正確。
代碼實現:
/**
* 0,有一個你想要測的方法a——此時為插入排序的代碼
*/
public static void sort(int[] a){
int N = a.length;
for(int i = 1;i < N;i++){
int tmp = a[i];
int j = i;
while(j - 1 >= 0 && less(tmp,a[j-1])){
a[j] = a[j -1];
j--;
}
a[j] = tmp;
}
}
private static boolean less(Comparable c0, Comparable c1) {
return c0.compareTo(c1) < 0;
}
/**
* 1,實現一個絕對正確但是復雜度不好的方法b
*/
public static void rightMethod(int[] a){
Arrays.sort(a);
}
/**
* 2,實現一個隨機樣本產生器
*/
public static int[] generateRandomArray(int maxSize,int maxValue){
//此時該數組中:長度隨機,每一個位置上的元素隨機
//生成長度隨機的數組
int[] a = new int[(int) ((maxSize + 1) * Math.random())]; //此時數組的長度為[0,maxSize]整數
for (int i = 0; i < a.length; i++) {
a[i] = (int) ((maxValue + 1) * Math.random()) - (int) ((maxValue) * Math.random()); //產生正數或者負數
}
return a;
}
// 3,實現比對的方法
private static boolean isEqual(int[] a1, int[] a2) {
if((a1 == null && a2 != null) || (a1 != null && a2 == null))
return false;
if(a1 == null && a2 == null)
return true;
if(a1.length != a2.length)
return false;
for(int i = 0;i < a1.length;i++) {
if(a1[i] != a2[i])
return false;
}
return true;
}
/**
*
* 4,把方法a和方法b比對很多次來驗證方法a是否正確。
* for test
*/
public static void main(String[] args) {
int testTime = 500000;
int maxSize = 10; //產生數組長度為10的隨機數組
int maxValue = 100;
boolean succeed = true;
for (int i = 0; i < testTime; i++) {
int[] a1 = generateRandomArray(maxSize, maxValue);
int[] a2 = copyArray(a1);
int[] a3 = copyArray(a1);
sort(a1);
rightMethod(a2);
if(!isEqual(a1,a2)){
succeed = false;
//打印出錯的數組,printArray是自己實現的打印的方法
printArray(a3);
break;
}
}
System.out.println(succeed ? "Nice":"F@#K");
int[] arr = generateRandomArray(maxSize,maxValue);
printArray(arr);
sort(arr);
printArray(arr);
}
/**
* 數組打印
*/
private static void printArray(int[] a) {
for (int i = 0; i < a.length; i++) {
System.out.print(a[i] + " ");
}
System.out.println();
}
/**
* 數組的拷貝
*/
private static int[] copyArray(int[] a) {
int[] aCopy = new int[a.length];
for (int i = 0; i < a.length; i++) {
aCopy[i] = a[i];
}
return aCopy;
}
------------------------------------------------------------------------------------
面試時:準備對數器:排序,堆,二叉樹
2018年10月9日
剖析遞歸行為和遞歸行為時間復雜度的估算
舉個例子
/*
在數組中找最大值——遞歸來實現
*/
public static int getMax(int[] a,int lo,int hi) {
if(lo == hi)
return a[lo];
int mid = (lo + hi) /2;
int getMaxLeft = getMax(a,lo,mid);
int getMaxRight = getMax(a, mid + 1, hi);
return Math.max(getMaxLeft, getMaxRight);
}
遞歸函數就是系統在幫你壓棧
分析遞歸函數行為的通式:
master公式:
T(N) = a * T(N/b) + O(N^d)
master公式適用于每一次劃分的子問題的規模一致的情況下
其中參數的含義:
N/b:子過程的樣本量
a : 子過程量發生的次數
O(N ^ d) :除去調用子過程外,剩下的過程的時間復雜度
T(N):樣本量為N時的時間復雜度
如上算法:
樣本量為N/2的過程發生了兩次,為2 * T(N/2)
子過程發生的次數為2
除去調用子過程,我們剩下的過程的時間復雜度為:O(1)——因為只進行了一個兩個數之間求最大值的運算
因此:
此時的a = 2,b = 2,d = 0
則時間復雜度的分析為:
log(b,a) > d ==> 復雜度為O(N ^ log(b,a))
log(b,a) = d ==> 復雜度為O(N^d * logN)
log(b,a) < d ==> 復雜度為O(N^d)
此時我們的算法滿足的是:log(b,a) > d,則時間復雜度為:O(N)
歸并排序:
public class Merge {
private static Comparable[] aux;
public static void sort(Comparable[] a){
if(a == null|| a.length < 2)
return;
aux = new Comparable[a.length];
sort(a,0,a.length - 1);
}
public static void sort(Comparable[] a,int lo,int hi){
if(hi <= lo){
return;
}
int mid = lo + (hi - lo)/2;
sort(a,lo,mid);
sort(a,mid + 1,hi);
merge(a,lo,mid,hi);
}
private static void merge(Comparable[] a, int lo, int mid, int hi) {
int i = lo;
int j = mid + 1;
for (int k = lo; k <= hi; k++) {
aux[k] = a[k];
}
for (int k = lo; k <= hi; k++) {
if(i > mid) a[k] = aux[j++];
else if (j > hi) a[k] = aux[i++];
else if(less(aux[i],aux[j])) a[k] = aux[i++];
else a[k] = aux[j++];
}
}
private static boolean less(Comparable c0, Comparable c1) {
return c0.compareTo(c1) < 0;
}
}
時間復雜度分析:
①子過程樣本量為N/2——>也就是b = 2
②子過程發生次數為2——>也就是a = 2
③除去調用子過程外,剩下的過程為兩個有序數組的合并,時間復雜度為:O(N/2 + N/2) = O(N) ——>d = 1
④滿足 log(b,a) = d ==> 復雜度為O(N^d * logN)
則時間復雜度:O(N * logN),額外空間復雜度為:O(N)
快速排序:(此時為隨機快排,最常用的排序算法)
public static void sort(Comparable[] a){
if(a == null || a.length < 2)
return;
StdRandom.shuffle(a);
sort(a, 0, a.length - 1);
}
private static void sort(Comparable[] a, int lo, int hi) {
if (hi <= lo) {
return;
}
int pos = partition(a,lo,hi);
sort(a,lo,pos-1);
sort(a, pos + 1, hi);
}
/關鍵代碼/
private static int partition(Comparable[] a, int lo, int hi) {
Comparable v = a[lo];
int i = lo;
int j = hi + 1;
while (true) {
while(less(a[++i],v)) if(i == hi) break;
while(less(v,a[--j])) if(j == lo) break;
if(j <= i)
break;
exch(a, i, j);
}
//交換a[lo]和a[j]
exch(a,lo,j);
return j;
}
private static void exch(Comparable[] a, int i, int j) {
Comparable tmp = a[i];
a[i] = a[j];
a[j] = tmp;
}
private static boolean less(Comparable c0, Comparable c1) {
return c0.compareTo(c1) < 0;
}
時間復雜度分析:
關鍵代碼在于:partition
每一次partition均會固定第一個為partition item
-----------------------------------------------
此時的快速排序的算法的時間復雜度是與數據狀況有關系的
例如:
情況一:最壞情況1
[0,1,2,3,4,5,6]
partition(a,0,6)
----------------------------------------------------------------------------
當進入partition時,由于i所指向的1大于0,因此i停止移動,此時i == 0
j從6出發,一直大于0,知道j == 0停止
此時j <= i退出循環
最后交換a[0]和a[0]
一共比較N-1次,交換一次
時間復雜度為O(N)
----------------------------------------------------------------------------
partition(a,1,6)
----------------------------------------------------------------------------
此時與上一個是類似的情況,進行了N-2次比較,交換一次
時間復雜度為O(N-1)
----------------------------------------------------------------------------
..................
因此,此時的時間復雜度為N + (N - 1) + (N - 2) + .......... + 1 = o(N2)
情況二:最壞情況2
[6,5,4,3,2,1,0]
與上一種情況類似,時間復雜度也是O(N2)
情況三:最好情況
中間為x,此時時間復雜度為O(NlogN),聯系到3-way partition sort
快速排序的時間復雜度:
時間復雜度為:O(NlogN)——此時的時間復雜度是一個長期期望的一個時間復雜度
額外空間復雜度為:O(logN)
理解此時的額外空間復雜度:空間浪費在了劃分點pos上
-----------------------------------
int pos = partition(a,lo,hi);
sort(a,lo,pos-1);
sort(a, pos + 1, hi);
-------------------------------------------
sort代碼塊中,每一次我們都需要記錄pos的值,此時,當sort(a,lo,pos-1);執行完后,我們才知道右側部分在哪兒
要注意:數組能被二分多少次,額外的空間就是多少
------------------------------------------
因此:長期的期望下:額外的空間復雜度為O(logN)
快速排序對于大量的等值存在時的優化:3-way partition
public static void sort(Comparable[] a){
if (a == null || a.length < 2) {
return;
}
StdRandom.shuffle(a);
sort(a, 0, a.length - 1);
}
private static void sort(Comparable[] a, int lo, int hi) {
if (hi <= lo) {
return;
}
Comparable v = a[lo];
int lt = lo;
int gt = hi;
int i = lo;
while (i <= gt) {
int cmp = a[i].compareTo(v);
if(cmp < 0) exch(a, lt++, i++);
else if(cmp > 0) exch(a, i, gt--);
else i++;
}
sort(a,lo,lt-1);
sort(a,gt+1,hi);
}
private static void exch(Comparable[] a, int i, int j) {
Comparable tmp = a[i];
a[i] = a[j];
a[j] = tmp;
}
快速排序的思考:
有些算法在設計的過程中,我們想要繞過本身的數據狀況怎么辦?
** 有兩個主要的做法:
** ①隨機打亂數據狀況,那么此時的時間復雜度就是一個概率事件,可能好可能壞
** ②利用哈希進行打亂
三個O(NlogN)的算法:①QickSort ②MergeSort ③HeapSort
最常用的就是快QuickSort:
* 常數項很低
在工程上:其實并不常見遞歸函數的存在,通常會改為非遞歸的版本
堆排序——HeapSort
public static void sort(Comparable[] a){
int N = a.length;
//構建二叉堆——時間復雜度為O(N)
for (int k = N/2; k >= 1; k--) {
sink(a,k,N);
}
//進行排序——時間復雜度為:O(NlogN)
while(N > 1){
exch(a,1,N);
sink(a,1,--N);
}
}
/*
- 將父節點與較大的的子節點進行交換
- */
private static void sink(Comparable[] a, int k, int N) {
while(2 * k <= N){
int j = 2 * k;
if(j < N && less(a,j,j+1)) j = j + 1; //j指向較大的字節點
if(!less(a,k,j)) break;
exch(a,j,k); //交換父節點和較大字節點
k = j; //k繼續向下進行判斷
}
}
private static void exch(Comparable[] a, int i, int j) {
i--;
j--;
Comparable v = a[i];
a[i] = a[j];
a[j] = v;
}
private static boolean less(Comparable[] a, int i, int j) {
return a[--i].compareTo(a[--j]) < 0;
}
時間復雜度分析:
此時利用的是堆這種數據結構:二叉堆
——堆有序的完全二叉樹稱為二叉堆
——二叉樹的高度為logN
堆排序中有兩個關鍵的步驟:
①構建二叉堆——時間復雜度為O(N)
* 代碼:
--------------------------------
for (int k = N/2; k >= 1; k--) {
sink(a,k,N);
}
---------------------------------
* 時間復雜度:
----------------------------------------------------------------------------
每一次sink,都是對一條鏈進行操作,而鏈的長度是當前堆的高度
因為葉子節點本身就是堆有序的,即有N/2個不需要進行sink
而對于剩下的1-N/2,都需要sink,時間復雜度為:
log(N-1) + log(N - 2) + log(N - 3) + ............ + log(N - N/2)
即此時的時間復雜度為O(N)
----------------------------------------------------------------------------
②進行排序——時間復雜度為:O(NlogN)
* 代碼:
--------------------------------
while(N > 1){
exch(a,1,N);
sink(a,1,--N);
}
-------------------------------
* 時間復雜度:
------------------------------------------------------------------------------------------------------
本身sink(1)的時間復雜度為logN,但是每一次數組的最后都是存放最大值,下次便不再參與比較,每一次搞定的都是末尾的數
總結:堆排序時間復雜度為:O(NlogN),額外空間復雜度為:O(1)
堆是一種很重要的數據結構
排序的穩定性及其匯總
排序的穩定性指的是:排序后相同的數在原始數組中的相對位置不變
幾大排序算法的穩定性
第一類:時間復雜度為O(N2)的排序算法
①冒泡排序:
——可以實現排序穩定性
* 相同的值不進行交換
②插入排序:
——可以實現排序穩定性
* 向前插入時,遇到相同的值則停止
③選擇排序:
——不可以實現排序穩定性
* 例如:5,5,5,4,0,1
此時選擇最小的0與第一位5進行交換,此時就已經破壞了穩定性
第二類:時間復雜度為O(NlogN)的排序算法
④歸并排序:
——可以實現排序穩定性
* 最后merge中:當左邊和右邊相等時,就拷貝左邊的,就可以保證排序穩定性
⑤快速排序:
——不可以實現排序穩定性
* 因為partition過程無法做到排序穩定性,partition item總是a[lo],可能就導致相同的數順序被打亂
⑥堆排序
——不可以實現穩定性
* 構建大頂堆的時候排序穩定性就已經被破壞
為什么要追求排序的穩定性?
答:實際業務中,希望原始信息不被抹去
1、如果只是簡單的進行數字的排序,那么穩定性將毫無意義。
2、如果排序的內容僅僅是一個復雜對象的某一個數字屬性,那么穩定性依舊將毫無意義(所謂的交換操作的開銷已經算在算法的開銷內了,如果嫌棄這種開銷,不如換算法好了?)
3、如果要排序的內容是一個復雜對象的多個數字屬性,但是其原本的初始順序毫無意義,那么穩定性依舊將毫無意義。
4、除非要排序的內容是一個復雜對象的多個數字屬性,且其原本的初始順序存在意義,那么我們需要在二次排序的基礎上保持原有排序的意義,才需要使用到穩定性的算法,例如要排序的內容是一組原本按照價格高低排序的對象,如今需要按照銷量高低排序,使用穩定性算法,可以使得想同銷量的對象依舊保持著價格高低的排序展現,只有銷量不同的才會重新排序。(當然,如果需求不需要保持初始的排序意義,那么使用穩定性算法依舊將毫無意義)。
介紹一下工程中的綜合排序算法
常見的綜合排序:
----------------------------------------------------------------------------
Ⅰ 若數組長度很長,則在工程上:
* 首先進行判斷:是基本類型還是自定義類型
** 如果為基礎類型:則使用快速排序,因為不需要考慮數據穩定性
** 如果為自定義類型,則使用的是歸并排序,因為需要考慮數據穩定性
----------------------------------------------------------------------------
Ⅱ 若數組長度短,則直接使用插入排序,不管是基本數據類型還是自定義類型
雖然插入排序時間復雜度為:O(N2),但是在數據量很小的情況下,劣勢不會有很大表現
----------------------------------------------------------------------------
數組長度小于60時,直接使用插入排序
此時聯系:快排和歸并在數據量小的時候的優化
面試:為什么在綜合排序中數據量小的部分選擇使用插入排序
答:雖然復雜度高,但是常數項很低,在數據量很小的情況下,劣勢不會有很大表現
只有在數據量很大的時候,常數項才可以被忽略,插入排序的劣勢才表現出來
面試:為什么基本類型選擇使用快排,而自定義類型選擇歸并
答:考慮到的是數據穩定性問題,快排因為partition數據穩定性得不到保證
而歸并排序是可以設計為排序穩定性的
面試:數組中:將數組中奇數放左邊,偶數放右邊,要求原始數組相對次序不變,且時間復雜度為O(N),空間復雜度為O(1)
答:不能,因為此時奇數偶數也是一個0/1問題,對于快速排序中也是0/1標準,而快速排序的時間復雜度為O(NlogN)
0/1 stable sort很難
認識比較器的作用
Arrays.sort(stus,new IdAscendingComparator());
Arrays方法可以傳入一個數組和自定義的比較器
利用到比較器的集合:
PriorityQueue TreeMap