左神直通BAT算法筆記(基礎篇)-上

我的技術博客:www.zhenganwen.topjava

時間複雜度

時間複雜度是衡量算法好壞的重要指標之一。時間複雜度反映的是不肯定性樣本量的增加對於算法操做所需時間的影響程度,與算法操做是否涉及到樣本量以及涉及了幾回直接相關,如遍歷數組時時間複雜度爲數組長度n(對應時間複雜度爲O(n)),而對數據的元操做(如加減乘除與或非等)、邏輯操做(如if判斷)等都屬於常數時間內的操做(對應時間複雜度O(1))。node

在化簡某算法時間複雜度表達式時需遵循如下規則:面試

  • 對於同同樣本量,可省去低階次數項,僅保留高階次數項,如O(n^2)+O(n)可化簡爲O(n^2)O(n)+O(1)可化簡爲O(n)
  • 可省去樣本量前的常量係數,如O(2n)可化簡爲O(n)O(8)可化簡爲O(1)
  • 對於不一樣的不肯定性樣本量,不能按照上述兩個規則進行化簡,要根據實際樣本量的大小分析表達式增量。如O(logm)+O(n^2)不能化簡爲O(n^2)O(logm)。而要視m、n二者之間的差距來化簡,好比m>>n時能夠化簡爲O(logm),由於表達式增量是由樣本量決定的。

額外空間複雜度

算法額外空間複雜度指的是對於輸入樣本,通過算法操做須要的額外空間。好比使用冒泡排序對一個數組排序,期間只須要一個臨時變量temp,那麼該算法的額外空間複雜度爲O(1)。又如歸併排序,在排序過程當中須要建立一個與樣本數組相同大小的輔助數組,儘管在排序事後該數組被銷燬,但該算法的額外空間複雜度爲O(n)算法

經典例題——觸類旁通

找出B中不屬於A的數

找出數組B中不屬於A的數,數組A有序而數組B無序。假設數組A有n個數,數組B有m個數,寫出算法並分析時間複雜度。api

方法一:遍歷

首先遍歷B,將B中的每一個數拿到到A中找,若找到則打印。對應算法以下:數組

int A[] = {1, 2, 3, 4, 5};
int B[] = {1, 4, 2, 6, 5, 7};

for (int i = 0; i < 6; ++i) {
  int temp = B[i];
  bool flag = false;
  for (int j = 0; j < 5; ++j) {
    if (A[j] == temp) {
      flag = true;    //找到了
      break;
    }
  }
  if (!flag) {    //沒找到
    printf("%d", temp);
  }
}
複製代碼

不難看出上述算法的時間複雜度爲O(m*n),由於將兩個數組都遍歷了一遍緩存

方法二:二分查找

因爲數組A是有序的,在一個有序序列中查找一個元素可使用二分法(也稱折半法)。原理就是將查找的元素與序列的中位數進行比較,若是小於則去掉中位數及其以後的序列,若是大於則去掉中位數及其以前的序列,若是等於則找到了。若是不等於那麼再將其與剩下的序列繼續比較直到找到或剩下的序列爲空爲止。安全

利用二分法對應題解的代碼以下:bash

for (int i = 0; i < 6; ++i) {		//B的長度爲6
  int temp = B[i];
  //二分法查找
  int left = 0,right = 5-1;			//A的長度爲5
  int mid = (left + right) / 2;
  while (left < right && A[mid] != temp) {
    if (A[mid] > temp) {
      right = mid - 1;
    } else {
      left = mid + 1;
    }
    mid = (left + right) / 2;
  }
  if (A[mid] != temp) {
    printf("%d", temp);
  }
}
複製代碼

for循環m次,while循環logn次(若是沒有特別說明,log均以2爲底),此算法的時間複雜度爲O(mlogn)數據結構

方法三:排序+外排

第三種方法就是將數組B也排序,而後使用逐次比對的方式來查找A數組中是否含有B數組中的某元素。引入a、b兩個指針分別指向數組A、B的首元素,比較指針指向的元素值,當a<b時,向後移動a指針查找該元素;當a=b時,說明A中存在該元素,跳過該元素查找,向後移動b;當a>b時說明A中不存在該元素,打印該元素並跳過該元素的查找,向後移動b。直到a或b有一個到達數組末尾爲止(若a先到達末尾,那麼b和b以後的數都不屬於A)

對應題解的代碼以下:

void fun3(int A[],int a_length,int B[],int b_length){
    quickSort(B, 0, b_length - 1);	//使用快速排序法對數組B排序->O(mlogm)
    int* a = A,*b=B;
    while (a <= A + a_length - 1 || b <= B + b_length - 1) {
        if (*a == *b) {
            b++;
            continue;
        }
        if (*a > *b) {
            printf("%d", *b);
            b++;
        } else {
            a++;
        }
    }

    if (a == A + a_length) {	//a先到頭
        while (b < B + b_length) {
            printf("%d", *b);
            b++;
        }
    }
}
複製代碼

快速排序的代碼以下:

#include <stdlib.h>
#include <time.h>

//交換兩個int變量的值
void swap(int &a, int &b){
    int temp = a;
    a = b;
    b = temp;
}

//產生一個low~high之間的隨機數
int randomInRange(int low, int high){
    srand((int) time(0));
    return (rand() % (high - low))+low;
}

//快速排序的核心算法,隨機選擇一個數,將比該數小的移至數組左邊,比該數大的移至
//數組右邊,最後返回該數的下標(移動完以後該數的下標可能與移動以前不同)
int partition(int arr[],int start,int end){
    if (arr == NULL || start < 0 || end <= 0 || start > end) {
        return -1;
    }

    int index = randomInRange(start, end);//隨機選擇一個數
    swap(arr[index], arr[end]);//將該數暫時放至末尾

    int small = start - 1;
    //遍歷前n-1個數與該數比較並以該數爲界限將前n-1個數
    //分爲兩組,small指向小於該數的那一組的最後一個元素
    for (index = start; index < end; index++) {
        if (arr[index] < arr[end]) {
            small++;
            if (small != index) {
                swap(arr[small], arr[index]);
            }
        }
    }

    //最後將該數放至數值較小的那一個組的中間
    ++small;
    swap(arr[small], arr[end]);
    return small;
}

void quickSort(int arr[],int start,int end) {
    if (start == end) {
        return;
    }
    int index = partition(arr, start, end);
    if (index > start) {
        quickSort(arr,start, index - 1);
    }
    if (index < end) {
        quickSort(arr, index + 1, end);
    }
}
複製代碼

此種方法的時間複雜度爲:O(mlogm)(先對B排序)+O(m+n)(最壞的狀況是指針a和b都到頭)。

三種方法的比較

  1. O(m*n)
  2. O(mlogn)(以2爲底)
  3. O(mlogm)+O(m+n)(以2爲底)

易知算法2比1更優,由於增加率logn<n。而2和3的比較取決於樣本量m和n之間的差距,若m>>n那麼2更優,不難理解:數組B元素較多,那麼對B的排序確定要花費較長時間,而這一步並非題解所必需的,不如採用二分法;相反地,若m<<n,那麼3更優。

荷蘭國旗問題

給定一個數組arr,和一個數num,請把小於num的數放在數組的左邊,等於num的數放在數組的中間,大於num的數放在數組的右邊。

要求額外空間複雜度O(1),時間複雜度O(N)

思路:利用兩個指針LR,將L指向首元素以前,將R指向尾元素以後。從頭遍歷序列,將當前遍歷元素與num比較,若num,則將其與L的右一個元素交換位置並遍歷下一個元素、右移L;若=num則直接遍歷下一個元素;若>num則將其和R的左一個元素交換位置,並從新判斷當前位置元素與num的關係。直到遍歷的元素下標到爲R-1爲止。

void swap(int &a, int &b){
    int temp = a;
    a = b;
    b = temp;
}
void partition(int arr[],int startIndex,int endIndex,int num){
    int L = startIndex - 1, R = endIndex + 1, i = startIndex;
    while (i <= R - 1) {
        if (arr[i] < num) {
            swap(arr[i++], arr[++L]);
        } else if (arr[i] > num) {
            swap(arr[i], arr[--R]);
        } else {
            i++;
        }
    }
}

int main(){
    int arr[] = {1,2, 1, 5, 4, 7, 2, 3, 9,1};
    travles(arr, 8);
    partition(arr, 0, 7, 2);
    travles(arr, 8);
    return 0;
}
複製代碼

L表明小於num的數的右界,R表明大於num的左界,partition的過程就是遍歷元素、不斷壯大L、R範圍的過程。這裏比較難理解的地方多是爲何arr[i]<num時要右移Larr[i]>num時卻不左移R,這是由於對於當前元素arr[i],若是arr[i]<num進行swap(arr[i],arr[L+1])以後對於當前下標的數據情況是知曉的(必定有arr[i]=arr[L+1]),由於是從頭遍歷到i的,而L+1<=i。可是若是arr[i]>num進行swap(arr[i],arr[R-1])以後對於當前元素的數據情況是不清楚的,由於R-1>=iarr[R-1]還沒遍歷到。

矩陣打印問題

轉圈打印方塊矩陣

給定一個4階矩陣以下:

打印結果以下(要求額外空間複雜度爲O(1)):

1 2 3 4 8 12 16 15 14 13 9 5 6 7 11 10
複製代碼

思路:這類問題須要將思惟打開,從宏觀的層面去找出問題存在的共性從而求解。若是你的思惟侷限在1是如何變到2的、4是怎麼變到8的、11以後爲何時十、它們之間有什麼關聯,那麼你就陷入死衚衕了。

從宏觀的層面找共性,其實轉圈打印的過程就是不斷順時針打印外圍元素的過程,只要給你一個左上角的點(如(0,0))和右下角的點(如(3,3)),你就可以打印出1 2 3 4 8 12 16 15 14 13 9 5;一樣,給你(1,1)(2,2),你就能打印出6 7 11 10。這個根據兩點打印正方形上元素的過程能夠抽取出來,整個問題也就迎刃而解了。

打印一個矩陣某個正方形上的點的邏輯以下:

//
// Created by zaw on 2018/10/21.
//
#include <stdio.h>
#define FACTORIAL 4

void printSquare(int leftUp[], int rigthDown[],int matrix[][FACTORIAL]){
    int i = leftUp[0], j = leftUp[1];
    while (j < rigthDown[1]) {
        printf("%d ", matrix[i][j++]);
    }
    while (i < rigthDown[0]) {
        printf("%d ", matrix[i++][j]);
    }
    while (j > leftUp[1]) {
        printf("%d ", matrix[i][j--]);
    }
    while (i > leftUp[0]) {
        printf("%d ", matrix[i--][j]);
    }
}

void printMatrixCircled(int matrix[][FACTORIAL]){
    int leftUp[] = {0, 0}, rightDown[] = {FACTORIAL-1,FACTORIAL-1};
    while (leftUp[0] < rightDown[0] && leftUp[1] < rightDown[1]) {
        printSquare(leftUp, rightDown, matrix);
        ++leftUp[0];
        ++leftUp[1];
        --rightDown[0];
        --rightDown[1];
    }
}

int main(){
    int matrix[4][4] = {
            {1,  2,  3,  4},
            {5,  6,  7,  8},
            {9,  10, 11, 12},
            {13, 14, 15, 16}
    };
    printMatrixCircled(matrix);//1 2 3 4 8 12 16 15 14 13 9 5 6 7 11 10
}
複製代碼

旋轉方塊矩陣

給定一個方塊矩陣,請把該矩陣調整成順時針旋轉90°以後的樣子,要求額外空間複雜度爲O(1)

思路:拿上圖舉例,首先選取矩陣四個角上的點1,3,9,7,按順時針的方向13的位置(1->3)、3->99->77->1,這樣對於旋轉後的矩陣而言,這四個點已經調整好了。接下來只需調整2,6,8,4的位置,調整方法是同樣的。只需對矩陣第一行的前n-1個點採用一樣的方法進行調整、對矩陣第二行的前前n-3個點……,那麼調整n階矩陣就容易了。

這也是在宏觀上觀察數據變更的通常規律,找到以不變應萬變的通解(給定一個點,肯定矩陣上以該點爲角的正方形,將該正方形旋轉90°),整個問題就不攻自破了。

//
// Created by zaw on 2018/10/21.
//
#include <stdio.h>
#define FACTORIAL 4

void circleSquare(int leftUp[],int rightDown[],int matrix[][FACTORIAL]){
    int p1[] = {leftUp[0], leftUp[1]};
    int p2[] = {leftUp[0], rightDown[1]};
    int p3[] = {rightDown[0], rightDown[1]};
    int p4[] = {rightDown[0],leftUp[1]};
    while (p1[1] < rightDown[1]) {
        //swap
        int tmp = matrix[p4[0]][p4[1]];
        matrix[p4[0]][p4[1]] = matrix[p3[0]][p3[1]];
        matrix[p3[0]][p3[1]] = matrix[p2[0]][p2[1]];
        matrix[p2[0]][p2[1]] = matrix[p1[0]][p1[1]];
        matrix[p1[0]][p1[1]] = tmp;

        p1[1]++;
        p2[0]++;
        p3[1]--;
        p4[0]--;
    }
}

void circleMatrix(int matrix[][FACTORIAL]){
    int leftUp[] = {0, 0}, rightDown[] = {FACTORIAL - 1, FACTORIAL - 1};
    while (leftUp[0] < rightDown[0] && leftUp[1] < rightDown[1]) {
        circleSquare(leftUp, rightDown, matrix);
        leftUp[0]++;
        leftUp[1]++;
        --rightDown[0];
        --rightDown[1];
    }
}

void printMatrix(int matrix[][FACTORIAL]){
    for (int i = 0; i < FACTORIAL; ++i) {
        for (int j = 0; j < FACTORIAL; ++j) {
            printf("%2d ", matrix[i][j]);
        }
        printf("\n");
    }
}

int main(){
    int matrix[FACTORIAL][FACTORIAL] = {
            {1,  2,  3,  4},
            {5,  6,  7,  8},
            {9,  10, 11, 12},
            {13, 14, 15, 16}
    };
    printMatrix(matrix);
    circleMatrix(matrix);
    printMatrix(matrix);
}
複製代碼

之字形打印矩陣

對如上矩陣的打印結果以下(要求額外空間複雜度爲O(1)):

1 2 7 13 8 3 4 9 14 15 10 5 6 11 16 17 12 18
複製代碼

此題也是須要從宏觀上找出一個共性:給你兩個,你可否將該兩點連成的45°斜線上的點按給定的打印方向打印出來。拿上圖舉例,給出(2,0)(0,2)turnUp=true,應該打印出13,8,3。那麼整個問題就變成了兩點的走向問題了,開始時兩點均爲(0,0),而後一個點往下走,另外一個點往右走(如1->71->2);當往下走的點是邊界點時就往右走(如13->14),當往右走的點到邊界時就往下走(如6->12)。每次兩點走一步,並打印兩點連線上的點。

//
// Created by zaw on 2018/10/22.
//
#include <stdio.h>

const int rows = 3;
const int cols = 6;

void printLine(int leftDown[],int rightUp[], bool turnUp,int matrix[rows][cols]){
    int i,j;
    if (turnUp) {
        i = leftDown[0], j = leftDown[1];
        while (j <= rightUp[1]) {
            printf("%d ", matrix[i--][j++]);
        }
    } else {
        i = rightUp[0], j = rightUp[1];
        while (i <= leftDown[0]) {
            printf("%d ", matrix[i++][j--]);
        }
    }
}

void zigZagPrintMatrix(int matrix[rows][cols]){
    if (matrix==NULL)
        return;
    int leftDown[] = {0, 0}, rightUp[] = {0, 0};
    bool turnUp = true;
    while (leftDown[1] <= cols - 1) {
        printLine(leftDown, rightUp, turnUp, matrix);
        turnUp = !turnUp;
        if (leftDown[0] < rows - 1) {
            leftDown[0]++;
        } else {
            leftDown[1]++;
        }
        if (rightUp[1] < cols - 1) {
            ++rightUp[1];
        } else {
            ++rightUp[0];
        }
    }
}

int main(){
    int matrix[rows][cols] = {
            {1,  2,  3,  4,  5,  6},
            {7,  8,  9,  10, 11, 12},
            {13, 14, 15, 16, 17, 18}
    };
    zigZagPrintMatrix(matrix);//1 2 7 13 8 3 4 9 14 15 10 5 6 11 16 17 12 18
    return 0;
}
複製代碼

在行和列都排好序的矩陣上找數

如圖:

任何一列或一行上的數是有序的,實現一個函數,判斷某個數是否存在於矩陣中。要求時間複雜度爲O(M+N),額外空間複雜度爲O(1)

從矩陣右上角的點開始取點與該數比較,若是大於該數,那麼說明這個點所在的列都不存在該數,將這個點左移;若是這個點上的數小於該數,那麼說明這個點所在的行不存在該數,將這個點下移。直到找到與該數相等的點爲止。最壞的狀況是,該數只有一個且在矩陣左下角上,那麼時間複雜度爲O(M-1+N-1)=O(M+N)

//
// Created by zaw on 2018/10/22.
//
#include <stdio.h>
const int rows = 4;
const int cols = 4;

bool findNumInSortedMatrix(int num,int matrix[rows][cols]){
    int i = 0, j = cols - 1;
    while (i <= rows - 1 && j <= cols - 1) {
        if (matrix[i][j] > num) {
            --j;
        } else if (matrix[i][j] < num) {
            ++i;
        } else {
            return true;
        }
    }
    return false;
}

int main(){
    int matrix[rows][cols] = {
            {1, 2, 3, 4},
            {2, 4, 5, 8},
            {3, 6, 7, 9},
            {4, 8, 9, 10}
    };
    if (findNumInSortedMatrix(7, matrix)) {
        printf("find!");
    } else {
        printf("not exist!");
    }
    return 0;
}
複製代碼

島問題

一個矩陣中只有0和1兩種值,每一個位置均可以和本身的上、下、左、右四個位置相連,若是有一片1連在一塊兒,這個部分叫作一個島,求一個矩陣中有多少個島?

好比矩陣:

1 0 1
0 1 0
1 1 1

就有3個島。

分析:咱們能夠遍歷矩陣中的每一個位置,若是遇到1就將與其相連的一片1都感染成2,並自增島數量。

public class IslandNum {
	
	public static int getIslandNums(int matrix[][]){
		int res = 0 ;
		for(int i = 0 ; i < matrix.length ; i++){
			for(int j = 0 ; j < matrix[i].length ; j++){
				if(matrix[i][j] == 1){
					res++;
					infect(matrix , i , j);
				}
			}
		}
		return res;
	}

	public static void infect(int matrix[][], int i ,int j){
		if(i < 0 || i >= matrix.length || j < 0 || j >= matrix[i].length || matrix[i][j] != 1){
			return;
		}
		matrix[i][j] = 2;
		infect(matrix , i-1 , j);
		infect(matrix , i+1 , j);
		infect(matrix , i , j-1);
		infect(matrix , i , j+1);
	}

	public static void main(String[] args){
		int matrix[][] = {
			{1,0,0,1,0,1},
			{0,1,1,0,0,0},
			{1,0,0,0,1,1},
			{1,1,1,1,1,1}
		};
		System.out.println(getIslandNums(matrix));
	}
}
複製代碼

經典結構和算法

字符串

KMP算法

KMP算法是由一個問題而引起的:對於一個字符串str(長度爲N)和另外一個字符串match(長度爲M),若是matchstr的子串,請返回其在str第一次出現時的首字母下標,若match不是str的子串則返回-1

最簡單的方法是將str從頭開始遍歷並與match逐次比較,若碰到了不匹配字母則終止這次遍歷轉而從str的第二個字符開始遍歷並與match逐次比較,直到某一次的遍歷每一個字符都與match匹配不然返回-1。易知此種作法的時間複雜度爲O(N*M)

KMP算法則給出求解該問題時間複雜度控制在O(N)的解法。

首先該算法須要對應match建立一個與match長度相同的輔助數組help[match.length],該數組元素表示match某個下標以前的子串的先後綴子串最大匹配長度前綴子串表示一個串中以串首字符開頭的不包含串尾字符的任意個連續字符,後綴子串則表示一個串中以串尾字符結尾的不包括串首字符的任意個連續字符。好比abcd的前綴子串能夠是aababc,但不能是abcd,而abcd的後綴字串能夠是dcdbcd,但不能是abcd。再來講一下help數組,對於char match[]="abc1abc2"來講,有help[7]=3,由於match[7]='2',所以match下標在7以前的子串abc1abc的前綴子串和後綴子串相同的狀況下,前綴子串的最大長度爲3(即前綴字串和後綴字串都取abc);又如match="aaaab",有help[4]=3(前綴子串和後綴子串最大匹配長度當二者爲aaa時取得),相應的有help[3]=2help[2]=1

假設當要尋找的子串matchhelp數組找到以後(對於一個串的help數組的求法在介紹完KMP算法以後再詳細說明)。就能夠進行KMP算法求解此問題了。KMP算法的邏輯(結論)是,對於stri~(i+k)部分(ii+k均爲str的合法下標)和match0~k部分(kmatch的合法下標),若是有str[i]=match[0]str[i+1]=match[1]……str[i+k-1]=match[k-1],但str[i+k]!=[k],那麼str的下標不用從i+k變爲i+1從新比較,只需將子串str[0]~str[i+k-1]的最大匹配前綴子串的後一個字符cn從新與str[i+k]向後依次比較,後面若是又遇到了不匹配的字符重複此操做便可:

當遇到不匹配字符時,常規的作法是將str的遍歷下標sIndex移到i+1的位置並將match的遍歷下標mIndex移到0再依次比較,這種作法並無利用上一輪的比較信息(對下一輪的比較沒有任何優化)。而KMP算法則不是這樣,當遇到不匹配的字符str[i+k]match[k]時,str的遍歷指針sIndex=i+k不用動,將match右滑並將其遍歷指針mIndex打到子串match[0]~match[k-1]的最大匹配前綴子串的後一個下標n的位置。而後sIndexi+k開始,mIndexn開始,依次向後比較,若再遇到不匹配的數則重複此過程。

對應代碼以下:

void length(char* str){
  if(str==NULL)
    return -1;
  int len=0;
  while(*(str++)!='\0'){
    len++;
  }
  return len;
}

int getIndexOf(char* str,char* m){
    int slen = length(str) , mlen = length(m);
    if(mlen > slen)
        return -1;
    int help[mlen];
    getHelpArr(str,help);
    int i=0,j=0;	//sIndex,mIndex
    while(i < slen && j < mlen){
        if(str[i] == m[j]){
            i++;
            j++;
        }else if(help[j] != -1){
            j = help[j];    //mIndex -> cn's index
        }else{	//the first char is not match,move the sIndex
            i++;    
        }
    }
    return j == mlen ? i - mlen : -1;
}
複製代碼

能夠發現KMP算法中str的遍歷指針並無回溯這個動做(只向後移動),當完成匹配時sIndex的移動次數小於N,不然sIndex移動到串尾也會終止循環,因此while對應的匹配過程的時間複雜度爲O(N)(if(help[j] != -1){ j = help[j] }的執行次數只會是常數次,所以能夠忽略)。

下面只要解決如何求解一個串的help數組,此問題就解決了。help數組要從前到後求解,直接求help[n]是很難有所頭緒的。當串match長度mlen=1時,規定help[0]=-1。當mlen=2時,去掉match[1]以後只剩下match[0],最大匹配子串長度爲0(由於前綴子串不能包含串尾字符,後綴子串不能包含串首字符),即help[1]=0。當mlen>2時,help[n](n>=2)均可以推算出來:

如上圖所示,若是咱們知道了help[n-1],那麼help[n]的求解有兩種狀況:若是match[cn]=match[n-1],那麼由a區域與b區域(a、b爲子串match[0~n-2]的最大匹配前綴子串和後綴字串)相同可知help[n]=help[n-1]+1;若是match[cn]!=match[n-1],那麼求a區域中下一個能和b區域後綴子串中匹配的較大的一個,即a區域的最大匹配前綴字串c區域,將match[n-1]和c區域的後一個位置(cn')上的字符比較,若是相等則help[n]等於c區域的長度+1,而c區域的長度就是help[cn]help數組的定義如此);若是不等則將cn打到cn'的位置繼續和match[n-1]比較,直到cn被打到0爲止(即help[cn]=-1爲止),那麼此時help[n]=0

對應代碼以下:

int* getHelpArr(char* s,int help[]){
    if(s==NULL)
        return NULL;
    int slen = length(s);
    help[0]=-1;
    help[1]=0;
    int index = 2;//help數組從第三個元素開始的元素值須要依次推算
    int cn = 0;		//推算help[2]時,help[1]=0,即s[1]以前的字符組成的串中不存在最大匹配先後子串,那麼cn做爲最大匹配前綴子串的後一個下標天然就是0了
    while(index < slen){
        if(s[index-1] == s[cn]){	//if match[n-1] == match[cn]
            help[index] = help[index-1] + 1;
            index++;
            cn++;
        }else if(help[cn] == -1){	//cn reach 0
            help[index]=0;
            index++;
            cn++;
        }else{
            cn = help[cn];	//set cn to cn' and continue calculate help[index]
        }
    }
    return help;
}
複製代碼

那麼這個求解help數組的過程的時間複雜度如何計算呢?仔細觀察剋制while循環中僅涉及到indexcn這兩個變量的變化:

第一個if分支 第二個if分支 第三個if分支
index 增大 增大 不變
index-cn 不變 不變 增大

能夠發現while循環執行一次不是index增大就是index-cn增大,而index < slenindex - cn < slen,即index最多自增Mmatch串的長度)次 ,index-cn最多增長M次,如此while最多執行M+M次,即時間複雜爲O(2M)=O(M)

綜上所述,使用KMP求解此問題的時間複雜度爲O(M)(求解matchhelp數組的時間複雜度)+O(N)(匹配的時間複雜度)=O(N)(由於N > M)。

KMP算法的應用

  1. 判斷一個二叉樹是不是另外一棵二叉樹的子樹(即某棵樹的結構和數據狀態和另外一棵二叉樹的子樹樣)。

    思路:若是這棵樹的序列化串是另外一棵樹的序列化串的子串,那麼前者一定是後者的子樹。

前綴樹(字典樹)

前綴樹的介紹

前綴樹是一種存儲字符串的高效容器,基於此結構的操做有:

  • insert插入一個字符串到容器中

  • search容器中是否存在某字符串,返回該字符串進入到容器的次數,沒有則返回0

  • delete將某個字符串進入到容器的次數減1

  • prefixNumber返回全部插入操做中,以某個串爲前綴的字符串出現的次數

設計思路:該結構的重點實如今於存儲。前綴樹以字符爲存儲單位,將其存儲在結點之間的樹枝上而非結點上,如插入字符串abc以後前綴樹以下:

每次插入串都要從頭結點開始,遍歷串中的字符依次向下「鋪路」,如上圖中的abc3條路。對於每一個結點而言,它能夠向下鋪a~z26條不一樣的路,假如來到某個結點後,它要向下鋪的路(取決於遍歷到哪一個字符來了)被以前插入串的過程鋪過了那麼就能夠直接走這條路去往下一個結點,不然就要先鋪路再去往下一個結點。如再插入串abdebcd的前綴樹將以下所示:

根據前綴樹的searchprefixNumber兩個操做,咱們還須要在每次鋪路後記錄如下每一個結點通過的次數(across),以及每次插入操做每一個結點做爲終點結點的次數(end)。

前綴樹的實現

前綴樹的實現示例:

public class TrieTree {

  public static class TrieNode {
    public int across;
    public int end;
    public TrieNode[] paths;

    public TrieNode() {
      super();
      across = 0;
      end = 0;
      paths = new TrieNode[26];
    }
  }

  private TrieNode root;

  public TrieTree() {
    super();
    root = new TrieNode();
  }

  //向樹中插入一個字符串
  public void insert(String str) {
    if (str == null || str.length() == 0) {
      return;
    }
    char chs[] = str.toCharArray();
    TrieNode cur = root;
    for (char ch : chs) {
      int index = ch - 'a';
      if (cur.paths[index] == null) {
        cur.paths[index] = new TrieNode();
      }
      cur = cur.paths[index];
      cur.across++;
    }
    cur.end++;
  }

  //查詢某個字符串插入的次數
  public int search(String str) {
    if (str == null || str.length() == 0) {
      return 0;
    }
    char chs[] = str.toCharArray();
    TrieNode cur = root;
    for (char ch : chs) {
      int index = ch - 'a';
      if (cur.paths[index] == null) {
        return 0;
      }else{
        cur = cur.paths[index];
      }
    }
    return cur.end;
  }

  //刪除一次插入過的某個字符串
  public void delete(String str) {
    if (search(str) > 0) {
      char chs[] = str.toCharArray();
      TrieNode cur = root;
      for (char ch : chs) {
        int index = ch - 'a';
        if (--cur.paths[index].across == 0) {
          cur.paths[index] = null;
          return;
        }
        cur = cur.paths[index];
      }
      cur.end--;
    }
  }

	//查詢全部插入的字符串中,以prefix爲前綴的有多少個
  public int prefixNumber(String prefix) {
    if (prefix == null || prefix.length() == 0) {
      return 0;
    }
    char chs[] = prefix.toCharArray();
    TrieNode cur = root;
    for (char ch : chs) {
      int index = ch - 'a';
      if (cur.paths[index] == null) {
        return 0;
      }else{
        cur = cur.paths[index];
      }
    }
    return cur.across;
  }

  public static void main(String[] args) {
    TrieTree tree = new TrieTree();
    tree.insert("abc");
    tree.insert("abde");
    tree.insert("bcd");
    System.out.println(tree.search("abc"));	//1
    System.out.println(tree.prefixNumber("ab"));	//2
  }
}
複製代碼

前綴樹的相關問題

一個字符串類型的數組arr1,另外一個字符串類型的數組arr2:

  • arr2中有哪些字符,是arr1中出現的?請打印
  • arr2中有哪些字符,是做爲arr1中某個字符串前綴出現的?請打印
  • arr2中有哪些字符,是做爲arr1中某個字符串前綴出現的?請打印arr2中出現次數最大的前綴。

數組

冒泡排序

冒泡排序的核心是從頭遍歷序列。以升序排列爲例:將第一個元素和第二個元素比較,若前者大於後者,則交換二者的位置,再將第二個元素與第三個元素比較,若前者大於後者則交換二者位置,以此類推直到倒數第二個元素與最後一個元素比較,若前者大於後者,則交換二者位置。這樣一輪比較下來將會把序列中最大的元素移至序列末尾,這樣就安排好了最大數的位置,接下來只需對剩下的(n-1)個元素,重複上述操做便可。

void swap(int *a, int *b){
  int temp = *a;
  *a = *b;
  *b = temp;
}

void bubbleSort(int arr[], int length) {
  if(arr==NULL || length<=1){
    return;
  }
  for (int i = length-1; i > 0; i--) {	//只需比較(length-1)輪
    for (int j = 0; j < i; ++j) {
      if (arr[j] > arr[j + 1]) {
        swap(&arr[j], &arr[j + 1]);
      }
    }
  }
}
複製代碼

該算法的時間複雜度爲n+(n-1)+...+1,很明顯是一個等差數列,由(首項+末項)*項數/2求其和爲(n+1)n/2,可知時間複雜度爲O(n^2)

選擇排序

以升序排序爲例:找到最小數的下標minIndex,將其與第一個數交換,接着對子序列(1-n)重複該操做,直到子序列只含一個元素爲止。(即選出最小的數放到第一個位置,該數安排好了,再對剩下的數選出最小的放到第二個位置,以此類推)

void selectionSort(int arr[], int length) {
  for (int i = 0; i < length-1; ++i) {    //要進行n-1次選擇,選出n-1個數分別放在前n-1個位置上
    if(arr==NULL || length<=1){
      return;
    }
    int minIndex = i;	//記錄較小數的下標
    for (int j = i+1; j < length; ++j) {
      if (arr[minIndex] > arr[j]) {
        minIndex = j;
      }
    }
    if (minIndex != i) {
      swap(&arr[minIndex],&arr[i]);
    }
  }
}
複製代碼

一樣,不可貴出該算法的時間複雜度(big o)爲O(n^2)(n-1+n-2+n-3+…+1)

插入排序

插入排序的過程能夠聯想到打撲克時揭一張牌而後將其到手中有序紙牌的合適位置上。好比我如今手上的牌是七、八、九、J、Q、K,這時揭了一張10,我須要將其依次與K、Q、J、九、八、7比較,當比到9時發現大於9,因而將其插入到9以後。對於一個無序序列,能夠將其當作一摞待揭的牌,首先將首元素揭起來,由於揭以前手上無牌,所以這次揭牌無需比較,此後每揭一次牌都須要進行上述的插牌過程,當揭完以後,手上的握牌順序就對應着該序列的有序形式。

void swap(int *a, int *b){
    int temp = *a;
    *a = *b;
    *b = temp;
}
void insertionSort(int arr[], int length){
    if(arr==NULL || length<=1){
        return;
    }
    for (int i = 1; i < length; ++i) {		//第一張牌無需插入,直接入手,後續揭牌需比較而後插入,所以從第二個元素開始遍歷(插牌)
      	//將新揭的牌與手上的逐次比較,若小於則交換,不然中止,比較完了還沒遇到更小的也中止
        for (int j = i - 1; j >= 0 || arr[j] <= arr[j + 1]; j--) {
            if (arr[j] > arr[j + 1]) {
                swap(&arr[j], &arr[j + 1]);
            }
        }
    }
}
複製代碼

插入排序的big o該如何計算?能夠發現若是序列有序,那麼該算法的big o爲O(n),由於只是遍歷了一次序列(這時最好狀況);若是序列降序排列,那麼該算法的big o爲O(n^2)(每次插入前的比較交換加起來要:1+2+…+n-1)(最壞狀況)。**通常應用場景中都是按算法的最壞狀況來考量算法的效率的,由於你作出來的應用要可以承受最壞狀況。**即該算法的big o爲O(n^2)

歸併排序

歸併排序的核心思想是先讓序列的左半部分有序、再讓序列的右半部分有序,最後從兩個子序列(左右兩半)從頭開始逐次比較,往輔助序列中填較小的數。

以序列{2,1,4,3}爲例,歸併排序的過程大體以下:

算法代碼示例:

void merge(int arr[],int helpArr[], int startIndex, int midIndex,int endIndex) {
    int L = startIndex, R = midIndex + 1, i = startIndex;
    while (L <= midIndex && R <= endIndex) { //只要沒有指針沒越界就逐次比較
        helpArr[i++] = arr[L] < arr[R] ? arr[L++] : arr[R++];
    }
    while (L != midIndex + 1) {
        helpArr[i++] = arr[L++];
    }
    while (R != endIndex + 1) {
        helpArr[i++] = arr[R++];
    }
    for (i = startIndex; i <= endIndex; i++) {
        arr[i] = helpArr[i];
    }
}

void mergeSort(int arr[],int helpArr[], int startIndex, int endIndex) {
    int midIndex;
    if (startIndex < endIndex) {  //當子序列只含一個元素時,再也不進行此子過程
      	//(endIndex+startIndex)/2可能會致使int溢出,下面求中位數的作法更安全
        midIndex = startIndex + ((endIndex - startIndex) >> 1);
        mergeSort(arr, helpArr, startIndex, midIndex);        //對左半部分排序
        mergeSort(arr, helpArr, midIndex + 1, endIndex);      //對右半部分排序
        merge(arr, helpArr, startIndex, midIndex, endIndex);  //使總體有序
    }
}

int main(){
    int arr[] = {9, 1, 3, 4, 7, 6, 5};
    travels(arr, 7);//遍歷打印
    int helpArr[7];
    mergeSort(arr, helpArr, 0, 7);
    travels(arr, 7);

    return 0;
}
複製代碼

此算法的核心就是第2四、2五、26這三行。第26行應該不難理解,就是使用兩個指針L、R外加一個輔助數組,將兩個序列有序地併入輔助數組。但爲何2四、25行執行事後數組左右兩半部分就分別有序了呢?這就又牽扯到了歸併排序的核心思想:先讓一個序列左右兩半部分有序,而後再併入使總體有序。所以2四、25是對左右兩半部分分別遞歸執行歸併排序,直到某次遞歸時左右兩半部分均爲一個元素時遞歸終止。當一個序列只含兩個元素時,調用mergeSort會發現2四、25行是無效操做,直接執行merge。就像上圖所示,兩行遞歸完畢後,左右兩半部分都會變得有序。

當一個遞歸過程比較複雜時(不像遞歸求階乘那樣一幕瞭然),咱們能夠列舉簡短樣本進行分析。

對於這樣複雜的遞歸行爲,千萬不要想着追溯整個遞歸過程,只需分析第一步要作的事(好比此例中第一步要作的是就是mergeSort函數所呈現出來的那樣:對左半部分排序、對右半部分排序、最後併入,你先無論是怎麼排序的,不要被2四、25行的mergeSort給帶進去了)和遞歸終止的條件(好比此例中是``startIndex>=endIndex`,即要排序的序列只有一個元素時)。

歸併排序的時間複雜度是O(nlogn),額外空間複雜度是O(n)

根據Master公式(本文 小技巧一節中有講到)可得T(n)=2T(n/2)+O(n),第一個2的含義是子過程(對子序列進行歸併排序)要執行兩次,第二個2的含義是子過程樣本量佔一半(由於分紅了左右兩半部分),最後O(n)表示左右有序以後進行的併入操做爲O(n+n)=O(n)(L、R指針移動次數總和爲n,將輔助數組覆蓋源數組爲n),符合T(n)=aT(n/b)+O(n^d),經計算該算法的時間複雜度爲O(nlogn)

小和問題

在一個數組中,每個數左邊比當前數小的數累加起來,叫作這個數組的小和。求一個數組的小和。例如:

對於數組[1,3,4,2,5]
1左邊比1小的數,沒有;
3左邊比3小的數,1;
4左邊比4小的數,一、3;
2左邊比2小的數,1;
5左邊比5小的數,一、三、四、2;
因此小和爲1+1+3+1+1+3+4+2=16
複製代碼

簡單的作法就是遍歷一遍數組,將當前遍歷的數與該數以前數比較並記錄小於該數的數。易知其時間複雜度爲O(n^2)(0+1+2+……+n-1)。

更優化的作法是利用歸併排序的併入邏輯

對應代碼:

int merge(int arr[],int helpArr[], int startIndex, int midIndex,int endIndex) {
    int L = startIndex, R = midIndex + 1, i = startIndex;
    int res=0;
    while (L <= midIndex && R <= endIndex ) { //只要沒有指針沒越界就逐次比較
        res += arr[L] < arr[R] ? arr[L] * (endIndex - R + 1) : 0;
        helpArr[i++] = arr[L] < arr[R] ? arr[L++] : arr[R++];
    }
    while (L != midIndex + 1) {
        helpArr[i++] = arr[L++];
    }
    while (R != endIndex + 1) {
        helpArr[i++] = arr[R++];
    }
    for (i = startIndex; i <= endIndex; i++) {
        arr[i] = helpArr[i];
    }
    return res;
}

int mergeSort(int arr[],int helpArr[], int startIndex, int endIndex) {
    int midIndex;
    if (startIndex < endIndex) {  //當子序列只含一個元素時,再也不進行此子過程
        midIndex = startIndex + ((endIndex - startIndex) >> 1);
        return mergeSort(arr, helpArr, startIndex, midIndex) +        //對左半部分排序
               mergeSort(arr, helpArr, midIndex + 1, endIndex) +     //對右半部分排序
               merge(arr, helpArr, startIndex, midIndex, endIndex);  //使總體有序
    }
    return 0;	//一個元素時不存在小和
}

int main(){
    int arr[] = {1,3,4,2,5};
    int helpArr[5];
    printf("small_sum:%d\n",mergeSort(arr, helpArr, 0, 4)) ;
    return 0;
}
複製代碼

該算法在歸併排序的基礎上作了略微改動,即merge中添加了變量res記錄每次併入操做應該累加的小和、mergeSort則將每次併入應該累加的小和彙總。此種作法的複雜度與歸併排序的相同,優於遍歷的作法。能夠理解,依次求每一個數的小和過程當中有不少比較是重複的,而利用歸併排序求小和時利用了併入的兩個序列分別有序的特性省去了沒必要要的比較,如134併入25時,2>1直接推出2後面的數都>1,所以直接1*(endIndex-indexOf(2)+1)便可。這在樣本量不大的狀況下看不出來優化的效果,試想一下若是樣本量爲2^32,那麼依照前者求小和O(n^2)可知時間複雜度爲O(21億的平方),而歸併排序求小和則只需O(21億*32),足以見得O(n^2)O(nlogn)的優劣。

逆序對問題

在一個數組中,左邊的數若是比右邊的數大,則這兩個數構成一個逆序對,請打印全部逆序對。

這題的思路也能夠利用歸併排序來解決,在併入操做時記錄arr[L]>arr[R]的狀況便可。

快速排序

經典快排

經典快排就是將序列中比尾元素小的移動到序列左邊,比尾元素大的移動到序列右邊,對以該元素爲界的左右兩個子序列(均不包括該元素)重複此操做。

首先咱們要考慮的是對給定的一個數,如何將序列中比該數小的移動到左邊,比該數大的移動到右邊。

思路:利用一個輔助指針small,表明較小數的右邊界(初始指向首元素前一個位置),遍歷序列每次遇到比該數小的數就將其與arr[small+1]交換並右移small,最後將該數與arr[small+1]交換即達到目的。對應算法以下:

void partition(int arr[], int startIndex, int endIndex){
    int small = startIndex - 1;
    for (int i = startIndex; i < endIndex; ++i) {
        if(arr[i] < arr[endIndex]) {
            if (small + 1 != i) {
                swap(arr[++small], arr[i]);
            } else {
                //若是small、i相鄰則不用交換
                small++;
            }
        }
    }
    swap(arr[++small], arr[endIndex]);
}
int main(){
    int arr[] = {1, 2, 3, 4, 6, 7, 8, 5};
    travles(arr, 8);//1 2 3 4 6 7 8 5
    partition(arr, 0, 7);
    travles(arr, 8);//1 2 3 4 5 7 8 6
    return 0;
}
複製代碼

接着就是快排的遞歸邏輯:對1 2 3 4 6 7 8 5序列partition以後,去除以前的比較參數5,對剩下的子序列1234786繼續partition,直到子序列爲一個元素爲止:

int partition(int arr[], int startIndex, int endIndex){
    int small = startIndex - 1;
    for (int i = startIndex; i < endIndex; ++i) {
        if(arr[i] < arr[endIndex]) {
            if (small + 1 != i) {
                swap(arr[++small], arr[i]);
            } else {
                //若是small、i相鄰則不用交換
                small++;
            }
        }
    }
    swap(arr[++small], arr[endIndex]);
    return small;
}

void quickSort(int arr[], int startIndex, int endIndex) {
    if (startIndex > endIndex) {
        return;
    }
    int index = partition(arr, startIndex, endIndex);
    quickSort(arr, startIndex, index - 1);
    quickSort(arr, index + 1, endIndex);
}
int main(){
    int arr[] = {1, 5, 6, 2, 7, 3, 8, 0};
    travles(arr, 8);	//1 5 6 2 7 3 8 0
    quickSort(arr, 0,7);
    travles(arr, 8);	//0 1 2 3 5 6 7 8
    return 0;
}
複製代碼

經典排序的時間複雜度與數據情況有關,若是每一次partition時,尾元素都是序列中最大或最小的,那麼去除該元素序列並未如咱們劃分爲樣本量相同的左右兩個子序列,而是隻安排好了一個元素(就是去掉的那個元素),這樣的話時間複雜度就是O(n-1+n-2+……+1)=O(n^2);但若是每一次partition時,都將序列分紅了兩個樣本量相差無幾的左右兩個子序列,那麼時間複雜度就是O(nlogn)(使用Master公式求解)。

由荷蘭國旗問題引起對經典快排的改進

能夠發現這裏partition的過程與荷蘭國旗問題中的partition十分類似,可否之後者的partition實現經典快排呢?咱們來試一下:

int* partition(int arr[], int startIndex, int endIndex){ ;
    int small = startIndex - 1, great = endIndex + 1, i = startIndex;
    while (i <= great - 1) {
        if (arr[i] < arr[endIndex]) {
            swap(arr[++small], arr[i++]);
        } else if (arr[i] > arr[endIndex]){
            swap(arr[--great], arr[i]);
        } else {
            i++;
        }
    }
    int range[] = {small, great};
    return range;
}

void quickSort(int arr[], int startIndex, int endIndex) {
    if (startIndex > endIndex) {
        return;
    }
    int* range = partition(arr, startIndex, endIndex);
    quickSort(arr, startIndex, range[0]);
    quickSort(arr, range[1], endIndex);
}

int main(){
    int arr[] = {1, 5, 6, 2, 7, 3, 8, 0};
    travles(arr, 8);	//1 5 6 2 7 3 8 0
    quickSort(arr, 0,7);
    travles(arr, 8);	//0 1 2 3 5 6 7 8
    return 0;
}
複製代碼

比較一下經典排序和使用荷蘭國旗問題改進後的經典排序,不難發現,後者一次partition能去除一個以上的元素(等於arr[endIndex]的區域),而前者每次partition只能去除一個元素,這裏的去除至關於安排(排序)好了對應元素的位置。所以後者比經典排序更優,可是優化不大,只是常數時間內的優化,實質上的效率仍是要看數據情況(最後的狀況爲O(nlogn),最壞的狀況爲O(n^2))。

隨機快排——O(nlogn)

上面談到了快排的短板是依賴數據情況,那麼咱們有沒有辦法消除這個依賴,讓他成爲真正的O(nlogn)呢?

事實上,爲了讓算法中的操做不依託於數據情況(如快排中每一次partition取尾元素做爲比較,這就沒有規避樣本的數據情況,若是尾元素是最大或最小值就成了最壞狀況)經常有兩種作法:

一、使用隨機取數

二、將樣本數據哈希打亂

隨機快排就是採用上了上述第一種解決方案,在每一輪的partition中隨機選擇序列中的一個數做爲要比較的數:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

void swap(int &a, int &b){
    int temp = a;
    a = b;
    b = temp;
}

//產生[startIndex,endIndex]之間的隨機整數
int randomInRange(int startIndex,int endIndex){
    return rand() % (endIndex - startIndex + 1) + startIndex;
}

int* partition(int arr[], int startIndex, int endIndex){ ;
    int small = startIndex - 1, great = endIndex + 1, i = startIndex;
    int randomNum = arr[randomInRange(startIndex, endIndex)];
    while (i <= great - 1) {
        if (arr[i] < randomNum) {
            swap(arr[++small], arr[i++]);
        } else if (arr[i] > randomNum){
            swap(arr[--great], arr[i]);
        } else {
            i++;
        }
    }
    int range[] = {small, great};
    return range;
}

void quickSort(int arr[], int startIndex, int endIndex) {
    if (startIndex > endIndex) {
        return;
    }
    int* range = partition(arr, startIndex, endIndex);
    quickSort(arr, startIndex, range[0]);
    quickSort(arr, range[1], endIndex);
}

void travles(int dataArr[], int length){
    for (int i = 0; i < length; ++i) {
        printf("%d ", dataArr[i]);
    }
    printf("\n");
}

int main(){
    srand(time(NULL));//此後調用rand()時將以調用時的時間爲隨機數種子
    int arr[] = {9,7,1,3,2,6,8,4,5};
    travles(arr, 9);
    quickSort(arr, 0,8);
    travles(arr, 9);
    return 0;
}
複製代碼

觀察比較代碼能夠發現隨機快排只不過是在partition時隨機選出一個下標上的數做爲比較對象,從而避免了每一輪選擇尾元素會受數據情況的影響的問題。

那麼隨機快排的時間複雜度又爲多少呢?

經數學論證,因爲每一輪partition選出的做爲比較對象的數是隨機的,即序列中的每一個數都有1/n的機率被選上,那麼該算法時間複雜度爲機率事件,經數學論證該算法的數學指望O(nlogn)。雖說是數學指望,但在實際工程中,經常就把隨機快排的時間複雜度當作O(nlog)

堆排序

什麼是堆

堆結構就是將一顆徹底二叉樹映射到數組中的一種存儲方式:

大根堆和小根堆

當堆的每一顆子樹(包括樹自己)的最大值就是其結點時稱爲大根堆;相反,當堆的每一顆子樹的最小值就是其根結點時稱爲小根堆。其中大根堆的應用較爲普遍,是一種很重要的數據結構。

heapInsert和heapify

大根堆最重要的兩個操做就是heapInsertheapify,前者是當一個元素加入到大根堆時應該自底向上與其父結點比較,若大於父結點則交換;後者是當堆中某個結點的數值發生變化時,應不斷向下與其孩子結點中的最大值比較,若小於則交換。下面是對應的代碼:

//index以前的序列符合大根堆排序,將index位置的元素加入堆結構,但不能破壞大根堆的特性
void heapInsert(int arr[],int index){
    while (arr[index] > arr[(index - 1) / 2]) { //當該結點大於父結點時
        swap(arr[index], arr[(index - 1) / 2]);
        index = (index - 1) / 2;    //繼續向上比較
    }
}

//數組中下標從0到heapSize符合大根堆排序
//index位置的值發生了變化,從新調整堆結構爲大根堆
//heapSize指的是數組中符合大根堆排序的範圍而不是數組長度,最大爲數組長度,最小爲0 
void heapify(int arr[], int heapSize, int index){
    int leftChild = index * 2 + 1;
    while (leftChild < heapSize) {  //當該結點有左孩子時
        int greatOne = leftChild + 1 < heapSize && arr[leftChild + 1] > arr[leftChild] ?
                leftChild + 1 : leftChild;  //只有當右孩子存在且大於左孩子時,最大值是右孩子,不然是左孩子
        greatOne = arr[greatOne] > arr[index] ? greatOne : index;//將父結點與最大孩子結點比較,肯定最大值
        if (greatOne == index) {
            //若是最大值是自己,則不用繼續向下比較
            break;
        }
        swap(arr[index], arr[greatOne]);

        //next turn下一輪
        index = greatOne;
        leftChild = index * 2 + 1;
    }
}
複製代碼

創建大根堆

void buildBigRootHeap(int arr[],int length){
    if (arr == NULL || length <= 1) {
        return;
    }
    for (int i = 0; i < length; ++i) {
        heapInsert(arr, i);
    }
}
複製代碼

利用heapify排序

前面作了那麼多鋪墊都是爲了創建大根堆,那麼如何利用它來排序呢?

對應代碼實現以下:

void heapSort(int arr[],int length){
    if (arr == NULL || length <= 1) {
        return;
    }
  	//先創建大根堆
    for (int i = 0; i < length; ++i) {
        heapInsert(arr, i);
    }
 	  //循環彈出堆頂元素並heapify
    int heapSize = length;
    swap(arr[0], arr[--heapSize]);//至關於彈出堆頂元素
    while (heapSize > 0) {
        heapify(arr, heapSize, 0);
        swap(arr[0], arr[--heapSize]);
    }
}

int main(){
    int arr[] = {9,7,1,3,6,8,4,2,5};
    heapSort(arr, 9);
    travles(arr, 9);
    return 0;
}
複製代碼

堆排序的優點在於不管是入堆一個元素heapInsert仍是出堆一個元素以後的heapify都不是將整個樣本遍歷一遍(O(n)級別的操做),而是樹層次上的遍歷(O(logn)級別的操做)。

這樣的話堆排序過程當中,創建堆的時間複雜度爲O(nlogn),循環彈出堆頂元素並heapify的時間複雜度爲O(nlogn),整個堆排序的時間複雜度爲O(nlogn),額外空間複雜度爲O(1)

優先級隊列結構(好比Java中的PriorityQueue)就是堆結構。

排序算法的穩定性

排序算法的穩定性指的是排序先後是否維持值相同的元素在序列中的相對次序。如序列271532,在排序過程當中若是能維持第一次出現的2在第二次出現的2的前面,那麼該排序算法可以保證穩定性。首先咱們來分析一下前面所講排序算法的穩定性,再來談談穩定性的意義。

  • 冒泡排序。能夠保證穩定性,只需在比較相鄰兩個數時只在後一個數比前一個數大的狀況下才交換位置便可。
  • 選擇排序。沒法保證穩定性,好比序列926532,在第一輪maxIndex的選擇出來以後(maxIndex=0),第二次出現的2(尾元素)將與9交換位置,那麼兩個2的相對次序就發生了變化,而這個交換是否會影響穩定性在咱們coding的時候是不可預測的。
  • 插入排序。能夠保證穩定性,每次插入一個數到有序序列中時,遇到比它大的就替換,不然不替換。這樣的話,值相同的元素,後面插入的就總在前面插入的後面了。
  • 歸併排序。能夠保證穩定性,在左右兩半子序列排好序後的merge過程當中,比較大小時若是相等,那麼優先插入左子序列中的數。
  • 快排。不能保證穩定性,由於partition的過程會將比num小的與small區域的右一個數交換位置,將比num大的與great區域的左一個數交換位置,而smallgreat分居序列兩側,很容易打亂值相同元素的相對次序。
  • 堆排序。不能保證穩定性。二叉樹若是交換位置的結點是相鄰層次的能夠保證穩定性,但堆排序中彈出堆頂元素後的heapify交換的是第一層的結點和最後一層的結點。

維持穩定性通常是爲了知足業務需求。假設下面是一張不一樣廠商下同一款產品的價格和銷售狀況表:

品牌 價格 銷量
三星 1603 92
小米 1603 74
vivo 1604 92

要求先按價格排序,再按銷量排序。若是保證穩定性,那麼排序後應該是這樣的:

品牌 價格 銷量
三星 1603 92
vivo 1604 92
小米 1603 74

即按銷量排序後,銷量相同的兩條記錄會保持以前的按價格排序的狀態,這樣先前的價格排序這個工做就沒白作。

比較器的使用

以前所講的一些算法大都是對基本類型的排序,但實際工程中要排序的對象多是沒法預測的,那麼如何實現一個通用的排序算法以應對呢?事實上,以前的排序均可以歸類爲基於比較的排序。也就是說咱們只須要對要比較的對象實現一個比較器,而後排序算法基於比較器來排序,這樣算法和具體要排序的對象之間就解耦了。之後在排序以前,基於要排序的對象實現一個比較器(定義瞭如何比較對象大小的邏輯),而後將比較器丟給排序算法便可,這樣就實現了複用。

Java(本人學的是Java方向)中,這個比較器就是Comparator接口,咱們須要實現其中的compare方法,對於要排序的對象集合定義一個比較大小的邏輯,而後在構造用來添加這類對象的有序容器時傳入這個構造器便可。封裝好的容器會在容器元素髮生改變時使用咱們的比較器來從新組織這些元素。

import lombok.AllArgsConstructor;
import lombok.Data;
import java.util.PriorityQueue;
import java.util.Comparator;

public class ComparatorTest {
    
    @Data
    @AllArgsConstructor
    static class Student {
        private long id;
        private String name;
        private double score;
    }

    static class IdAscendingComparator implements Comparator<Student> {
        /** * 底層排序算法對兩個元素比較時會調用這個方法 * @param o1 * @param o2 * @return 若返回正數則認爲o1<o2,返回0則認爲o1=o2,不然認爲o1>o2 */
        @Override
        public int compare(Student o1, Student o2) {
            return o1.getId() < o2.getId() ? -1 : 1;
        }
    }

    public static void main(String[] args) {
        //大根堆
        PriorityQueue heap = new PriorityQueue(new IdAscendingComparator());
        Student zhangsan = new Student(1000, "zhangsan", 50);
        Student lisi = new Student(999, "lisi", 60);
        Student wangwu = new Student(1001, "wangwu", 50);
        heap.add(zhangsan);
        heap.add(lisi);
        heap.add(wangwu);
        while (!heap.isEmpty()) {
            System.out.println(heap.poll());//彈出並返回堆頂元素
        }
    }
}
複製代碼

還有TreeSet等,都是在構造是傳入比較器,不然將直接根據元素的值(Java中引用類型變量的值爲地址,比較將毫無心義)來比較,這裏就不一一列舉了。

有關排序問題的補充

  1. 歸併排序能夠作到額外空間複雜度爲O(1),可是比較難,感興趣的能夠搜 歸併排序 內部緩存法
  2. 快速排序能夠作到保證穩定性,可是很難,能夠搜01 stable sort(論文)
  3. 有一道題是:是奇數放到數組左邊,是偶數放到數組右邊,還要求奇數和奇數之間、偶數和偶數之間的原始相對次序不變。這道題和歸併排序一模一樣,只不過歸併排序是將arr[length-1]arr[randomIndex]做爲比較的標準,而這道題是將是否能整除2做爲比較的標準,這類問題都同稱爲o1 sort,要使這類問題作到穩定性,要看01 stable sort這篇論文。

工程中的綜合排序算法

實際工程中的排序算法通常會將 歸併排序插入排序快速排序綜合起來,集你們之所長來應對不一樣的場景要求:

  • 當要排序的元素爲基本數據類型且元素個數較少時,直接使用 插入排序。由於在樣本規模較小時(好比60),O(NlogN)的優點並不明顯甚至不及O(N^2),而在O(N^2)的算法中,插入排序的常數時間操做最少。
  • 當要排序的元素爲對象數據類型(包含若干字段),爲保證穩定性將採用 歸併排序
  • 當要排序的元素爲基本數據類型且樣本規模較大時,將採用 快速排序

桶排序

上一節中所講的都是基於比較的排序,也即經過比較肯定每一個元素所處的位置。那麼能不能不比較而實現排序呢?這就涉及到了 桶排序 這個方法論:準備一些桶,將序列中的元素按某些規則放入翻入對應的桶中,最後根據既定的規則依次倒出桶中的元素。

非基於比較的排序,與被排序的樣本的實際數據情況有很大關係,因此在實際中並不經常使用。

計數排序

計數排序是 桶排序 方法論的一種實現,即準備一個與序列中元素的數據範圍大小相同的數組,而後遍歷序列,將遇到的元素做爲數組的下標並將該位置上的數加1。例如某序列元素值在0~100之間,請設計一個算法對其排序,要求時間複雜度爲O(N)

#include <stdio.h>
void countSort(int arr[],int length){
    int bucketArr[101];
    int i;
    for(i = 0 ; i <= 100 ; i++){
        bucketArr[i]=0;	//init buckets
    }
    for(i = 0 ; i < length ; i++){
        bucketArr[arr[i]]++;	//put into buckets
    }
    int count, j=0;
    for(i = 0 ; i <= 100 ; i++) {
        if (bucketArr[i] != 0) { //pour out
            count = bucketArr[i];
            while (count-- > 0) {
                arr[j++] = i;
            }
        }
    }
}

void travels(int arr[], int length){
    for (int i = 0; i < length; ++i) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

int main(){
    int arr[] = {9, 2, 1, 4, 5, 2, 1, 6, 3, 8, 1, 2};
    travels(arr, 12);//9 2 1 4 5 2 1 6 3 8 1 2
    countSort(arr, 12);
    travels(arr, 12);//1 1 1 2 2 2 3 4 5 6 8 9
    return 0;
}
複製代碼

若是下次面試官問你有沒有事件複雜度比O(N)更優的排序算法時,不要忘了計數排序哦!!!

補充問題

  1. 給定一個數組,求若是排序後,相鄰兩數的最大值,要求時間複雜度爲O(N),且要求不能用非基於比較的排序。

    這道題的思路比較巧妙:首先爲這N個數準備N+1個桶,而後以其中的最小值和最大值爲邊界將數值範圍均分紅N等分,而後遍歷數組將對應範圍類的數放入對應的桶中,下圖以數組長度爲9舉例

    這裏比較難理解的是:

    • 題目問的是求若是排序後,相鄰兩數的最大差值。該算法巧妙的藉助一個空桶(N個數進N+1個桶,必然有一個是空桶),將問題轉向了求兩個相鄰非空桶 (其中可能隔着若干個空桶)之間前桶的最大值和後桶最小值的差值,而無需在乎每一個桶中進了哪些數(只需記錄每一個桶入數的最大值和最小值以及是否有數

    對應代碼以下:

    #include <stdio.h>
    
    //根據要入桶的數和最大最小值獲得對應桶編號
    int getBucketId(int num,int bucketsNum,int min,int max){
        return (num - min) * bucketsNum / (max - min);
    }
    
    int max(int a, int b){
        return a > b ? a : b;
    }
    
    int min(int a, int b){
        return a < b ? a : b;
    }
    
    int getMaxGap(int arr[], int length) {
        if (arr == NULL || length < 2) {
            return -1;
        }
        int maxValue = -999999, minValue = 999999;
        int i;
        //找出最大最小值
        for (i = 0; i < length; ++i) {
            maxValue = max(maxValue, arr[i]);
            minValue = min(minValue, arr[i]);
        }
        //記錄每一個桶的最大最小值以及是否有數,初始時每一個桶都沒數
        int maxs[length + 1], mins[length + 1];
        bool hasNum[length + 1];
        for (i = 0; i < length + 1; i++) {	
            hasNum[i] = false;
        }
        //put maxValue into the last bucket
        mins[length] = maxs[length] = maxValue;
        hasNum[length] = true;
    
        //iterate the arr
        int bid; //bucket id
        for (i = 0; i < length; i++) {
            if (arr[i] != maxValue) {
                bid = getBucketId(arr[i], length + 1, minValue, maxValue);
              	//若是桶裏沒數,則該數入桶後,最大最小值都是它,不然更新最大最小值
                mins[bid] = !hasNum[bid] ? arr[i] : arr[i] < mins[bid] ? arr[i] : mins[bid];
                maxs[bid] = !hasNum[bid] ? arr[i] : arr[i] > maxs[bid] ? arr[i] : maxs[bid];
                hasNum[bid] = true;
            }
        }
    
        //find the max gap between two nonEmpty buckets
        int res = 0, j = 0;
        for (i = 0; i < length; ++i) {
            j = i + 1;//the next nonEmtpy bucket id
            while (!hasNum[j]) {//the last bucket must has number
                j++;
            }
            res = max(res, (mins[j] - maxs[i]));
        }
    
        return res;
    }
    
    int main(){
        int arr[] = {13, 41, 67, 26, 55, 99, 2, 82, 39, 100};
        printf("%d", getMaxGap(arr, 9));	//17
        return 0;
    }
    複製代碼

鏈表

反轉單鏈表和雙向鏈表

實現反轉單向鏈表和反轉雙向鏈表的函數,要求時間複雜度爲O(N),額外空間複雜度爲O(1)

此題的難點就是反轉一個結點的next指針後,就沒法在該結點經過next指針找到後續的結點了。所以每次反轉以前須要將該結點的後繼結點記錄下來。

#include<stdio.h>
#include<malloc.h>
#define MAX_SIZE 100

struct LinkNode{
	int data;
	LinkNode* next;
};

void init(LinkNode* &head){
	head = (LinkNode*)malloc(sizeof(LinkNode));
	head->next=NULL;
}

void add(int i,LinkNode* head){
	LinkNode* p = (LinkNode*)malloc(sizeof(LinkNode));
	p->data = i;
	p->next =  head->next;
	head->next = p;
}

void printList(LinkNode* head){
	if(head==NULL)
		return;
	LinkNode* p = head->next;
	while(p != NULL){
		printf("%d ",p->data);
		p = p->next;
	}
	printf("\n");
}
複製代碼
#include<stdio.h>
#include "LinkList.cpp"

void reverseList(LinkNode *head){
	if(head == NULL)
		return;
	LinkNode* cur = head->next;
	LinkNode* pre = NULL;
	LinkNode* next = NULL;
	while(cur != NULL){
		next = cur->next;
		cur->next = pre;
		pre = cur;
		cur = next;
	}
	//pre -> end node
	head->next = pre;
	return;
}

int main(){
	LinkNode* head;
	init(head);
	add(1,head);
	add(2,head);
	add(3,head);
	add(4,head);
	
	printList(head);
	reverseList(head);
	printList(head);
}
複製代碼

判斷一個鏈表是否爲迴文結構

請實現一個函數判斷某個單鏈表是不是迴文結構,如1->3->1返回true1->2->2->1返回true2->3->1返回false

咱們能夠利用迴文鏈表先後兩半部分逆序的特色、結合棧先進後出來求解此問題。將鏈表中間結點以前的結點依次壓棧,而後從中間結點的後繼結點開始遍歷鏈表的後半部分,將遍歷的結點與棧彈出的結點比較。

代碼示例以下:

#include<stdio.h>
#include "LinkList.cpp"
#include "SqStack.cpp"

/* 判斷某鏈表是不是迴文結構 一、首先找到鏈表的中間結點(如果偶數個結點則是中間位置的左邊一個結點) 二、使用一個棧將中間結點以前的結點壓棧,而後從中間結點的後一個結點開始從棧中拿出結點比較 */

bool isPalindromeList(LinkNode* head){
	if(head == NULL)
		return false;
		
	LinkNode *slow = head , *fast = head;
	SqStack* stack;
	init(stack);
	
	//fast指針每走兩步,slow指針才走一步 
	while(fast->next != NULL && fast->next->next != NULL){
		fast = fast->next->next;	
		slow = slow->next;	
		push(slow,stack);
	}
	
	//鏈表沒有結點或只有一個結點,不是迴文結構
	if(isEmpty(stack)) 
		return false;
		
	//判斷偶數個結點仍是奇數個結點
	if(fast->next != NULL){	//奇數個結點,slow須要再走一步 
		slow = slow->next;
	}
	
	//從slow的後繼結點開始遍歷鏈表,將每一個結點與棧頂結點比較
	LinkNode* node;
	slow = slow->next;
	while(slow != NULL){
		pop(stack,node);
		//一旦發現有一個結點不一樣就不是迴文結構 
		if(slow->data != node->data)
			return false;
		slow = slow->next;
	} 
	return true;
}

int main(){
	
	LinkNode* head;
	init(head);
	add(2,head);
	add(3,head);
	add(3,head);
	add(2,head);
	printList(head);
	
	if(isPalindromeList(head)){
		printf("是迴文鏈表");
	}else{
		printf("不是迴文鏈表");
	}
	return 0;
}
複製代碼

LinkList.cpp:

#include<stdio.h>
#include<malloc.h>
#define MAX_SIZE 100

struct LinkNode{
	int data;
	LinkNode* next;
};

void init(LinkNode* &head){
	head = (LinkNode*)malloc(sizeof(LinkNode));
	head->next=NULL;
}

void add(int i,LinkNode* head){
	LinkNode* p = (LinkNode*)malloc(sizeof(LinkNode));
	p->data = i;
	p->next =  head->next;
	head->next = p;
}

void printList(LinkNode* head){
	if(head==NULL)
		return;
	LinkNode* p = head->next;
	while(p != NULL){
		printf("%d ",p->data);
		p = p->next;
	}
	printf("\n");
}
複製代碼

SqStack:

#include<stdio.h>
#include<malloc.h>

struct SqStack{
	LinkNode* data[MAX_SIZE];
	int length;
}; 

void init(SqStack* &stack){
	stack = (SqStack*)malloc(sizeof(SqStack));
	stack->length=0; 
}

bool isEmpty(SqStack* stack){
	if(stack->length > 0)
		return false;
	return true;
}

bool isFull(SqStack* stack){
	if(stack->length == MAX_SIZE)
		return true;
	return false;
}

void push(LinkNode* i,SqStack* stack){
	if(stack==NULL)
		return;
	if(!isFull(stack)){
		stack->data[stack->length++] = i;
	}
}

bool pop(SqStack* stack,LinkNode* &i){
	if(stack==NULL)
		return false;
	if(!isEmpty(stack))
		i = stack->data[--stack->length];
		return true;
}
複製代碼

進階:要求使用時間複雜度爲O(N),額外空間複雜度爲O(1)求解此問題。

思路:咱們能夠先將鏈表的後半部分結點的next指針反向,而後從鏈表的兩頭向中間推動,逐次比較。(固然了,爲了避免破壞原始數據結構,咱們在得出結論以後還須要將鏈表指針恢復原樣)

#include<stdio.h>
#include "LinkList.cpp"
#include "SqStack.cpp"

bool isPalindromeList(LinkNode* head){
    /*第一步、與方法一同樣,找到中間結點*/
    if(head == NULL)
        return false;

    LinkNode *n1 = head , *n2 = head;
    while(n2->next != NULL && n2->next->next != NULL){
        n2 = n2->next->next;
        n1 = n1->next;
    }
    //若是沒有結點或者只有一個首結點
    if(n2 == head){
        return false;
    }
    //若是是奇數個結點
    if(n2->next != NULL){
        n1 = n1->next;  //n1 -> middle node
    }

    /*第二步、不使用額外空間,在鏈表自身上作文章:反轉鏈表後半部分結點的next指針*/
    n2 = n1->next;  // n2 -> right part first node
    n1->next = NULL;//middle node->next = NULL
    LinkNode *n3 = NULL;
    while (n2 != NULL) {
        n3 = n2->next;  //記錄下一個要反轉指針的結點
        n2->next = n1;  //反轉指針
        n1 = n2;
        n2 = n3;
    }
    //n1 -> end node
    n3 = n1;  //record end node
    n2 = head->next;
    while (n2 != NULL) {
        if (n2->data != n1->data) {
            return false;
        }
        n2 = n2->next;  //move n2 forward right
        n1 = n1->next;  //move n1 forward left
    }
    //recover the right part nodes
    n2 = n3; //n2 -> end node
    n1 = NULL;
    while (n2 != NULL) {
        n3 = n2->next;
        n2->next = n1;
        n1=n2;
        n2 = n3;
    }

    return true;
}


/*bool isPalindromeList(LinkNode* head){ if(head == NULL) return false; LinkNode *slow = head , *fast = head; SqStack* stack; init(stack); //fast指針每走兩步,slow指針才走一步 while(fast->next != NULL && fast->next->next != NULL){ fast = fast->next->next; slow = slow->next; push(slow,stack); } //鏈表沒有結點或只有一個結點,不是迴文結構 if(isEmpty(stack)) return false; //判斷偶數個結點仍是奇數個結點 if(fast->next != NULL){ //奇數個結點,slow須要再走一步 slow = slow->next; } //從slow的後繼結點開始遍歷鏈表,將每一個結點與棧頂結點比較 LinkNode* node; slow = slow->next; while(slow != NULL){ pop(stack,node); //一旦發現有一個結點不一樣就不是迴文結構 if(slow->data != node->data) return false; slow = slow->next; } return true; }*/

int main(){

    LinkNode* head;
    init(head);
    add(2,head);
    add(3,head);
    add(3,head);
    add(1,head);
    printList(head);

    if(isPalindromeList(head)){
        printf("yes");
    }else{
        printf("no");
    }
    return 0;
}
複製代碼

鏈表與荷蘭國旗問題

將單向鏈表按某值劃分紅左邊小、中間相等、右邊大的形式

#include<stdio.h>
#include "LinkList.cpp"

/* partition一個鏈表有兩種作法。 1,將鏈表中的全部結點放入一個數組中,那麼就轉換成了荷蘭國旗問題,但這種作法會使用O(N)的額外空間; 2,分出邏輯上的small,equal,big三個區域,遍歷鏈表結點將其添加到對應的區域中,最後再將這三個區域連起來。 這裏只示範第二種作法: */
void partitionList(LinkNode *head,int val){
	if(head == NULL)
		return;
	LinkNode *smH = NULL;	//small area head node
	LinkNode *smT = NULL;	//small area tail node
	LinkNode *midH = NULL;	//equal area head node
	LinkNode *midT = NULL;	//equal area tail node
	LinkNode *bigH = NULL;	//big area head node
	LinkNode *bigT = NULL;	//big area tail node
	LinkNode *cur = head->next;	
	LinkNode *next = NULL;//next node need to be distributed to the three areas
	while(cur != NULL){
		next = cur->next;
		cur->next = NULL;
		if(cur->data > val){
			if(bigH == NULL){
				bigH = bigT = cur;
			}else{
				bigT->next = cur;
				bigT = cur;
			}
		}else if(cur->data == val){
			if(midH == NULL){
				midH = midT = cur;
			}else{
				midT->next = cur;
				midT = cur;
			}
		}else{
			if(smH == NULL){
				smH = smT = cur;
			}else{
				smT->next = cur;
				smT = cur;
			}
		}
		cur = next;
	}
	//reconnect small and equal
	if(smT != NULL){
		smT->next = midH;
		midT = midT == NULL ? midT : smT;
	}
	//reconnect equal and big
	if(bigT != NULL){
		midT->next = bigH;
	}

	head = smH != NULL ? smH : midH != NULL ? midH : bigH;

	return;
} 

int main(){
	LinkNode* head;
	init(head);
	add(5,head);
	add(2,head);
	add(7,head);
	add(9,head);
	add(1,head);
	add(3,head);
	add(5,head);
	printList(head);
	partitionList(head,5);
	printList(head);
}
複製代碼

複製含有隨機指針結點的鏈表

藉助哈希表,額外空間O(N)

將鏈表的全部結點複製一份,以key,value源結點,副本結點的方式存儲到哈希表中,再創建副本結點之間的關係(next、rand指針域)

import java.util.HashMap;
import java.util.Map;

public class CopyLinkListWithRandom {

    public static class Node {
        public Node(int data) {
            this.data = data;
        }

        public Node() {
        }

        int data;
        Node next;
        Node rand;
    }

    public static Node copyLinkListWithRandom(Node head) {
        if (head == null) {
            return null;
        }
        Node cur = head;
        Map<Node, Node> copyMap = new HashMap<>();
        while (cur != null) {
            copyMap.put(cur, new Node(cur.data));
            cur = cur.next;
        }
        cur = head;
        while (cur != null) {
            copyMap.get(cur).next = copyMap.get(cur.next);
            copyMap.get(cur).rand = copyMap.get(cur.rand);
            cur = cur.next;
        }
        return copyMap.get(head);
    }

    public static void printListWithRandom(Node head) {
        if (head != null) {
            while (head.next != null) {
                head = head.next;
                System.out.print("node data:" + head.data);
                if (head.rand != null) {
                    System.out.println(",rand data:" + head.rand.data);
                } else {
                    System.out.println(",rand is null");
                }
            }
        }
    }
  
    public static void main(String[] args) {
        Node head = new Node();
        head.next = new Node(1);
        head.next.next = new Node(2);
        head.next.next.next = new Node(3);
        head.next.next.next.next = new Node(4);
        head.next.rand = head.next.next.next.next;
        head.next.next.rand = head.next.next.next;
        printListWithRandom(head);

        System.out.println("==========");

        Node copy = copyLinkListWithRandom(head);
        printListWithRandom(copy);
    }
}
複製代碼

進階操做:額外空間O(1)

將副本結點追加到對應源結點以後,創建副本結點之間的指針域,最後將副本結點從該鏈表中分離出來。

//extra area O(1)
public static Node copyLinkListWithRandom2(Node head){
  if (head == null) {
    return null;
  }
  Node cur = head;
  //copy every node and append
  while (cur != null) {
    Node copy = new Node(cur.data);
    copy.next = cur.next;
    cur.next = copy;
    cur = cur.next.next;
  }
  //set the rand pointer of every copy node
  Node copyHead = head.next;
  cur = head;
  Node curCopy = copyHead;
  while (curCopy != null) {
    curCopy.rand = cur.rand == null ? null : cur.rand.next;
    cur = curCopy.next;
    curCopy = cur == null ? null : cur.next;
  }
  //split
  cur = head;
  Node next = null;
  while (cur != null) {
    curCopy = cur.next;
    next = cur.next.next;
    curCopy.next = next == null ? null : next.next;
    cur.next = next;
    cur = next;
  }
  return copyHead;
}
複製代碼

若兩個可能有環的單鏈表相交,請返回相交的第一個結點

根據單鏈表的定義,每一個結點有且只有一個next指針,那麼若是單鏈表有環,它的結構將是以下所示:

相交會致使兩個結點指向同一個後繼結點,但不可能出現一個結點有兩個後繼結點的狀況。

一、當相交的結點不在環上時,有以下兩種狀況:

二、當相交的結點在環上時,只有一種狀況:

綜上,兩單鏈表若相交,要麼都無環,要麼都有環。

此題還須要注意的一點是若是鏈表有環,那麼如何獲取入環呢(由於不能經過next是否爲空來判斷是不是尾結點了)。這裏就涉及到了一個規律:若是快指針fast和慢指針slow同時從頭結點出發,fast走兩步而slow走一步,當二者相遇時,將fast指針指向頭結點,使二者都一次只走一步,二者會在入環結點相遇。

public class FirstIntersectNode {
    public static class Node{
        int data;
        Node next;
        public Node(int data) {
            this.data = data;
        }
    }

    public static Node getLoopNode(Node head) {
        if (head == null) {
            return null;
        }
        Node fast = head;
        Node slow = head;
        do {
            slow = slow.next;
            if (fast.next == null || fast.next.next == null) {
                return null;
            } else {
                fast = fast.next.next;
            }
        } while (fast != slow);
        //fast == slow
        fast = head;
        while (fast != slow) {
            slow = slow.next;
            fast = fast.next;
        }
        return slow;
    }

    public static Node getFirstIntersectNode(Node head1, Node head2) {
        if (head1 == null || head2 == null) {
            return null;
        }
        Node loop1 = getLoopNode(head1);    //兩鏈表的入環結點loop1和loop2
        Node loop2 = getLoopNode(head2);
        //no loop
        if (loop1 == null && loop2 == null) {
            return noLoop(head1, head2);
        }
        //both loop
        if (loop1 != null && loop2 != null) {
            return bothLoop(head1, head2, loop1, loop2);
        }
        //don't intersect
        return null;
    }

    private static Node bothLoop(Node head1, Node head2, Node loop1, Node loop2) {
        Node cur1 = head1;
        Node cur2 = head2;
        //入環結點相同,相交點不在環上
        if (loop1 == loop2) {
            int n = 0;
            while (cur1.next != loop1) {
                n++;
                cur1 = cur1.next;
            }
            while (cur2.next != loop1) {
                n--;
                cur2 = cur2.next;
            }
            cur1 = n > 0 ? head1 : head2;           //將cur1指向結點數較多的鏈表
            cur2 = cur1 == head1 ? head2 : head1;   //將cur2指向另外一個鏈表
            n = Math.abs(n);
            while (n != 0) {                        //將cur1先走兩鏈表結點數差值個結點
                cur1 = cur1.next;
                n--;
            }
            while (cur1 != cur2) {                  //cur1和cur2會在入環結點相遇
                cur1 = cur1.next;
                cur2 = cur2.next;
            }
            return cur1;
        }
        //入環結點不一樣,相交點在環上
        cur1 = loop1.next;
        while (cur1 != loop1) {
            if (cur1 == loop2) {    //鏈表2的入環結點在鏈表1的環上,說明相交
                return loop1;   //返回loop1或loop2都可,由於整個環就是兩鏈表的相交部分
            }
            cur1 = cur1.next;
        }
        //在鏈表1的環上轉了一圈也沒有找到鏈表2的入環結點,說明不想交
        return null;
    }

    private static Node noLoop(Node head1, Node head2) {
        Node cur1 = head1;
        Node cur2 = head2;
        int n = 0;
        while (cur1.next != null) {
            n++;
            cur1 = cur1.next;
        }
        while (cur2.next != null) {
            n--;
            cur2 = cur2.next;
        }
        if (cur1 != cur2) {     //兩鏈表的尾結點不一樣,不可能相交
            return null;
        }
        cur1 = n > 0 ? head1 : head2;           //將cur1指向結點數較多的鏈表
        cur2 = cur1 == head1 ? head2 : head1;   //將cur2指向另外一個鏈表
        n = Math.abs(n);
        while (n != 0) {                        //將cur1先走兩鏈表結點數差值個結點
            cur1 = cur1.next;
            n--;
        }
        while (cur1 != cur2) {                  //cur1和cur2會在入環結點相遇
            cur1 = cur1.next;
            cur2 = cur2.next;
        }
        return cur1;
    }

    public static void printList(Node head) {
        for (int i = 0; i < 50; i++) {
            System.out.print(head.data+" ");
            head = head.next;
        }
        System.out.println();
    }

}
複製代碼

對應三種狀況測試以下:

public static void main(String[] args) {

        //==================== both loop ======================
        //1->2->[3]->4->5->6->7->[3]...
        Node head1 = new Node(1);
        head1.next = new Node(2);
        head1.next.next = new Node(3);
        head1.next.next.next = new Node(4);
        head1.next.next.next.next = new Node(5);
        head1.next.next.next.next.next = new Node(6);
        head1.next.next.next.next.next.next = new Node(7);
        head1.next.next.next.next.next.next.next = head1.next.next;

        //9->8->[6]->7->3->4->5->[6]...
        Node head2 = new Node(9);
        head2.next = new Node(8);
        head2.next.next = head1.next.next.next.next.next;
        head2.next.next.next = head1.next.next.next.next.next.next;
        head2.next.next.next.next = head1.next.next;
        head2.next.next.next.next.next = head1.next.next.next;
        head2.next.next.next.next.next.next = head1.next.next.next.next;
        head2.next.next.next.next.next.next.next = head1.next.next.next.next.next;

        printList(head1);
        printList(head2);
        System.out.println(getFirstIntersectNode(head1, head2).data);
        System.out.println("==================");

        //1->[2]->3->4->5->6->7->8->4...
        Node head3 = new Node(1);
        head3.next = new Node(2);
        head3.next.next = new Node(3);
        head3.next.next.next = new Node(4);
        head3.next.next.next.next = new Node(5);
        head3.next.next.next.next.next = new Node(6);
        head3.next.next.next.next.next.next = new Node(7);
        head3.next.next.next.next.next.next.next = new Node(8);
        head3.next.next.next.next.next.next.next.next = head1.next.next.next;

        //9->0->[2]->3->4->5->6->7->8->4...
        Node head4 = new Node(9);
        head4.next = new Node(0);
        head4.next.next = head3.next;
        head4.next.next.next = head3.next.next;
        head4.next.next.next.next = head3.next.next.next;
        head4.next.next.next.next.next = head3.next.next.next.next;
        head4.next.next.next.next.next.next = head3.next.next.next.next.next;
        head4.next.next.next.next.next.next.next = head3.next.next.next.next.next.next;
        head4.next.next.next.next.next.next.next.next = head3.next.next.next.next.next.next.next;
        head4.next.next.next.next.next.next.next.next.next = head3.next.next.next;

        printList(head3);
        printList(head4);
        System.out.println(getFirstIntersectNode(head3,head4).data);
        System.out.println("==================");

        //============= no loop ==============
        //1->[2]->3->4->5
        Node head5 = new Node(1);
        head5.next = new Node(2);
        head5.next.next = new Node(3);
        head5.next.next.next = new Node(4);
        head5.next.next.next.next = new Node(5);
        //6->[2]->3->4->5
        Node head6 = new Node(6);
        head6.next = head5.next;
        head6.next.next = head5.next.next;
        head6.next.next.next = head5.next.next.next;
        head6.next.next.next.next = head5.next.next.next.next;

        System.out.println(getFirstIntersectNode(head5,head6).data);
    }
複製代碼

棧和隊列

用數組結構實現大小固定的棧和隊列

//
// Created by zaw on 2018/10/21.
//
#include <stdio.h>
#include <malloc.h>
#define MAX_SIZE 1000

struct ArrayStack{
    int data[MAX_SIZE];
    int top;
};

void init(ArrayStack *&stack) {
    stack = (ArrayStack *) malloc(sizeof(ArrayStack));
    stack->top = -1;
}

bool isEmpty(ArrayStack* stack){
    return stack->top == -1 ?;
}

bool isFull(ArrayStack *stack){
    return stack->top == MAX_SIZE - 1 ?;
}

void push(int i, ArrayStack *stack){
    if (!isFull(stack)) {
        stack->data[++stack->top] = i;
    }
}

int pop(ArrayStack* stack){
    if (!isEmpty(stack)) {
        return stack->data[stack->top--];
    }
}

int getTopElement(ArrayStack *stack){
    if (!isEmpty(stack)) {
        return stack->data[stack->top];
    }
}

int main(){

    ArrayStack* stack;
    init(stack);
    push(1, stack);
    push(2, stack);
    push(3, stack);

    printf("%d ", pop(stack));
    printf("%d ", getTopElement(stack));
    printf("%d ", pop(stack));
    printf("%d ", pop(stack));
		//3 2 2 1
    return 0;
}
複製代碼
//
// Created by zaw on 2018/10/21.
//
#include <stdio.h>
#include <malloc.h>
#define MAX_SIZE 1000

//數組結構實現的環形隊列
struct ArrayCircleQueue{
    int data[MAX_SIZE];
    int front,rear;
};

void init(ArrayCircleQueue *&queue){
    queue = (ArrayCircleQueue *) malloc(sizeof(ArrayCircleQueue));
    queue->front = queue->rear = 0;
}

bool isEmpty(ArrayCircleQueue *queue){
    return queue->front == queue->rear;
}

bool isFull(ArrayCircleQueue *queue){
    return (queue->rear+1)%MAX_SIZE==queue->front;
}

void enQueue(int i, ArrayCircleQueue *queue){
    if (!isFull(queue)) {
        //move the rear and fill it
        queue->data[++queue->rear] = i;
    }
}

int deQueue(ArrayCircleQueue *queue){
    if (!isEmpty(queue)) {
        return queue->data[++queue->front];
    }
}

int main(){
    ArrayCircleQueue* queue;
    init(queue);
    enQueue(1, queue);
    enQueue(2, queue);
    enQueue(3, queue);
    while (!isEmpty(queue)) {
        printf("%d ", deQueue(queue));
    }
}
複製代碼

取棧中最小元素

實現一個特殊的棧,在實現棧的基本功能的基礎上,再實現返回棧中最小元素的操做getMin。要求以下:

  • poppushgetMin操做的時間複雜度都是O(1)
  • 設計的棧類型可使用現成的棧結構。

思路:因爲每次push以後都會可能致使棧中已有元素的最小值發生變化,所以須要一個容器與該棧聯動(記錄每次push產生的棧中最小值)。咱們能夠藉助一個輔助棧,數據棧push第一個元素時,將其也push到輔助棧,此後每次向數據棧push元素的同時將其和輔助棧的棧頂元素比較,若是小,則將其也push到輔助棧,不然取輔助棧的棧頂元素push到輔助棧。(數據棧正常pushpop數據,而輔助棧push每次數據棧push後產生的棧中最小值;但數據棧pop時,輔助棧也只需簡單的pop便可,保持同步)

//
// Created by zaw on 2018/10/21.
//
#include <stdio.h>
#include <malloc.h>
#include "ArrayStack.cpp"

int min(int a, int b){
    return a < b ? a : b;
}

struct GetMinStack{
    ArrayStack* dataStack;
    ArrayStack* helpStack;
};

void initGetMinStack(GetMinStack* &stack){
    stack = (GetMinStack *) malloc(sizeof(GetMinStack));
    init(stack->dataStack);
    init(stack->helpStack);
}

void push(int i, GetMinStack *stack) {
    if (!isFull(stack->dataStack)) {
        push(i, stack->dataStack);  //ArrayStack.cpp
        if (!isEmpty(stack->helpStack)) {
            i = min(i, getTopElement(stack->helpStack));
        }
        push(i, stack->helpStack);
    }
}

int pop(GetMinStack* stack){
    if (!isEmpty(stack->dataStack)) {
        pop(stack->helpStack);
        return pop(stack->dataStack);
    }
}

int getMin(GetMinStack *stack){
    if (!isEmpty(stack->dataStack)) {
        return getTopElement(stack->helpStack);
    }
}

int main(){
    GetMinStack *stack;
    initGetMinStack(stack);
    push(6, stack);
    printf("%d ", getMin(stack));//6
    push(3, stack);
    printf("%d ", getMin(stack));//3
    push(1, stack);
    printf("%d ", getMin(stack));//1
  	pop(stack);
    printf("%d ", getMin(stack));//3

    return 0;
}
複製代碼

僅用隊列結構實現棧結構

思路:只要將關注點放在 後進先出 這個特性就不難實現了。使用一個數據隊列和輔助隊列,當放入數據時使用隊列的操做正常向數據隊列中放,但出隊元素時,需將數據隊列的前n-1個數入隊輔助隊列,而將數據隊列的隊尾元素彈出來,最後數據隊列和輔助隊列交換角色。

//
// Created by zaw on 2018/10/21.
//
#include <stdio.h>
#include <malloc.h>
#include "../queue/ArrayCircleQueue.cpp"

struct DoubleQueueStack{
    ArrayCircleQueue* dataQ;
    ArrayCircleQueue* helpQ;
};

void init(DoubleQueueStack* &stack){
    stack = (DoubleQueueStack *) malloc(sizeof(DoubleQueueStack));
    init(stack->dataQ);
    init(stack->helpQ);
}

void swap(ArrayCircleQueue *&dataQ, ArrayCircleQueue *&helpQ){
    ArrayCircleQueue* temp = dataQ;
    dataQ = helpQ;
    helpQ = temp;
}

void push(int i,DoubleQueueStack* stack){
    if (!isFull(stack->dataQ)) {
        return enQueue(i, stack->dataQ);
    }
}

int pop(DoubleQueueStack* stack){
    if (!isEmpty(stack->dataQ)) {
        int i = deQueue(stack->dataQ);
        while (!isEmpty(stack->dataQ)) {
            enQueue(i, stack->helpQ);
            i = deQueue(stack->dataQ);
        }
        swap(stack->dataQ, stack->helpQ);
        return i;
    }
}

bool isEmpty(DoubleQueueStack* stack){
    return isEmpty(stack->dataQ);
}

int getTopElement(DoubleQueueStack* stack){
    if (!isEmpty(stack->dataQ)) {
        int i = deQueue(stack->dataQ);
        while (!isEmpty(stack->dataQ)) {
            enQueue(i, stack->helpQ);
            i = deQueue(stack->dataQ);
        }
        enQueue(i, stack->helpQ);
        swap(stack->dataQ, stack->helpQ);
        return i;
    }
}

int main(){

    DoubleQueueStack *stack;
    init(stack);
    push(1, stack);
    push(2, stack);
    push(3, stack);
    while (!isEmpty(stack)) {
        printf("%d ", pop(stack));
    }
    push(4, stack);
    printf("%d ", getTopElement(stack));
    
    return 0;
}
複製代碼

僅用棧結構實現隊列結構

思路:使用兩個棧,一個棧PutStack用來放數據,一個棧GetStack用來取數據。取數據時,若是PulllStack爲空則須要將PutStack中的全部元素一次性依次pop並放入GetStack

特別要注意的是這個 倒數據的時機:

  • 只有當GetStack爲空時才能往裏倒
  • 倒數據時必須一次性將PutStack中的數據倒完
//
// Created by zaw on 2018/10/21.
//
#include <stdio.h>
#include <malloc.h>
#include "../stack/ArrayStack.cpp"

struct DoubleStackQueue{
    ArrayStack* putStack;
    ArrayStack* getStack;
};

void init(DoubleStackQueue *&queue){
    queue = (DoubleStackQueue *) malloc(sizeof(DoubleStackQueue));
    init(queue->putStack);
    init(queue->getStack);
}

bool isEmpty(DoubleStackQueue *queue){
    return isEmpty(queue->getStack) && isEmpty(queue->putStack);
}

void pour(ArrayStack *stack1, ArrayStack *stack2){
    while (!isEmpty(stack1)) {
        push(pop(stack1), stack2);
    }
}

void enQueue(int i, DoubleStackQueue *queue){
    if (!isFull(queue->putStack)) {
        push(i, queue->putStack);
    } else {
        if (isEmpty(queue->getStack)) {
            pour(queue->putStack, queue->getStack);
            push(i, queue->putStack);
        }
    }
}

int deQueue(DoubleStackQueue* queue){
    if (!isEmpty(queue->getStack)) {
        return pop(queue->getStack);
    } else {
        if (!isEmpty(queue->putStack)) {
            pour(queue->putStack, queue->getStack);
            return pop(queue->getStack);
        }
    }
}


int main(){
    DoubleStackQueue *queue;
    init(queue);
    enQueue(1, queue);
    printf("%d\n", deQueue(queue));
    enQueue(2, queue);
    enQueue(3, queue);
    while (!isEmpty(queue)) {
        printf("%d ", deQueue(queue));
    }
    return 0;
}
複製代碼
相關文章
相關標籤/搜索