超硬核十萬字!全網最全 資料結構 程式碼,隨便秒殺老師/面試官,我說的

2021-04-20 15:00:02

本文程式碼實現基本按照《資料結構》課本目錄順序,外加大量的複雜演演算法實現,一篇文章足夠。能換你一個收藏了吧?

 當然如果落下什麼了歡迎大家評論指出

目錄

順序儲存線性表實現 

單連結串列不帶頭標準c語言實現

單連結串列不帶頭壓縮c語言實現

約瑟夫環-(陣列、迴圈連結串列、數學) 

線性表表示集合

 線性表實現一元多項式操作

連結串列環問題

 

移除連結串列元素

迴文連結串列

連結串列表示整數,相加

LRU

LFU

合併連結串列

反轉連結串列

 反轉連結串列2

對連結串列排序

旋轉連結串列

 陣列實現棧

連結串列實現棧

陣列實現佇列

連結串列實現佇列

雙棧的實現

 棧/佇列 互相模擬實現

棧的排序

棧——括號匹配

棧——表示式求值 

借漢諾塔理解棧與遞迴

單調棧

雙端單調佇列

 單調佇列優化的揹包問題

01揹包問題 

完全揹包問題 

多重揹包問題 

 串的定長表示

串的堆分配實現

KMP

一、引子

二、分析總結

三、基本操作

四、原理

五、複雜度分析

Manacher

小問題一:請問,子串和子序列一樣麼?請思考一下再往下看

小問題二:長度為n的字串有多少個子串?多少個子序列?

一、分析列舉的效率

二、初步優化

問題三:怎麼用對稱軸向兩邊擴的方法找到偶迴文?(容易操作的)

那麼請問,加進去的符號,有什麼要求麼?是不是必須在原字元中沒出現過?請思考

小結:

三、Manacher原理

假設遍歷到位置i,如何操作呢

四、程式碼及複雜度分析

字首樹

字尾樹/字尾陣列

字尾樹:字尾樹,就是把一串字元的所有字尾儲存並且壓縮的字典樹。

 

相對於字典樹來說,字尾樹並不是針對大量字串的,而是針對一個或幾個字串來解決問題。比如字串的迴文子串,兩個字串的最長公共子串等等。

字尾陣列:就是把某個字串的所有字尾按照字典序排序後的陣列。(陣列中儲存起始位置就好了,結束位置一定是最後)

AC自動機

陣列缺失

二元樹遍歷

前序

中序

後序

進一步思考

二元樹序列化/反序列化

先序中序後序兩兩結合重建二元樹

先序遍歷

中序遍歷

後序遍歷

層次遍歷

輸入某二元樹的前序遍歷和中序遍歷的結果,請重建出該二元樹。假設輸入的前序遍歷和中序遍歷的結果中都不含重複的數位。例如輸入前序遍歷序列{1,2,4,7,3,5,6,8}和中序遍歷序列{4,7,2,1,5,3,8,6},則重建二元樹並返回。

輸入某二元樹的後序遍歷和中序遍歷的結果,請重建出該二元樹。假設輸入的前序遍歷和中序遍歷的結果中都不含重複的數位

輸入某二元樹的後序遍歷和先序遍歷的結果,請重建出該二元樹。假設輸入的前序遍歷和中序遍歷的結果中都不含重複的數位

先序中序陣列推後序陣列

二元樹遍歷

遍歷命名

方法1:我們可以重建整棵樹:

https://blog.csdn.net/hebtu666/article/details/84322113

方法2:我們可以不用重建,直接得出:

根據陣列建立平衡二元搜尋樹

java整體列印二元樹

判斷平衡二元樹

判斷完全二元樹

判斷二元搜尋樹

二元搜尋樹實現

堆的簡單實現

堆應用例題三連

一個資料流中,隨時可以取得中位數。

金條

專案最大收益(貪心問題)

 並查集實現

並查集入門三連:HDU1213 POJ1611 POJ2236

HDU1213

POJ1611

 POJ2236

線段樹簡單實現

功能:一樣的,依舊是查詢和改值。

查詢[s,t]之間最小的數。修改某個值。

那我們繼續說,如何查詢。

如何更新?

 樹狀陣列實現

最大搜尋子樹

morris遍歷

最小生成樹

拓撲排序

最短路

 

簡單迷宮問題

深搜DFS\廣搜BFS 

 皇后問題

一般思路:

優化1:

優化2:

二元搜尋樹實現

Abstract Self-Balancing Binary Search Tree

 

二元搜尋樹

概念引入

AVL樹

紅黑樹

size balance tree

伸展樹

Treap

最簡單的旋轉

帶子樹旋轉

程式碼實現

AVL Tree

前言

二元搜尋樹

AVL Tree

旋轉

旋轉總結

單向右旋平衡處理LL:

單向左旋平衡處理RR:

雙向旋轉(先左後右)平衡處理LR:

雙向旋轉(先右後左)平衡處理RL:

深度的記錄

單個節點的深度更新

寫出旋轉程式碼

總寫調整方法

插入完工

刪除

直觀表現程式

跳錶介紹和實現

c語言實現排序和查詢所有演演算法

 

 


順序儲存線性表實現 

在計算機中用一組地址連續的儲存單元依次儲存線性表的各個資料元素,稱作線性表的順序儲存結構。

 

順序儲存結構的主要優點是節省儲存空間,因為分配給資料的儲存單元全用存放結點的資料(不考慮c/c++語言中陣列需指定大小的情況),結點之間的邏輯關係沒有佔用額外的儲存空間。採用這種方法時,可實現對結點的隨機存取,即每一個結點對應一個序號,由該序號可以直接計算出來結點的儲存地址。但順序儲存方法的主要缺點是不便於修改,對結點的插入、刪除運算時,可能要移動一系列的結點。

優點:隨機存取表中元素。缺點:插入和刪除操作需要移動元素。

 

線性表中資料元素之間的關係是一對一的關係,即除了第一個和最後一個資料元素之外,其它資料元素都是首尾相接的(注意,這句話只適用大部分線性表,而不是全部。比如,迴圈連結串列邏輯層次上也是一種線性表(儲存層次上屬於鏈式儲存),但是把最後一個資料元素的尾指標指向了首位結點)。

給出兩種基本實現:

/*
靜態順序儲存線性表的基本實現
*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define LIST_INITSIZE 100
#define ElemType int
#define Status int
#define OK     1
#define ERROR  0

typedef struct
{
	ElemType elem[LIST_INITSIZE];
	int length;
}SqList;

//函數介紹
Status InitList(SqList *L); //初始化
Status ListInsert(SqList *L, int i,ElemType e);//插入
Status ListDelete(SqList *L,int i,ElemType *e);//刪除
void ListPrint(SqList L);//輸出列印
void DisCreat(SqList A,SqList *B,SqList *C);//拆分(按正負),也可以根據需求改
//雖然思想略簡單,但是要寫的沒有錯誤,還是需要鍛鍊coding能力的

Status InitList(SqList *L)
{
    L->length = 0;//長度為0
    return OK;
}

Status ListInsert(SqList *L, int i,ElemType e)
{
    int j;
    if(i<1 || i>L->length+1)
        return ERROR;//判斷非法輸入
    if(L->length == LIST_INITSIZE)//判滿
    {
        printf("表已滿");//提示
        return ERROR;//返回失敗
    }
    for(j = L->length;j > i-1;j--)//從後往前覆蓋,注意i是從1開始
        L->elem[j] = L->elem[j-1];
    L->elem[i-1] = e;//在留出的位置賦值
    (L->length)++;//表長加1
    return OK;//反回成功
}

Status ListDelete(SqList *L,int i,ElemType *e)
{
    int j;
    if(i<1 || i>L->length)//非法輸入/表空
        return ERROR;
    *e = L->elem[i-1];//為了返回值
    for(j = i-1;j <= L->length;j++)//從前往後覆蓋
        L->elem[j] = L->elem[j+1];
    (L->length)--;//長度減1
    return OK;//返回刪除值
}

void ListPrint(SqList L)
{
    int i;
    for(i = 0;i < L.length;i++)
        printf("%d ",L.elem[i]);
    printf("\n");//為了美觀
}

void DisCreat(SqList A,SqList *B,SqList *C)
{
    int i;
    for(i = 0;i < A.length;i++)//依次遍歷A中元素
    {
        if(A.elem[i]<0)//判斷
            ListInsert(B,B->length+1,A.elem[i]);//直接呼叫插入函數實現尾插
        else
            ListInsert(C,C->length+1,A.elem[i]);
    }
}

int main(void)
{
    //複製的
	SqList L;
	SqList B, C;
	int i;
	ElemType e;
	ElemType data[9] = {11,-22,33,-3,-88,21,77,0,-9};
	InitList(&L);
	InitList(&B);
	InitList(&C);
	for (i = 1; i <= 9; i++)
		ListInsert(&L,i,data[i-1]);
    printf("插入完成後L = : ");
	ListPrint(L);
    ListDelete(&L,1,&e);
	printf("刪除第1個後L = : ");
	ListPrint(L);
    DisCreat(L , &B, &C);
	printf("拆分L後B = : ");
	ListPrint(B);
	printf("拆分L後C = : ");
	ListPrint(C);
	printf("拆分L後L = : ");
	ListPrint(L);
}

靜態:長度固定

動態:不夠存放可以加空間(搬家)

 

/*
子任務名任務:1_2 動態順序儲存線性表的基本實現
*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define LIST_INIT_SIZE 100
#define LISTINCREMENT 10
#define Status int
#define OVERFLOW -1
#define OK 1
#define ERROR 0
#define ElemType int

typedef struct
{
	ElemType * elem;
	int length;
	int listsize;
}SqList;
//函數介紹
Status InitList(SqList *L); //初始化
Status ListInsert(SqList *L, int i,ElemType e);//插入
Status ListDelete(SqList *L,int i,ElemType *e);//刪除
void ListPrint(SqList L);//輸出列印
void DeleteMin(SqList *L);//刪除最小

Status InitList(SqList *L)
{
    L->elem = (ElemType *)malloc(LIST_INIT_SIZE*sizeof(ElemType));//申請100空間
	if(!L->elem)//申請失敗
		return ERROR;
	L->length = 0;//長度0
	L->listsize = LIST_INIT_SIZE;//容量100
	return OK;//申請成功
}

Status ListInsert(SqList *L,int i,ElemType e)
{
    int j;
    ElemType *newbase;
    if(i<1 || i>L->length+1)
        return ERROR;//非法輸入
        
    if(L->length >= L->listsize)//存滿了,需要更大空間
    {
        newbase = (ElemType*)realloc(L->elem,(L->listsize+LISTINCREMENT)*sizeof(ElemType));//大10的空間
        if(!newbase)//申請失敗
            return ERROR;
        L->elem = newbase;//調指標
        L->listsize+= LISTINCREMENT;//新容量
    }
    
    for(j=L->length;j>i-1;j--)//從後往前覆蓋
        L->elem[j] = L->elem[j-1];
    L->elem[i-1] = e;//在留出的位置賦值
    L->length++;//長度+1
    return OK;
}

Status ListDelete(SqList *L,int i,ElemType *e)
{
    int j;
    if(i<1 || i>L->length)//非法輸入/表空
        return ERROR;
    *e = L->elem[i-1];//為了返回值
    for(j = i-1;j <= L->length;j++)//從前往後覆蓋
        L->elem[j] = L->elem[j+1];
    (L->length)--;//長度減1
    return OK;//返回刪除值
}

void ListPrint(SqList L)
{
    int i;
    for(i=0;i<L.length;i++)
        printf("%d ",L.elem[i]);
    printf("\n");//為了美觀
}

void DeleteMin(SqList *L)
{
    //表空在Listdelete函數裡判斷
    int i;
    int j=0;//最小值下標
    ElemType *e;
    for(i=0;i<L->length;i++)//尋找最小
    {
        if(L->elem[i] < L->elem[j])
            j=i;
    }
    ListDelete(L,j+1,&e);//呼叫刪除,注意j要+1
}

int main(void)
{
	SqList L;
	int i;
	ElemType e;
	ElemType data[9] = {11,-22,-33,3,-88,21,77,0,-9};
	InitList(&L);
	for (i = 1; i <= 9; i++)
	{
		ListInsert(&L,i,data[i-1]);
	}
	printf("插入完成後 L = : ");
	ListPrint(L);
    ListDelete(&L, 2, &e);
	printf("刪除第 2 個後L = : ");
	ListPrint(L);
    DeleteMin(&L);
	printf("刪除L中最小值後L = : ");
	ListPrint(L);
	DeleteMin(&L);
	printf("刪除L中最小值後L = : ");
	ListPrint(L);
	DeleteMin(&L);
	printf("刪除L中最小值後L = : ");
	ListPrint(L);
}

單連結串列不帶頭標準c語言實現

 

連結串列是一種物理儲存單元上非連續、非順序的儲存結構資料元素的邏輯順序是通過連結串列中的指標連結次序實現的。連結串列由一系列結點(連結串列中每一個元素稱為結點)組成,結點可以在執行時動態生成。每個結點包括兩個部分:一個是儲存資料元素的資料域,另一個是儲存下一個結點地址的指標域。 相比於線性表順序結構,操作複雜。由於不必須按順序儲存,連結串列在插入的時候可以達到O(1)的複雜度,比另一種線性表順序錶快得多,但是查詢一個節點或者存取特定編號的節點則需要O(n)的時間,而線性表和順序表相應的時間複雜度分別是O(logn)和O(1)。

使用連結串列結構可以克服陣列連結串列需要預先知道資料大小的缺點,連結串列結構可以充分利用計算機記憶體空間,實現靈活的記憶體動態管理。但是連結串列失去了陣列隨機讀取的優點,同時連結串列由於增加了結點的指標域,空間開銷比較大。連結串列最明顯的好處就是,常規陣列排列關聯專案的方式可能不同於這些資料專案在記憶體磁碟上順序,資料的存取往往要在不同的排列順序中轉換。連結串列允許插入和移除表上任意位置上的節點,但是不允許隨機存取。連結串列有很多種不同的型別:單向連結串列雙向連結串列以及迴圈連結串列

 

下面給出不帶頭的單連結串列標準實現:

定義節點:

typedef struct node 
{ 
    int data;
    struct node * next;
}Node;

尾插:

void pushBackList(Node ** list, int data) 
{ 
    Node * head = *list;
    Node * newNode = (Node *)malloc(sizeof(Node));//申請空間
    newNode->data = data; newNode->next = NULL;
    if(*list == NULL)//為空
        *list = newNode;
    else//非空
    {
        while(head ->next != NULL)
            head = head->next;
        head->next = newNode;
    }
}

插入:

int insertList(Node ** list, int index, int data) 
{
    int n;
    int size = sizeList(*list); 
    Node * head = *list; 
    Node * newNode, * temp;
    if(index<0 || index>size) return 0;//非法
    newNode = (Node *)malloc(sizeof(Node)); //建立新節點
    newNode->data = data; 
    newNode->next = NULL;
    if(index == 0) //頭插
    {
        newNode->next = head; 
        *list = newNode; 
        return 1; 
    }
    for(n=1; n<index; n++) //非頭插
        head = head->next;
    if(index != size) 
        newNode->next = head->next; 
    //連結串列尾部next不需指定
    head->next = newNode; 
    return 1;
}

按值刪除:

void deleteList(Node ** list, int data) 
{ 
    Node * head = *list; Node * temp; 
    while(head->next!=NULL) 
    { 
        if(head->next->data != data) 
        { 
            head=head->next; 
            continue; 
        } 
        temp = head->next;
        if(head->next->next == NULL) //尾節點刪除
            head->next = NULL; 
        else 
            head->next = temp->next; 
        free(temp);
    }    
    head = *list; 
    if(head->data == data) //頭結點刪除
    { 
        temp = head; 
        *list = head->next; 
        head = head->next; 
        free(temp); 
    }
}

列印:

void printList(Node * head) 
{ 
    Node * temp = head; 
    for(; temp != NULL; temp=temp->next) 
        printf("%d ", temp->data); 
    printf("\n"); 
}

清空:

void freeList(Node ** list) 
{ 
    Node * head = *list; 
    Node * temp = NULL; 
    while(head != NULL) //依次釋放
    { 
        temp = head; 
        head = head->next; 
        free(temp); 
    } 
    *list = NULL; //置空
}

別的也沒啥了,都是基本操作

有些程式碼要分情況,很麻煩,可讀性較強吧

 

單連結串列不帶頭壓縮c語言實現

 

 

 注:單追求程式碼簡潔,所以寫法可能有點不標準。

//第一次拿c開始寫資料結構,因為自己寫的,追求程式碼量少,和學院ppt不太一樣。有錯請指出
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct node//定義節點
{
    int data;
    struct node * next;
}Node;

 

//函數介紹
void printlist(Node * head)//列印連結串列
int lenlist(Node * head)//返回連結串列長度
void insertlist(Node ** list,int data,int index)//插入元素
void pushback(Node ** head,int data)//尾部插入
void freelist(Node ** head)//清空連結串列
void deletelist(Node ** list,int data)//刪除元素
Node * findnode(Node ** list,int data)//查詢
void change(Node ** list,int data,int temp)//改變值

列印

void printlist(Node * head)//列印連結串列
{
    for(;head!=NULL;head=head->next) printf("%d ",head->data);
    printf("\n");//為了其他函數列印,最後換行
}

連結串列長度

int lenlist(Node * head)//返回連結串列長度
{
    int len;
    Node * temp = head;
    for(len=0; temp!=NULL; len++) temp=temp->next;
    return len;
}

插入元素

void insertlist(Node ** list,int data,int index)//插入元素,用*list將head指標和next統一表示
{
    if(index<0 || index>lenlist(*list))return;//判斷非法輸入
    Node * newnode=(Node *)malloc(sizeof(Node));//建立
    newnode->data=data;
    newnode->next=NULL;
    while(index--)list=&((*list)->next);//插入
    newnode->next=*list;
    *list=newnode;
}

尾部增加元素

void pushback(Node ** head,int data)//尾插,同上
{
    Node * newnode=(Node *)malloc(sizeof(Node));//建立
    newnode->data=data;
    newnode->next=NULL;
    while(*head!=NULL)head=&((*head)->next);//插入
    *head=newnode;
}

清空連結串列

void freelist(Node ** head)//清空連結串列
{
    Node * temp=*head;
    Node * ttemp;
    *head=NULL;//指標設為空
    while(temp!=NULL)//釋放
    {
        ttemp=temp;
        temp=temp->next;
        free(ttemp);
    }
}

刪除

void deletelist(Node ** list,int data)//刪除連結串列節點
{
    Node * temp;//作用只是方便free
    while((*list)->data!=data && (*list)->next!=NULL)list=&((*list)->next);
    if((*list)->data==data){
        temp=*list;
        *list=(*list)->next;
        free(temp);
    }
}

查詢

Node * findnode(Node ** list,int data)//查詢,返回指向節點的指標,若無返回空
{
    while((*list)->data!=data && (*list)!=NULL) list=&((*list)->next);
    return *list;
}

改值

void change(Node ** list,int data,int temp)//改變
{
    while((*list)->data!=data && (*list)->next!=NULL)list=&((*list)->next);
    if((*list)->data==data)(*list)->data=temp;
}

 

最後測試

int main(void)//測試
{
    Node * head=NULL;
    Node ** gg=&head;
    int i;
    for(i=0;i<10;i++)pushback(gg,i);
    printf("連結串列元素依次為: ");
    printlist(head);
    printf("長度為%d\n",lenlist(head));
    freelist(gg);
    printf("釋放後長度為%d\n",lenlist(head));
    for(i=0;i<10;i++)pushback(gg,i);
    deletelist(gg,0);//頭
    deletelist(gg,9);//尾
    deletelist(gg,5);
    deletelist(gg,100);//不存在
    printf("再次建立連結串列,刪除節點後\n");
    printlist(head);
    freelist(gg);
    for(i=0;i<5;i++)pushback(gg,i);
    insertlist(gg,5,0);//頭
    insertlist(gg,5,5);
    insertlist(gg,5,7);//尾
    insertlist(gg,5,10);//不存在
    printlist(head);
    printf("找到%d\n把3變為100",*findnode(gg,5));
    change(gg,3,100);
    change(gg,11111,1);//不存在
    printlist(head);
}

約瑟夫環-(陣列、迴圈連結串列、數學) 

約瑟夫環(約瑟夫問題)是一個數學的應用問題:已知n個人(以編號1,2,3...n分別表示)圍坐在一張圓桌周圍。從編號為k的人開始報數,數到m的那個人出列;他的下一個人又從1開始報數,數到m的那個人又出列;依規律重複下去,直到圓桌周圍的人全部出列。

 

約瑟夫環運作如下:

1、一群人圍在一起坐成環狀(如:N)

2、從某個編號開始報數(如:S)

3、數到某個數(如:M)的時候,此人出列,下一個人重新報數

4、一直迴圈,直到所有人出列  ,約瑟夫環結束

模擬過程,求出最後的人。

把陣列看成一個環,從第s個元素開始按m-1間隔刪除元素,重複過程,直到元素全部去掉。

 

void Josephus(int a[],int n,int m,int s)
{
    int i,j;
    int k=n;
    for(i=0;i<n;i++)a[i]=i+1;//編號
    i=(s+n-1)%n;
    while(k)
    {
        for(j=1;j<m;j++)i=(i+1)%k;//依次報數,頭尾相連
        printf("%d\n",a[i]);//出局
        for(j=i+1;j<k;j++)a[j-1]=a[j];//刪除本節點
        k--;
    }
    //模擬結束,最後輸出的就是留下的人
}

 

可以用帶頭單迴圈連結串列來求解:

也是一樣的,只是實現不同,給出核心程式碼:

    while(k)
    {
        for(j=1;j<m;j++)
        {
            pr=p;
            p=p->link;
            if(p==head)//頭結點跳過
            {
                pr=p;
                p=p->link;
            }
        }
        k--;
        //列印
        pr->link=p->link;//刪結點
        free(p);
        p=pr->link;//從下一個繼續
    }

雙向迴圈連結串列也可以解,和單連結串列類似,只是不需要保持前趨指標。

 

數學可解:

效率最高


int check_last_del(int n,int m)
{
	int i = 1;
	int ret = 0;
	for (i = 2; i<=n;i++)
        ret = (ret + m) %i;
	return ret+1;//因為ret是從0到n-1,最後別忘了加1。
}

線性表表示集合

集合我們高中都學過吧?

最重要的幾個特點:元素不能重複、各個元素之間沒有關係、沒有順序

集合內的元素可以是單元素或者是集合。

對集合的操作:交集並集差集等,還有對自身的加減等。

需要頻繁的加減元素,所以順序儲存效率較低,但是我們還是說一下是怎麼實現的:

    用01向量表示集合,因為現實中任何一個有窮集合都能對應到一個0、1、2.....n這麼一個序列中。所以可以對應過來,每位的01代表這個元素存在與否即可。

連結儲存表示使用有序連結串列來實現,雖然集合是無序的,但是我們的連結串列可以是有序的。可以按升序排列。而連結串列理論上可以無限增加,所以連結串列可以表示無限集。

下面我們來實現一下:

我們定義一個節點:

typedef int ElemType;
typedef struct SetNode{//節點定義
    ElemType data;//資料
    struct SetNode * link;
}*LinkedSet//集合定義

然後要實現那些操作了,首先想插入吧:我們對於一個新元素,查詢集合中是否存在,存在就不插入,不存在就插入到查詢失敗位置。

刪除也簡單,查詢存在就刪除。

 

我們說兩個集合的操作:

求兩個集合的並:

兩個連結串列,都是升序。把他們去重合並即可。

其實和連結串列歸併的merge過程是一樣的,只是相等的時候插入一個,兩個指標都向後走就行了。

我就再寫一遍吧。

void UnionSet(LinkedSet & A,LinkedSet & B,LinkedSet & C)
{
    SetNode *pa=A->link,*pb=B->link,*pc=C;
    while(pa && pb)//都不為空
    {
        if(pa->data==pb->data)//相等,插一次,兩邊向後
        {
            pc->link=new SetNode;
            pc->data=pa->data;
            pa=pa->link;
            pb=pb->link;
        }
        else if(pa->data<pb->data)//插小的,小的向後
        {
            pc->link=new SetNode;
            pc->data=pa->data;
            pa=pa->link;
        }
        else
        {
            pc->link=new SetNode;
            pc->data=pb->data;
            pb=pb->link;
        }
        pc=pc->link;//注意指標
    }
    if(pa)p=pa;//剩下的接上
    else p=pb;//只執行一個
    while(p)//依次複製
    {
        pc->link=new SetNode;
        pc->data=p->data;
        pc=pc->link;
        p=p->link;
    }
    pc->link=NULL;
}

求兩個集合的交,更簡單,還是這三種情況,誰小誰向後,相等才插入。

void UnionSet(LinkedSet & A,LinkedSet & B,LinkedSet & C)
{
    SetNode *pa=A->link,*pb=B->link,*pc=C;
    while(pa && pb)//都不為空
    {
        if(pa->data==pb->data)//相等,插一次,兩邊向後
        {
            pc->link=new SetNode;
            pc->data=pa->data;
            pa=pa->link;
            pb=pb->link;
            pc=pc->link;//注意指標,就不是每次都向後了,只有插入才向後
        }
        else if(pa->data<pb->data)//小的向後
        {
            pa=pa->link;
        }
        else
        {
            pb=pb->link;
        }
    }
    pc->link=NULL;
}

求兩個集合的差:高中可能沒學這個概念,其實就是A-B,就是B中的元素,A都不能有了。

運算你可以把B元素全過一遍,A中有就去掉,但是這樣時間複雜度太高了,我們需要O(A+B)而不是O(A*B)

因為有序,很好操作,還是兩個指標,

如果AB相同,都向後移。

或者,B小,B就向後移。

如果A小,說明B中不含這個元素,我們把它複製到結果連結串列裡。

 

思想還行,實在懶得寫了,有時間再說吧。

 線性表實現一元多項式操作

 

陣列存放:

不需要記錄冪,下標就是。

比如1,2,3,5表示1+2x+3x^2+5x^3

有了思路,我們很容易定義結構

typedef struct node{
    float * coef;//係數陣列
    int maxSize;//最大容量
    int order;//最高階數
}Polynomial;

先實現求和:我們想求兩個式子a+b,結果存在c中。

邏輯很簡單,就是相加啊。

void Add(Polynomial & A,Polynomial & B,Polynomial & C)
{
    int i;
    int m=A.order;
    int n=B.order;
    for(i=0;i<=m && i<=n;i++)//共有部分加一起
        C.coef[i]=A.coef[i]+B.coef[i];
    while(i<=m)//只會執行一個,作用是把剩下的放入c
        C.coef[i]=A.coef[i];
    while(i<=n)
        C.coef[i]=B.coef[i];
    C.order=(m>n)?m:n;//等於較大項
}

實現乘法:

我們思考一下,兩個多項式怎麼相乘?

把a中每一項都和b中每一項乘一遍就好了。

高中知識

 

void Mul(Polynomial & A,Polynomial & B,Polynomial & C)
{
    int i;
    int m=A.order;
    int n=B.order;
    if(m+n>C.maxSize)
    {
        printf("超限");
        return;
    }
    for(i=0;i<=m+n;i++)//注意範圍,是最高項的冪加起來
        C.coef[i]=0.0;
    for(i=0;i<=m;i++)
    {
        for(j=0;j<=n;j++)
        {
            C.coef[i+j]+=A.coef[i]*B.coef[j];
        }
    }
    C.order=m+n;//注意範圍,是最高項的冪加起來
}

 

利用陣列存放雖然簡單,但是當冪相差很大時,會造成空間上的嚴重浪費(包括時間也是),所以我們考慮採用連結串列儲存。

 

我們思考一下如何儲存和做運算。

 

我們肯定要再用一個變數記錄冪了。每個節點記錄係數和指數。

考慮如何相加:

對於c,其實剛開始是空的,我們首先要實現一個插入功能,然後,遍歷a和b,進一步利用插入函數來不斷尾插。

因為a和b都是升冪排列,所以相加的時候,絕對不會發生結果冪小而後遇到的情況,所以放心的一直插入就好了。

具體實現也比較好想:a和b冪相等就加起來,不等就小的單獨插入,然後指標向後移。

加法就放老師寫的程式碼吧,很漂亮的程式碼:(沒和老師商量,希望不會被打)

老師原地插的,都一樣都一樣

老師原文:http://www.edu2act.net/article/shu-ju-jie-gou-xian-xing-biao-de-jing-dian-ying-yong/

void AddPolyn(polynomial &Pa, polynomial &Pb)
	//多項式的加法:Pa = Pa + Pb,利用兩個多項式的結點構成「和多項式」。 
{
	LinkList ha = Pa;		//ha和hb分別指向Pa和Pb的頭指標
	LinkList hb = Pb;
	LinkList qa = Pa->next;
	LinkList qb = Pb->next;	//ha和hb分別指向pa和pb的前驅
	while (qa && qb)		//如果qa和qb均非空
	{
		float sum = 0.0;
		term a = qa->data;
		term b = qb->data;
		switch (cmp(a,b))
		{
		case -1:	//多項式PA中當前結點的指數值小
			ha = qa;
			qa = qa->next;
			break;
		case 0:		//兩者指數值相等
			sum = a.coef + b.coef;
			if(sum != 0.0)
			{	//修改多項式PA中當前結點的係數值
				qa->data.coef = sum;
				ha = qa;
			}else
			{	//刪除多項式PA中當前結點
				DelFirst(ha, qa);
				free(qa);
			}
			DelFirst(hb, qb);
			free(qb);
			qb = hb->next;
			qa = ha->next;
			break;
		case 1:
			DelFirst(hb, qb);
			InsFirst(ha, qb);
			qb = hb->next;
			ha = ha->next;
			break;
		}//switch
	}//while
	if(!ListEmpty(Pb))
		Append(Pa,qb);
	DestroyList(hb);

}//AddPolyn

對於乘法,我們就不能一直往後插了,因為遍歷兩個式子,可能出現冪變小的情況。所以我們要實現一個插入函數,如果c中有這一項,就加起來,沒這一項就插入。

我們先實現插入函數:(哦,對了,我沒有像老師那樣把係數和指數再定義一個結構體,都放一起了。還有next我寫的link,還有點別的不一樣,都無傷大雅,絕對能看懂)

void Insert(Polynomial &L,float c,int e)//係數c,指數e
{
    Term * pre=L;
    Term * p=L->link;
    while(p && p->exp<e)//查詢
    {
        pre=p;
        p=p->link;
    }
    if(p->exp==e)//如果有這一項
    {
        if(p->coef+c)//如果相加是0了,就刪除節點
        {
            pre->link=p->link;
            free(p);
        }
        else//相加不是0,就合併
        {
            p->coef+=c;
        }
    }
    else//如果沒這一項,插入就好了,連結串列插入寫了很多遍了
    {
            Term * pc=new Term;//建立
            pc->exp=e;
            pc->coef=c;
            pre->link=pc;
            pc->link=p;        
    }
}

插入寫完了,乘法就好實現了,還是兩個迴圈,遍歷a和b,只是最後呼叫Insert方法實現就ok

insert(c,乘係數,加冪)

 

拓展:一維陣列可以模擬一元多項式。類似的,二維陣列可以模擬二元多項式。實現以後有時間寫了再放連結。

 

 

連結串列環問題

1.判斷單連結串列是否有環

  使用兩個slow, fast指標從頭開始掃描連結串列。指標slow 每次走1步,指標fast每次走2步。如果存在環,則指標slow、fast會相遇;如果不存在環,指標fast遇到NULL退出。

  就是所謂的追擊相遇問題:

    

2.求有環單連結串列的環長

   在環上相遇後,記錄第一次相遇點為Pos,之後指標slow繼續每次走1步,fast每次走2步。在下次相遇的時候fast比slow正好又多走了一圈,也就是多走的距離等於環長。

  設從第一次相遇到第二次相遇,設slow走了len步,則fast走了2*len步,相遇時多走了一圈:

    環長=2*len-len。

3.求有環單連結串列的環連線點位置

  第一次碰撞點Pos到連線點Join的距離=頭指標到連線點Join的距離,因此,分別從第一次碰撞點Pos、頭指標head開始走,相遇的那個點就是連線點。

     

  在環上相遇後,記錄第一次相遇點為Pos,連線點為Join,假設頭結點到連線點的長度為LenA,連線點到第一次相遇點的長度為x,環長為R

    第一次相遇時,slow走的長度 S = LenA + x;

    第一次相遇時,fast走的長度 2S = LenA + n*x;

    所以可以知道,LenA + x =  n*R;  LenA = n*R -x;

4.求有環單連結串列的連結串列長

   上述2中求出了環的長度;3中求出了連線點的位置,就可以求出頭結點到連線點的長度。兩者相加就是連結串列的長度。

 

程式設計實現:

  下面是程式碼中的例子:

  

  具體程式碼如下:

#include <stdio.h>
#include <stdlib.h>
typedef struct node{
    int value;
    struct node *next;
}LinkNode,*Linklist;

/// 建立連結串列(連結串列長度,環節點起始位置)
Linklist createList(){
    Linklist head = NULL;
    LinkNode *preNode = head;
    LinkNode *FifthNode = NULL;
    for(int i=0;i<6;i++){
        LinkNode *tt = (LinkNode*)malloc(sizeof(LinkNode));
        tt->value = i;
        tt->next = NULL;
        if(preNode == NULL){
            head = tt;
            preNode = head;
        }
        else{
            preNode->next =tt;
            preNode = tt;
        }

        if(i == 3)
            FifthNode = tt;
    }
    preNode->next = FifthNode;
    return head;
}

///判斷連結串列是否有環
LinkNode* judgeRing(Linklist list){
    LinkNode *fast = list;
    LinkNode *slow = list;

    if(list == NULL)
        return NULL;

    while(true){
        if(slow->next != NULL && fast->next != NULL && fast->next->next != NULL){
            slow = slow->next;
            fast = fast->next->next;
        }
        else
            return NULL;

        if(fast == slow)
            return fast;
    }
}

///獲取連結串列環長
int getRingLength(LinkNode *ringMeetNode){
    int RingLength=0;
    LinkNode *fast = ringMeetNode;
    LinkNode *slow = ringMeetNode;
    for(;;){
        fast = fast->next->next;
        slow = slow->next;
        RingLength++;
        if(fast == slow)
            break;
    }
    return RingLength;
}

///獲取連結串列頭到環連線點的長度
int getLenA(Linklist list,LinkNode *ringMeetNode){
    int lenA=0;
    LinkNode *fast = list;
    LinkNode *slow = ringMeetNode;
    for(;;){
        fast = fast->next;
        slow = slow->next;
        lenA++;
        if(fast == slow)
            break;
    }
    return lenA;
}

///環起始點
///如果有環, 釋放空空間時需要注意.
LinkNode* RingStart(Linklist list, int lenA){
    if (!list || lenA <= 0){
        return NULL;
    }

    int i = 0;
    LinkNode* tmp = list;
    for ( ; i < lenA; ++i){
        if (tmp != NULL){
            tmp = tmp->next;
        }
    }

    return (i == lenA)? tmp : NULL;
}

///釋放空間
int freeMalloc(Linklist list, LinkNode* ringstart){
    bool is_ringstart_free = false; //環起始點只能被釋放空間一次
    LinkNode *nextnode = NULL;

    while(list != NULL){
        nextnode = list->next;
        if (list == ringstart){ //如果是環起始點
            if (is_ringstart_free)
                break;  //如果第二次遇到環起始點addr, 表示已經釋放完成
            else
                is_ringstart_free = true;   //記錄已經釋放一次
        }
        free(list);
        list = nextnode;
    }

    return 0;
}

int main(){
    Linklist list = NULL;
    LinkNode *ringMeetNode  = NULL;
    LinkNode *ringStartNode = NULL;

    int LenA       = 0;
    int RingLength = 0;

    list = createList();
    ringMeetNode = judgeRing(list); //快慢指標相遇點

    if(ringMeetNode == NULL)
        printf("No Ring\n");
    else{
        printf("Have Ring\n");
        RingLength = getRingLength(ringMeetNode);   //環長
        LenA = getLenA(list,ringMeetNode);

        printf("RingLength:%d\n", RingLength);
        printf("LenA:%d\n", LenA);
        printf("listLength=%d\n", RingLength+LenA);
    }

    ringStartNode = RingStart(list, LenA);  //獲取環起始點
    freeMalloc(list, ringStartNode);    //釋放環節點, 有環時需要注意. 採納5樓建議
    return 0;
}

 

移除連結串列元素

 

刪除連結串列中等於給定值 val 的所有節點。

範例:

輸入: 1->2->6->3->4->5->6, val = 6
輸出: 1->2->3->4->5

思路:就刪唄,注意第一個數可能會被刪

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
	public ListNode removeElements(ListNode head, int val) {
		ListNode p = new ListNode(-1);
		p.next = head;
		//因為要刪除的可能是連結串列的第一個元素,所以用一個h節點來做處理
		ListNode h = p;
		while(p.next!=null) {
			if(p.next.val==val) {
				p.next = p.next.next;
			}else{
                p = p.next;
            }	
		}
		return h.next;
	}
}

迴文連結串列

請判斷一個連結串列是否為迴文連結串列。

範例 1:

輸入: 1->2
輸出: false
範例 2:

輸入: 1->2->2->1
輸出: true
進階:
你能否用 O(n) 時間複雜度和 O(1) 空間複雜度解決此題?

思路:逆置前一半,然後從中心出發開始比較即可。

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public boolean isPalindrome(ListNode head) {
        if(head == null || head.next == null) {
            return true;
        }
        ListNode slow = head, fast = head;
        ListNode pre = head, prepre = null;
        while(fast != null && fast.next != null) {
            pre = slow;
            slow = slow.next;
            fast = fast.next.next;
            pre.next = prepre;
            prepre = pre;
        }
        if(fast != null) {
            slow = slow.next;
        }
        while(pre != null && slow != null) {
            if(pre.val != slow.val) {
                return false;
            }
            pre = pre.next;
            slow = slow.next;
        }
        return true;
    }
}

連結串列表示整數,相加

思路:就模仿加法即可。。。題目還貼心的給把順序反過來了。

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
        ListNode ans=new ListNode(0);
        ListNode tempA=l1;
        ListNode tempB=l2;
        ListNode temp=ans;
        int out=0;
        while(tempA!=null || tempB!=null){
            int a=tempA!=null?tempA.val:0;
            int b=tempB!=null?tempB.val:0;
            ans.next=new ListNode((a+b+out)%10);
            ans=ans.next;
            out=(a+b+out)/10;
            if(tempA!=null)tempA=tempA.next;
            if(tempB!=null)tempB=tempB.next;
        }
        if(out!=0){
          ans.next=new ListNode(out);  
        }
        return temp.next;
    }
}

LRU

LRU全稱是Least Recently Used,即最近最久未使用的意思。

LRU演演算法的設計原則是:如果一個資料在最近一段時間沒有被存取到,那麼在將來它被存取的可能性也很小。也就是說,當限定的空間已存滿資料時,應當把最久沒有被存取到的資料淘汰。(這一段是找的,讓大家理解一下什麼是LRU)。

 

說一下我們什麼時候見到過LRU:其實老師們肯定都給大家舉過這麼個例子:你在圖書館,你把書架子裡的書拿到桌子上。。但是桌子是有限的,你有時候不得不把一些書放回去。這就相當於記憶體和硬碟。這個例子都說過吧?

LRU就是記錄你最長時間沒看過的書,就把它放回去。在cache那裡見過吧

 

然後最近在研究redis,又看到了這個LRU,所以就想寫一下吧。

題目:設計一個結構,這個結構可以查詢K-V,但是容量有限,當存不下的時候就要把用的年代最久遠的那個東西扔掉。

其實思路很簡單,我們維護一個雙向連結串列即可,get也就是使用了,我們就把把它提到最安全的位置。新來的KV就依次放即可。

我們就先寫這個雙向連結串列結構

先寫節點結構:

	public static class Node<V> {
		public V value;
		public Node<V> last;//前
		public Node<V> next;//後

		public Node(V value) {
			this.value = value;
		}
	}

然後寫雙向連結串列結構: 我們沒必要把連結串列操作都寫了,分析一下,我們只有三個操作:

1、加節點

2、使用了某個節點就把它調到尾,代表優先順序最高

3、把優先順序最低的移除,也就是去頭部

(不會的,翻我之前的連結串列操作都有寫)

	public static class NodeDoubleLinkedList<V> {
		private Node<V> head;//頭
		private Node<V> tail;//尾

		public NodeDoubleLinkedList() {
			this.head = null;
			this.tail = null;
		}

		public void addNode(Node<V> newNode) {
			if (newNode == null) {
				return;
			}
			if (this.head == null) {//頭空
				this.head = newNode;
				this.tail = newNode;
			} else {//頭不空
				this.tail.next = newNode;
				newNode.last = this.tail;//注意讓本節點前指標指向舊尾
				this.tail = newNode;//指向新尾
			}
		}
/*某個點移到最後*/
		public void moveNodeToTail(Node<V> node) {
			if (this.tail == node) {//是尾
				return;
			}
			if (this.head == node) {//是頭
				this.head = node.next;
				this.head.last = null;
			} else {//中間
				node.last.next = node.next;
				node.next.last = node.last;
			}
			node.last = this.tail;
			node.next = null;
			this.tail.next = node;
			this.tail = node;
		}
/*刪除第一個*/
		public Node<V> removeHead() {
			if (this.head == null) {
				return null;
			}
			Node<V> res = this.head;
			if (this.head == this.tail) {//就一個
				this.head = null;
				this.tail = null;
			} else {
				this.head = res.next;
				res.next = null;
				this.head.last = null;
			}
			return res;
		}

	}

連結串列操作封裝完了就要實現這個結構了。

具體思路程式碼註釋

	public static class MyCache<K, V> {
		//為了kv or vk都能查
		private HashMap<K, Node<V>> keyNodeMap;
		private HashMap<Node<V>, K> nodeKeyMap;
		//用來做優先順序
		private NodeDoubleLinkedList<V> nodeList;
		private int capacity;//容量

		public MyCache(int capacity) {
			if (capacity < 1) {//你容量連1都不給,搗亂呢
				throw new RuntimeException("should be more than 0.");
			}
			this.keyNodeMap = new HashMap<K, Node<V>>();
			this.nodeKeyMap = new HashMap<Node<V>, K>();
			this.nodeList = new NodeDoubleLinkedList<V>();
			this.capacity = capacity;
		}

		public V get(K key) {
			if (this.keyNodeMap.containsKey(key)) {
				Node<V> res = this.keyNodeMap.get(key);
				this.nodeList.moveNodeToTail(res);//使用過了就放到尾部
				return res.value;
			}
			return null;
		}

		public void set(K key, V value) {
			if (this.keyNodeMap.containsKey(key)) {
				Node<V> node = this.keyNodeMap.get(key);
				node.value = value;//放新v
				this.nodeList.moveNodeToTail(node);//我們認為放入舊key也是使用過
			} else {
				Node<V> newNode = new Node<V>(value);
				this.keyNodeMap.put(key, newNode);
				this.nodeKeyMap.put(newNode, key);
				this.nodeList.addNode(newNode);//加進去
				if (this.keyNodeMap.size() == this.capacity + 1) {
					this.removeMostUnusedCache();//放不下就去掉優先順序最低的
				}
			}
		}

		private void removeMostUnusedCache() {
			//刪除頭
			Node<V> removeNode = this.nodeList.removeHead();
			K removeKey = this.nodeKeyMap.get(removeNode);
			//刪除掉兩個map中的記錄
			this.nodeKeyMap.remove(removeNode);
			this.keyNodeMap.remove(removeKey);
		}
	}

LFU

請你為 最不經常使用(LFU)快取演演算法設計並實現資料結構。可以自行百度介紹,非常著名的結構

實現 LFUCache 類:

LFUCache(int capacity) - 用資料結構的容量 capacity 初始化物件
int get(int key) - 如果鍵存在於快取中,則獲取鍵的值,否則返回 -1。
void put(int key, int value) - 如果鍵已存在,則變更其值;如果鍵不存在,請插入鍵值對。當快取達到其容量時,則應該在插入新項之前,使最不經常使用的項無效。在此問題中,當存在平局(即兩個或更多個鍵具有相同使用頻率)時,應該去除 最久未使用 的鍵。
注意「項的使用次數」就是自插入該項以來對其呼叫 get 和 put 函數的次數之和。使用次數會在對應項被移除後置為 0 。

為了確定最不常使用的鍵,可以為快取中的每個鍵維護一個 使用計數器 。使用計數最小的鍵是最久未使用的鍵。

當一個鍵首次插入到快取中時,它的使用計數器被設定為 1 (由於 put 操作)。對快取中的鍵執行 get 或 put 操作,使用計數器的值將會遞增。


你可以為這兩種操作設計時間複雜度為 O(1) 的實現嗎?

// 快取的節點資訊
struct Node {
    int key, val, freq;
    Node(int _key,int _val,int _freq): key(_key), val(_val), freq(_freq){}
};
class LFUCache {
    int minfreq, capacity;
    unordered_map<int, list<Node>::iterator> key_table;
    unordered_map<int, list<Node>> freq_table;
public:
    LFUCache(int _capacity) {
        minfreq = 0;
        capacity = _capacity;
        key_table.clear();
        freq_table.clear();
    }
    
    int get(int key) {
        if (capacity == 0) return -1;
        auto it = key_table.find(key);
        if (it == key_table.end()) return -1;
        list<Node>::iterator node = it -> second;
        int val = node -> val, freq = node -> freq;
        freq_table[freq].erase(node);
        // 如果當前連結串列為空,我們需要在雜湊表中刪除,且更新minFreq
        if (freq_table[freq].size() == 0) {
            freq_table.erase(freq);
            if (minfreq == freq) minfreq += 1;
        }
        // 插入到 freq + 1 中
        freq_table[freq + 1].push_front(Node(key, val, freq + 1));
        key_table[key] = freq_table[freq + 1].begin();
        return val;
    }
    
    void put(int key, int value) {
        if (capacity == 0) return;
        auto it = key_table.find(key);
        if (it == key_table.end()) {
            // 快取已滿,需要進行刪除操作
            if (key_table.size() == capacity) {
                // 通過 minFreq 拿到 freq_table[minFreq] 連結串列的末尾節點
                auto it2 = freq_table[minfreq].back();
                key_table.erase(it2.key);
                freq_table[minfreq].pop_back();
                if (freq_table[minfreq].size() == 0) {
                    freq_table.erase(minfreq);
                }
            } 
            freq_table[1].push_front(Node(key, value, 1));
            key_table[key] = freq_table[1].begin();
            minfreq = 1;
        } else {
            // 與 get 操作基本一致,除了需要更新快取的值
            list<Node>::iterator node = it -> second;
            int freq = node -> freq;
            freq_table[freq].erase(node);
            if (freq_table[freq].size() == 0) {
                freq_table.erase(freq);
                if (minfreq == freq) minfreq += 1;
            }
            freq_table[freq + 1].push_front(Node(key, value, freq + 1));
            key_table[key] = freq_table[freq + 1].begin();
        }
    }
};

合併連結串列

 

將兩個有序連結串列合併為一個新的有序連結串列並返回。新連結串列是通過拼接給定的兩個連結串列的所有節點組成的。 

範例:

輸入:1->2->4, 1->3->4
輸出:1->1->2->3->4->4

 

思路:連結串列歸併。

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        ListNode head=new ListNode(0);
        ListNode temp=head;
        while(l1!=null && l2!=null){
            if(l1.val>l2.val){
                temp.next=l2;
                l2=l2.next;
            }else{
                temp.next=l1;
                l1=l1.next;  
            }
            temp=temp.next;
        }
        if(l1!=null){
            temp.next=l1;
        }else{
            temp.next=l2;
        }
        return head.next;
    }
}

反轉連結串列

反轉一個單連結串列。

範例:

輸入: 1->2->3->4->5->NULL
輸出: 5->4->3->2->1->NULL
 

經典題不解釋,畫圖自己模擬記得清楚

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public ListNode reverseList(ListNode head) {
        ListNode prev = null;
        ListNode curr = head;
        while (curr != null) {
            ListNode nextTemp = curr.next;
            curr.next = prev;
            prev = curr;
            curr = nextTemp;
        }
        return prev;
    }
}

 反轉連結串列2

反轉從位置 m 到 n 的連結串列。請使用一趟掃描完成反轉。

說明:
1 ≤ m ≤ n ≤ 連結串列長度。

範例:

輸入: 1->2->3->4->5->NULL, m = 2, n = 4
輸出: 1->4->3->2->5->NULL

思路:反轉連結串列,只不過是反轉一部分,注意這一部分逆序之前做好記錄,方便逆序完後可以連結上連結串列的其他部分。

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public ListNode reverseBetween(ListNode head, int m, int n) {
        if (head == null) return null;
        ListNode cur = head, prev = null;
        while (m > 1) {
            prev = cur;
            cur = cur.next;
            m--;
            n--;
        }
        ListNode con = prev, tail = cur;
        ListNode third = null;
        while (n > 0) {
            third = cur.next;
            cur.next = prev;
            prev = cur;
            cur = third;
            n--;
        }
        if (con != null) {
            con.next = prev;
        } else {
            head = prev;
        }
        tail.next = cur;
        return head;
    }
}

對連結串列排序

丟人,我就是按插入排序老老實實寫的啊。。。。

別人肯定map了hhh。

對連結串列進行插入排序。


插入排序的動畫演示如上。從第一個元素開始,該連結串列可以被認為已經部分排序(用黑色表示)。
每次迭代時,從輸入資料中移除一個元素(用紅色表示),並原地將其插入到已排好序的連結串列中。

 

插入排序演演算法:

插入排序是迭代的,每次只移動一個元素,直到所有元素可以形成一個有序的輸出列表。
每次迭代中,插入排序只從輸入資料中移除一個待排序的元素,找到它在序列中適當的位置,並將其插入。
重複直到所有輸入資料插入完為止。
 

範例 1:

輸入: 4->2->1->3
輸出: 1->2->3->4
範例 2:

輸入: -1->5->3->4->0
輸出: -1->0->3->4->5

思路:按插入排序思路寫就可以啦,只是注意連結串列操作,比陣列麻煩很多。

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public ListNode insertionSortList(ListNode head) {
        ListNode ans=new ListNode(-1);
        ListNode temp=null;//要插入的地方
        ListNode key=null;//要插入的值
        while(head!=null){
            key=head;
            temp=ans;
            while(temp.next!=null && key.val>temp.next.val){
                temp=temp.next;
            }
            head=head.next;
            key.next=temp.next;
            temp.next=key;
        }
        return ans.next;

    }
}

旋轉連結串列

給定一個連結串列,旋轉連結串列,將連結串列每個節點向右移動 k 個位置,其中 k 是非負數。

範例 1:

輸入: 1->2->3->4->5->NULL, k = 2
輸出: 4->5->1->2->3->NULL
解釋:
向右旋轉 1 步: 5->1->2->3->4->NULL
向右旋轉 2 步: 4->5->1->2->3->NULL
範例 2:

輸入: 0->1->2->NULL, k = 4
輸出: 2->0->1->NULL
解釋:
向右旋轉 1 步: 2->0->1->NULL
向右旋轉 2 步: 1->2->0->NULL
向右旋轉 3 步: 0->1->2->NULL
向右旋轉 4 步: 2->0->1->NULL

思路:找準斷點,直接調指標即可。

注意:長度可能超過連結串列長度,要取模。

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public ListNode rotateRight(ListNode head, int k) {
        if(head==null){
            return null;
        }
        int len=0;
        ListNode temp=head;
        while(temp!=null){
            temp=temp.next;
            len++;
        }
        k=k%len;
        ListNode node=head;
        ListNode fast=head;
        while(k-->0){
            fast=fast.next;
        }
        while(fast.next!=null){
            node=node.next;
            fast=fast.next;
        }
        fast.next=head;
        ListNode ans=node.next;
        node.next=null;
        return ans;

    }
}

 陣列實現棧

學習了改進,利用define typedef比上次寫的連結串列更容易改變功能,方便維護,程式碼更健壯。

大佬別嫌棄,萌新總是很笨,用typedef都想不到。

#include<stdio.h>
#include<stdbool.h>
#define maxsize 10
typedef int datatype;
typedef struct stack
{
    datatype data[maxsize];
    int top;
}Stack;
Stack s;
void init()//初始化
{
    s.top=-1;
}
int Empty()//是否空
{
    if(s.top==-1)return 1;
    return 0;
}
int full()//是否滿
{
    if(s.top==maxsize-1)return 1;
    return 0;
}
void Push(datatype element)//入棧
{
    if(!full()){
        s.top++;
        s.data[s.top]=element;
    }
    else printf("棧滿\n");
}
void Pop()//出棧
{
    if(!Empty()) s.top--;
    else printf("棧空\n");
}
datatype Top()//取棧頂元素
{
    if(!Empty()) return s.data[s.top];
    printf("棧空\n");
}
void Destroy()//銷燬
{
    s.top=-1;
}

測試不寫了。

 

連結串列實現棧

 

棧,是操作受限的線性表,只能在一端進行插入刪除。

其實就是帶尾指標的連結串列,尾插

#include <stdio.h>
#include <stdlib.h>
#define OK 1
#define ERROR 0
#define Status int
#define SElemType int
//只在頭部進行插入和刪除(不帶頭結點)
typedef struct LNode
{
	SElemType data;
	struct LNode *next;
}LNode, *LinkList;

typedef struct 
{
	LNode *top;
	LNode *base;
	int length;
}LinkStack;


Status InitStack(LinkStack &S)
{
	S.base = NULL;
	S.top = NULL;
	S.length = 0;
	return OK;
}

Status GetTop(LinkStack S, SElemType &e)
{
	if(S.length == 0)
		return ERROR;
	e = S.top->data ;
	return OK;
}

Status Push(LinkStack &S, SElemType e)
{
	LNode *newNode = (LNode *)malloc(sizeof(LNode));
	newNode->data = e;
	newNode->next = S.top;
	S.top = newNode;
	if(!S.base)
		S.base = newNode;
	++S.length;
	return OK;
}

Status Pop(LinkStack &S, SElemType &e)
{
	LNode *p = S.top;
	if(S.length == 0)
		return ERROR;
	e = S.top->data;
	S.top = S.top->next;
	free(p);
	--S.length;
	return OK;
}

void PrintStack(LinkStack S)
{
	LNode *p = S.top;
	printf("由棧頂到棧底:");
	while (p)
	{
		printf("%d  ",p->data);
		p = p->next;
	}
	printf("\n");
}


int main(void)
{
	LinkStack LS;
	InitStack(LS);
	Push(LS,11);
	Push(LS,22);
	Push(LS,33);
	Push(LS,44);
	Push(LS,55);
	PrintStack(LS);
	SElemType e ;
	GetTop(LS , e);
	printf("棧頂元素是: %d\n",e);
	Pop(LS,e);
	PrintStack(LS);
	Pop(LS,e);
	PrintStack(LS);



	return 0;
}

陣列實現佇列

 

陣列實現佇列結構:

相對棧結構要難搞一些,佇列的先進先出的,需要一個陣列和三個變數,size記錄已經進來了多少個元素,不需要其它萌新看不懂的知識。

觸底反彈,頭尾追逐的感覺。

迴圈使用陣列。

具體解釋一下觸底反彈:當我們的隊頭已經到了陣列的底,我們就把對頭設為陣列的第一個元素,對於隊尾也是一樣。實現了對陣列的迴圈使用。

#include<stdio.h>
#include<stdbool.h>
#define maxsize 10
typedef int datatype;
typedef struct queue
{
    datatype arr[maxsize];
    int a,b,size;//頭、尾、數量
}queue;
queue s;
void init()//初始化
{
    s.a=0;
    s.b=0;
    s.size=0;
}
int Empty()//判空
{
    if(s.size==0)return 1;
    return 0;
}
int full()//判滿
{
    if(s.size==maxsize)return 1;
    return 0;
}
datatype peek()//檢視隊頭
{
    if(s.size!=0)return s.arr[s.a];
    printf("queue is null\n");
}
datatype poll()//彈出隊頭
{
    int temp=s.a;
    if(s.size!=0)
    {
        s.size--;
        s.a=s.a==maxsize-1? 0 :s.a+1;//觸底反彈
        return s.arr[temp];
    }
    printf("queue is null\n");
}
int push(datatype obj)//放入隊尾
{
    if(s.size!=maxsize)
    {
        s.size++;
        s.arr[s.b]=obj;
        s.b=s.b==maxsize-1? 0 : s.b+1;//觸底反彈
        return 1;
    }
    printf("queue is full\n");
    return 0;
}
//測試
int main()
{
    int i;
    init();
    if(Empty())printf("null\n");
    for(i=0;i<20;i++)push(i);
    while(!Empty())
    {
        printf("%d\n",poll());
    }
    printf("%d",poll());
}

連結串列實現佇列

 

這次寫的還算正規,稍微壓縮了一下程式碼,但是不影響閱讀

畫個圖幫助理解:

F->0->0->0<-R

第一個0不存資料 

 

#include<stdio.h>
#include<malloc.h>
#include<stdlib.h>
typedef int Elementype;//資料型別
//節點結構
typedef struct Node{
    Elementype Element;//資料域
    struct Node * Next;
}NODE,*PNODE;

//    定義佇列結構體
typedef struct QNode {
    PNODE Front;//隊頭
    PNODE Rear;//隊尾
} Queue, *PQueue;

void init(PQueue queue)//初始化
{//頭尾指向同一記憶體空間//頭結點,不存資料
    queue->Front = queue->Rear = (PNODE)malloc(sizeof(NODE));
    queue->Front->Next = NULL;//頭結點指標為空
}

int isEmpty(PQueue queue)//判空·
{
    if(queue->Front == queue->Rear)return 1;
    return 0;
}

void insert(PQueue queue,Elementype data)//入隊
{
    PNODE P = (PNODE)malloc(sizeof(NODE));//初始化
    P->Element = data;
    P->Next = NULL;
    queue->Rear->Next = P;//入隊
    queue->Rear = P;
}

void delete(PQueue queue,int * val)//出隊,用val返回值
{
    if(isEmpty(queue))printf("隊空");
    else
    {
        PNODE  P = queue->Front->Next;//前一元素
        *val = P->Element;//記錄值
        queue->Front->Next = P->Next;//出隊
        //注意一定要加上判斷,手動模擬一下就明白了
        if(P==queue->Rear)queue->Rear = queue->Front;
        free(P);//注意釋放
        P = NULL;
    }
}

void destroy(PQueue queue)//釋放
{
    //從頭開始刪
    while(queue->Front != NULL)//起臨時指標作用,無需再用別的空間
    {
        queue->Rear = queue->Front->Next;
        free(queue->Front);
        queue->Front = queue->Rear;
    }
}
//測試
int main(void)
{
    int i;
    int e;
    Queue a;
    PQueue queue=&a;
    init(queue);
    for(i=0;i<10;i++)
        insert(queue,i);
    while(!isEmpty(queue))//遍歷
    {
        delete(queue,&e);
        printf("%d ",e);
    }
    if(isEmpty(queue))printf("1\n");
    delete(queue,&e);
    destroy(queue);
}

雙棧的實現

利用棧底位置相對不變的特性,可以讓兩個順序棧共用一個空間。

具體實現方法大概有兩種:

一種是奇偶棧,就是所有下標為奇數的是一個棧,偶數是另一個棧。但是這樣一個棧的最大儲存就確定了,並沒有起到互補空缺的作用,我們實現了也就沒有太大意義。

還有一種就是,棧底分別設在陣列的頭和尾。進棧往中間進就可以了。這樣,整個陣列存滿了才會真的棧滿。

 

那我們直接開始程式碼實現

 

首先定義結構體:

typedef struct
{
  int top[2], bot[2];    //棧頂和棧底指標
  int *V;      //棧陣列
  int m;     //棧最大可容納元素個數
}DblStack;

 

初始化雙棧s,長度為n:

void Init(DblStack &S,int n)
{
    S.m = n;
    S.V = new int [n+10];
    S.bot[0] = S.top[0] = -1;
    S.bot[1] = S.top[1] = S.m;  
}

判空:

int EmptyStack0(DblStack S)
{
    if(S.top[0]==-1)return 0;
    else return 1;
}
int EmptyStack1(DblStack S)
{
    if(S.top[1]==S.m)return 0;
    else return 1;
}

判滿:(沒有單獨判斷一個棧的,是判斷整個儲存空間還有沒有地方)

int FullStack(DblStack S)
{
    if(S.top[1]-S.top[0]==1)return 1;
    else return 0;
}

進棧:

void Push0(DblStack &S,int e)
{
    if(S.top[1]-S.top[0]!=1)
    {
        S.top[0]++;
        S.V[S.top[0]] = e;
    }
}
void Push1(DblStack &S,int e)
{
    if(S.top[1]-S.top[0] != 1)
    {
        S.top[1]--;
        S.V[S.top[1]] = e;
    }
}

出棧:

void Pop0(DblStack &S,int &e)
{
    if(S.top[0]!=-1)
    {
        e = S.V[S.top[0]];
        S.top[0]--;
    }
}
void Pop1(DblStack &S,int &e)
{
    if(S.top[1]!=S.m)
    {
        e = S.V[S.top[1]];
        S.top[1]++;
    }
}

 棧/佇列 互相模擬實現

 

用兩個棧來實現一個佇列,完成佇列的Push和Pop操作。 佇列中的元素為int型別。

思路:大概這麼想:用一個輔助棧把進第一個棧的元素倒一下就好了。

比如進棧1,2,3,4,5

第一個棧:

5

4

3

2

1

然後倒到第二個棧裡

1

2

3

4

5

再倒出來,順序為1,2,3,4,5

實現佇列

然後要注意的事情:

1)棧2非空不能往裡面倒數,順序就錯了。棧2沒數再從棧1倒。

2)棧1要倒就一次倒完,不倒完的話,進新數也會循序不對。

import java.util.Stack;
 
public class Solution {
    Stack<Integer> stack1 = new Stack<Integer>();
    Stack<Integer> stack2 = new Stack<Integer>();
     
    public void push(int node) {
        stack1.push(node);
    }
     
    public int pop() {
        if(stack1.empty()&&stack2.empty()){
            throw new RuntimeException("Queue is empty!");
        }
        if(stack2.empty()){
            while(!stack1.empty()){
                stack2.push(stack1.pop());
            }
        }
        return stack2.pop();
    }
}

 

用兩個佇列實現棧,要求同上:

這其實意義不是很大,有些資料結構書上甚至說兩個佇列不能實現棧。

其實是可以的,只是時間複雜度較高,一個彈出操作時間為O(N)。

思路:兩個佇列,編號為1和2.

進棧操作:進1號佇列

出棧操作:把1號佇列全弄到2號佇列裡,剩最後一個別壓入,而是返回。

最後還得把1和2號換一下,因為現在是2號有數,1號空。

 

僅僅有思考價值,不實用。

比如壓入1,2,3

佇列1:1,2,3

佇列2:空

依次彈出1,2,3:

佇列1裡的23進入2號,3彈出

佇列1:空

佇列2:2,3

 

佇列2中3壓入1號,2彈出

佇列1:3

佇列2:空

 

佇列1中只有一個元素,彈出。

 

上程式碼:

public class TwoQueueImplStack {
	Queue<Integer> queue1 = new ArrayDeque<Integer>();
	Queue<Integer> queue2 = new ArrayDeque<Integer>();
//壓入
	public void push(Integer element){
		//都為空,優先1
		if(queue1.isEmpty() && queue2.isEmpty()){
			queue1.add(element);
			return;
		}
		//1為空,2有資料,放入2
		if(queue1.isEmpty()){
			queue2.add(element);
			return;
		}
		//2為空,1有資料,放入1
		if(queue2.isEmpty()){
			queue1.add(element);
			return;
		}
	}
//彈出
	public Integer pop(){
		//兩個都空,異常
		if(queue1.isEmpty() && queue2.isEmpty()){
			try{
				throw new Exception("satck is empty!");
			}catch(Exception e){
				e.printStackTrace();
			}
		}	
		//1空,2有資料,將2中的資料依次放入1,最後一個元素彈出
		if(queue1.isEmpty()){
			while(queue2.size() > 1){
				queue1.add(queue2.poll());
			}
			return queue2.poll();
		}
		
		//2空,1有資料,將1中的資料依次放入2,最後一個元素彈出
		if(queue2.isEmpty()){
			while(queue1.size() > 1){
				queue2.add(queue1.poll());
			}
			return queue1.poll();
		}
		
		return (Integer)null;
	}
//測試
	public static void main(String[] args) {
		TwoQueueImplStack qs = new TwoQueueImplStack();
		qs.push(2);
		qs.push(4);
		qs.push(7);
		qs.push(5);
		System.out.println(qs.pop());
		System.out.println(qs.pop());
		
		qs.push(1);
		System.out.println(qs.pop());
	}
}

 

棧的排序

  一個棧中元素的型別為整型,現在想將該棧從頂到底按從大到小的順序排序,只許申請一個棧。除此之外,可以申請新的變數,但是不能申請額外的資料結構,如何完成排序?

思路:

    將要排序的棧記為stack,申請的輔助棧記為help.在stack上執行pop操作,彈出的元素記為cru.

      如果cru小於或等於help的棧頂元素,則將cru直接壓入help.

      如果cru大於help的棧頂元素,則將help的元素逐一彈出,逐一壓入stack,直到cru小於或等於help的棧頂元素,再將cru壓入help.

一直執行以上操作,直到stack中的全部元素壓入到help,最後將heip中的所有元素逐一壓入stack,完成排序。

 

其實和維持單調棧的思路挺像的,只是彈出後沒有丟棄,接著放。

和基礎排序也挺像。

 

import java.util.Stack;
public class a{
   public static void sortStackByStack(Stack<Integer> stack){
       Stack<Integer> help=new Stack<Integer>();
       while(!stack.isEmpty()){
           int cru=stack.pop();
           while(!help.isEmpty()&&help.peek()<cru){
               stack.push(help.pop());
           }
           help.push(cru);
       }
       while (!help.isEmpty()) {
             stack.push(help.pop());        
    }
   }
}

棧——括號匹配

棧的應用,括號匹配。

經典做法是,遇左括號壓入,遇右括號判斷,和棧頂配對就繼續,不配對或者棧空就錯了。最後判斷是否為空。

程式碼有些麻煩。

 

我是遇左括號壓對應的右括號,最後判斷程式碼就會很簡單:相等即可。

class Solution {
public:
    bool isValid(string s) {
        int len=s.size();
        stack<char> st;
        for(int i=0;i<len;i++){
            if(s[i]=='(')st.push(')');
            else if(s[i]=='[')st.push(']');
            else if(s[i]=='{')st.push('}');
            else if(st.empty())return false;
            else if(st.top()!=s[i])return false;
            else st.pop();
        }
        return st.empty();
    }
};

棧——表示式求值 

今天把表示式求值給搞定吧。

 

問題:給你個表示式,有加減乘除和小括號,讓算出結果。

我們假定計算式是正確的,並且不會出現除數為0等錯誤。

py大法好啊,在保證可讀性的前提下能壓到一共就三十多行程式碼。

其實能壓到不到三十行,但是程式碼就不好看了。。。。

計算函數:

def getvalue(a, b, op):
    if op == "+":return a+b
    elif op == "-":return a-b
    elif op == "*":return a*b
    else:return a/b

 

出棧一個運運算元,兩個數值,計算,將結果入data用於之後計算

def process(data, opt):
    operator = opt.pop()
    num2 = data.pop()
    num1 = data.pop()
    data.append(getvalue(num1, num2, operator))

比較符號優先順序:
乘除運算優先順序比加減高。

op1優先順序比op2高返回True,否則返回False

def compare(op1, op2):
    return op1 in ["*","/"] and op2 in ["+","-"]

主函數:

基本思路:

處理每個數位為一個整數,處理每一項為一個單獨的數位,把括號內處理為一個單獨的數位。

把式子處理為只有整數、加減的式子再最後計算。

def calculate(s):
    data = []#資料棧
    opt = []#操作符棧
    i = 0  #表示式遍歷的索引
    while i < len(s):
        if s[i].isdigit():  # 數位,入棧data
            start = i
            while i+1  < len(s) and s[i + 1].isdigit():
                i += 1
            data.append(int(s[start: i + 1]))  # i為最後一個數位字元的位置
        elif s[i] == ")":  # 右括號,opt出棧,data出棧並計算,結果入data,直到左括號
            while opt[-1] != "(":
                process(data,opt)#優先順序高的一定先彈出
            opt.pop()  # 出棧的一定是左括號
        elif not opt or opt[-1] == "(":opt.append(s[i])#棧空,或棧頂為左括號,入opt
        elif s[i]=="(" or compare(s[i],opt[-1]):opt.append(s[i])#左括號或比棧頂優先順序高,入
        else: #優先順序不比棧頂高,opt出棧同時data出棧並計算,計算結果入data
            while opt and not compare(s[i], opt[-1]):
                if opt[-1] == "(":break  #遇到左括號,停止計算
                process(data, opt)
            opt.append(s[i])
        i += 1  #索引後移
    while opt:
        process(data, opt)
    print(data.pop())

借漢諾塔理解棧與遞迴

我們先說,在一個函數中,呼叫另一個函數。

首先,要意識到,函數中的程式碼和平常所寫程式碼一樣,也都是要執行完的,只有執行完程式碼,或者遇到return,才會停止。

那麼,我們在函數中呼叫函數,執行完了,就會重新回到本函數中,繼續向下執行,直到結束。

在執行其它函數時,本函數相當於中斷了,不執行了。那我們重新回來的時候,要從剛才暫停的地方開始,繼續執行,這期間,所有現場資訊都要原封不動,就相當於時間暫停了一樣,什麼都不能改變,這樣才能做到程式的準確。

所以,通常,在執行另一個函數之前,電腦會將現場資訊壓入一個系統棧,為被呼叫的函數分配儲存區,然後開始執行被調函數。執行完畢後,儲存計算結果,釋放被調函數的空間,按照被調函數裡儲存的返回地址,返回到原函數。

那什麼是遞迴函數呢?

就是多個函數巢狀呼叫。不同的是,這些函數是同一個函數,只是引數可能不同,甚至引數也一樣,只是儲存空間不同。

每一層遞迴所需資訊構成一個棧,每一塊記憶體儲著所有實在引數,所有區域性變數,上一層的返回地址,等等一切現場資訊。每執行完就彈出。

遞迴函數有著廣泛應用,主要適合可以把自身分化成一樣的子問題的問題。比如漢諾塔。

 

漢諾塔:漢諾塔(又稱河內塔)問題是源於印度一個古老傳說的益智玩具。大梵天創造世界的時候做了三根金剛石柱子,在一根柱子上從下往上按照大小順序摞著64片黃金圓盤。大梵天命令婆羅門把圓盤從下面開始按大小順序重新擺放在另一根柱子上。並且規定,在小圓盤上不能放大圓盤,在三根柱子之間一次只能移動一個圓盤。

思路:函數(n,a,b,c)含義是把n個盤子從a柱子搬到c柱子的方法

一個盤子,直接搬過去。

多個盤子,我們把n-1個盤子都移動到另一個柱子上,把最大的搬過去然後把剩下的搬過去。

 

def hanoi(n, a, b, c):
    if n == 1:
        print(a, '-->', c)
    else:
        hanoi(n - 1, a, c, b)
        print(a, '-->', c)
        hanoi(n - 1, b, a, c)
# 呼叫
hanoi(3, 'A', 'B', 'C')

結果:

A --> C
A --> B
C --> B
A --> C
B --> A
B --> C
A --> C

我們的棧:

第一次:

我們把hanoi(3, 'A', 'B', 'C')存了起來,呼叫了hanoi(3-1, 'A', 'C', 'B'),現在棧裡壓入了3, 'A', 'B', 'C',還有函數執行到的位置等現場資訊。然後執行hanoi(3-1, 'A', 'C', 'B'),發現要呼叫hanoi(3-1-1, 'A', 'B', 'C'),我們又把3-1, 'A', 'C', 'B'等資訊壓入了棧,現在棧是這樣的:

棧頭

2, 'A', 'C', 'B'

3, 'A', 'B', 'C'

棧尾

 

然後執行hanoi(3-1-1, 'A', 'B', 'C'),發現n=1了,列印了第一條A --> C,然後釋放掉了hanoi(3-1-1, 'A', 'B', 'C')的空間,並通過記錄的返址回到了hanoi(3-1, 'A', 'C', 'B'),然後執行列印語句A --> B,然後發現要呼叫hanoi(3-1-1, 'C', 'A', 'B'),此時棧又成了:

2, 'A', 'C', 'B'
3, 'A', 'B', 'C'

呼叫hanoi(1, 'A', 'C', 'B')發現可以直接列印,C --> B。

然後我們又回到了2, 'A', 'C', 'B'這裡。發現整個函數執行完了,那就彈出吧。這時棧是這樣的:

3, 'A', 'B', 'C'

只有這一個。

我們繼續執行這個函數的程式碼,發現

def hanoi(n, a, b, c):
    if n == 1:
        print(a, '-->', c)
    else:
        hanoi(n - 1, a, c, b)//執行到了這裡
        print(a, '-->', c)
        hanoi(n - 1, b, a, c)

 

那我們就繼續執行,發現要列印A --> C

然後繼續,發現要呼叫        hanoi(n - 1, b, a, c),那我們繼續把現場資訊壓棧,繼續執行就好了。

 

遞迴就是把大問題分解成小問題進而求解。

具體執行就是通過系統的棧來實現返回原函數的功能。

 轉存失敗重新上傳取消 

 

多色漢諾塔問題:

 

奇數號圓盤著藍色,偶數號圓盤著紅色,如圖所示。現要求將塔座A 上的這一疊圓盤移到塔座B 上,並仍按同樣順序疊置。在移動圓盤時應遵守以下移動規則:

規則(1):每次只能移動1 個圓盤;
規則(2):任何時刻都不允許將較大的圓盤壓在較小的圓盤之上;
規則(3):任何時刻都不允許將同色圓盤疊在一起;
 

其實雙色的漢諾塔就是和無色的漢諾塔演演算法類似,通過推理可知,無色漢諾塔的移動規則在雙色漢諾塔這裡的移動規則並沒有違反。

這裡說明第一種就可以了:Hanoi(n-1,A,C,B);

在移動過程中,塔座上的最低圓盤的編號與n-1具有相同奇偶性,塔座b上的最低圓盤的編號與n-1具有不相同的奇偶性,從而塔座上的最低圓盤的編號與n具有相同的奇偶性,塔座上c最低圓盤的編號與n具有不同的奇偶性;
 

所以把列印操作換成兩個列印即可

 

總:因為遞迴可能會有重複子問題的出現。

就算寫的很好,無重複子問題,也會因為來回撥用、返回函數,而速度較慢。所以,有能力的可以改為迭代或動態規劃等方法。

 

單調棧

通過使用棧這個簡單的結構,我們可以巧妙地降低一些問題的時間複雜度。

單調棧性質:

1、若是單調遞增棧,則從棧頂到棧底的元素是嚴格遞增的。若是單調遞減棧,則從棧頂到棧底的元素是嚴格遞減的。

2、越靠近棧頂的元素越後進棧。(顯而易見)

本文介紹單調棧用法

通過一道題來說明。

POJ2559

1. 題目大意:連結

給出一個柱形統計圖(histogram), 它的每個專案的寬度是1, 高度和具體問題有關。 現在程式設計求出在這個柱形圖中的最大面積的長方形。

7 2 1 4 5 1 3 3

7表示柱形圖有7個資料,分別是 2 1 4 5 1 3 3, 對應的柱形圖如下,最後求出來的面積最大的圖如右圖所示。

做法1:列舉每個起點和終點,矩形面積就是長*最小高度。O(N^3)

做法2:區間最小值優化。O(N^2)

做法3:以每一個下標為中心向兩邊擴,遇到更短的就停,這樣我們可以確定以每一個下標高度為最高的矩形。O(N^2)

單調棧:維護一個單調遞增棧,所有元素各進棧和出棧一次即可。每個元素出棧的時候更新最大的矩形面積。

過程:

1)判斷當前元素小於棧頂

2)條件滿足,就可以更新棧頂元素的最大長度了,並且把棧頂彈出

3)繼續執行(1),直到條件不滿足。

 

重要結論:

1)棧頂下面一個元素一定是,棧頂左邊第一個比棧頂小的元素

2)當前元素一定是,右邊第一個比棧頂小的元素。

為什麼呢?

比如這是個棧

1)如果右邊存在距離更近的比1號小的數,1號早已經彈出了。

2)如果左邊有距離更近的比1號小的數

                如果它比2號小,它會把2號彈出,自己成為2號

                 如果它比2號大,它不會彈出2號,但是它會壓棧,變成2號,原來的2號成為3號。

所以不管怎麼說,這個邏輯是正確的。

最後放程式碼並講解

 

下面看一道難一些的題

LeetCode 85 Maximal Rectangle

1 0 1 0 0

1 0 1 1 1

1 1 1 1 1

1 0 0 1 0

Return 6.二三行後面那六個1

 

給定一個由二進位制組成的矩陣map,找到僅僅包含1的最大矩形,並返回其面積。

這道題是一行一行的做。對每一行都求出每個元素對應的高度,這個高度就是對應的連續1的長度,然後對每一行都更新一次最大矩形面積。

連續1長度也很好更新,本個元素是0,長度就是0,本個元素是1,那就加上之前的。

具體思路程式碼中講解。

import java.util.Stack;

public class MaximalRectangle {

	public static int maxRecSize(int[][] map) {
		if (map == null || map.length == 0 || map[0].length == 0) {
			return 0;
		}
		int maxArea = 0;
		int[] height = new int[map[0].length];
		for (int i = 0; i < map.length; i++) {
			for (int j = 0; j < map[0].length; j++) {
				height[j] = map[i][j] == 0 ? 0 : height[j] + 1;//0長度為0,1長度為前面+1
			}
			maxArea = Math.max(maxRecFromBottom(height), maxArea);//呼叫第一題的思想
		}
		return maxArea;
	}

	//第一題思路
	public static int maxRecFromBottom(int[] height) {
		if (height == null || height.length == 0) {
			return 0;
		}
		int maxArea = 0;
		Stack<Integer> stack = new Stack<Integer>();
		for (int i = 0; i < height.length; i++) {
                //棧非空並且棧頂大
			while (!stack.isEmpty() && height[i] <= height[stack.peek()]) {
				int j = stack.pop();//彈出
				int k = stack.isEmpty() ? -1 : stack.peek();
				int curArea = (i - k - 1) * height[j];//計算最大
				maxArea = Math.max(maxArea, curArea);//更新總體最大
			}
			stack.push(i);//直到棧頂小,壓入新元素
		}
		//最後棧非空,右邊沒有更小元素使它們彈出
		while (!stack.isEmpty()) {
			int j = stack.pop();
			int k = stack.isEmpty() ? -1 : stack.peek();
			int curArea = (height.length - k - 1) * height[j];
			maxArea = Math.max(maxArea, curArea);
		}
		return maxArea;
	}

	public static void main(String[] args) {
		int[][] map = { { 1, 0, 1, 1 }, { 1, 1, 1, 1 }, { 1, 1, 1, 0 }, };
		System.out.println(maxRecSize(map));
	}

}

 

雙端單調佇列

 

這次介紹一種新的資料結構:雙端佇列:雙端佇列是指允許兩端都可以進行入隊和出隊操作的佇列,其元素的邏輯結構仍是線性結構。將佇列的兩端分別稱為前端和後端,兩端都可以入隊和出隊。

堆疊、佇列和優先佇列都可以採用雙端佇列來實現

本文介紹單調雙端佇列的原理及應用。

單調佇列,顧名思義,就是一個元素單調的佇列,那麼就能保證隊首的元素是最小(最大)的,從而滿足最優性問題的需求。

給定一個長度為n的數列,一個k,求所有的min(ai,ai+1.....ai+k-1),i=0,1,....n-k

通俗一點說就是一個長度固定的滑動的視窗,求每個視窗內的最小值。

你當然可以暴力求解,依次遍歷每個視窗.

介紹單調佇列用法:我們維護一個單調佇列

單調佇列呢,以單調遞增序列為例:

1、如果佇列的長度一定,先判斷隊首元素是否在規定範圍內,如果超範圍則增長隊首。

2、每次加入元素時和隊尾比較,如果當前元素小於隊尾且佇列非空,則減小尾指標,隊尾元素依次出隊,直到滿足佇列的調性為止

 

我們說演演算法的優化就是重複計算過程的去除。

按視窗一次次遍歷就是重複計算。最值資訊沒有利用好。

我們為什麼可以這麼維護?

首先,遍歷到的元素肯定在佇列元素之後。

其次,如果當前元素更小的話。

頭部的值比當前元素大,頭部還比當前元素先過期。所以以後計算再也不會用到它了。我們可以放心的去掉它。

下面給出程式碼和解釋

int n,k;//長度為n的數列,視窗為k
int a[MAX_N];//數列
int b[MAX_N];//存放
int deq[MAX_N]//模擬佇列

void solve()
{
    int s = 0,t = 0;//頭和尾
    for(int i=0;i<n;i++)
    {
        //不滿足單調,尾就彈出
        while(s<t && a[deq[t-1]]>=a[i])t--;
        //直到滿足,放入
        deq[t++]=i;
        //計算視窗最大值
        if(i-k+1>=0)b[i-k+1]=a[deq[s];
        //判斷頭過期彈出
        if(deq[s]==i-k+1)s++;
    }
}

基本入門就到這裡。

 單調佇列優化的揹包問題

對於揹包問題,經典的揹包九講已經講的很明白了,本來就不打算寫這方面問題了。

但是吧。

我發現,那個最出名的九講竟然沒寫佇列優化的揹包。。。。

那我必須寫一下咯嘿嘿,這麼好的思想。

 

我們回顧一下揹包問題吧。

 

01揹包問題 


題目 
有N件物品和一個容量為V的揹包。第i件物品的費用是c[i],價值是w[i]。求解將哪些物品裝入揹包可使這些物品的費用總和不超過揹包容量,且價值總和最大。 

這是最基礎的揹包問題,特點是:每種物品僅有一件,可以選擇放或不放。 

f[i][v]表示前i件物品恰放入一個容量為v的揹包可以獲得的最大價值。則其狀態轉移方程便是:

f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]}。 

就是說,對於本物品,我們選擇拿或不拿

比如費用是3.

相關圖解:

我們求表格中黃色部分,只和兩個黑色部分有關

拿了,揹包容量減少,我們價值加上減少後最大價值。

不拿,最大價值等於沒有這件物品,揹包不變,的最大價值。

完全揹包問題 


題目 
有N種物品和一個容量為V的揹包,每種物品都有無限件可用。第i種物品的費用是c[i],價值是w[i]。求解將哪些物品裝入揹包可使這些物品的費用總和不超過揹包容量,且價值總和最大。 


基本思路 
這個問題非常類似於01揹包問題,所不同的是每種物品有無限件。

f[i][v]=max{f[i-1][v],f[i][v-c[i]]+w[i]}

圖解:

因為我們拿了本物品還可以繼續拿無限件,對於當前物品,無論之前拿沒拿,還可以繼續拿,所以是f[i][v-c[i]]+w[i]

 

換一個角度說明這個問題為什麼可以f[i][v-c[i]]+w[i],也就是同一排。

其實是這樣的,我們對於黃色部分,也就是當前物品,有很多種選擇,可以拿一個,兩個。。。一直到揹包容量不夠了。

也就是說,可以不拿,也就是J1,可以拿一個,也就是G1+w[i],也可以拿兩個,也就是D1+2w[i],拿三個,A1+3w[i]。

但是我們看G2,G2其實已經是之前的最大了:A1+2w[i],D1+w[i],G1他們中最大的,對麼?

既然G2是他們中最大的。

我們怎麼求J2?

是不是隻要求G2+w[i]和J1的最大值就好了。

因為G2把剩下的情況都儲存好了。

 

多重揹包問題 


題目 
有N種物品和一個容量為V的揹包。第i種物品最多有n[i]件可用,每件費用是c[i],價值是w[i]。求解將哪些物品裝入揹包可使這些物品的費用總和不超過揹包容量,且價值總和最大。 

 

和之前的完全揹包不同,這次,每件物品有最多拿n[i]件的限制。

思路一:我們可以把物品全都看成01揹包:比如第i件,我們把它拆成n[i]件一樣的單獨物品即可。

思路二:思路一時間複雜度太高。利用二進位制思路:一個n位二進位制,能表示2^n種狀態,如果這些狀態就是拿了多少物品,我們可以把每一位代表的數都拿出來,比如n[i]=16,我們把它拆成1,2,4,8,1,每一堆物品看成一個單獨物品。

為什麼最後有個一?因為從0到16有十七種狀態,四位不足以表示。我們最後補上第五位1.

把拆出來的物品按01揹包做即可。

思路三:我們可以利用單調佇列:

https://blog.csdn.net/hebtu666/article/details/82720880

再回想完全揹包:為什麼可以那麼做?因為每件物品能拿無限件。所以可以。而多重揹包因為有了最多拿多少的限制,我們就不敢直接從G2中拿數,因為G2可能是拿滿了本物品以後才達到的狀態 。

比如n[i]=2,如果G2的狀態是2w[i],拿了兩個2物品達到最大值,我們的J2就不能再拿本物品了。

如何解決這個問題?就是我給的網址中的,雙端單調佇列

利用視窗最大值的思想。

大家想想怎麼實現再看下文。

 

發現問題了嗎?

我們求出J2以後,按原來的做法,是該求K2的,但是K2所需要的資訊和J2完全不同,紅色才是K2可能需要的資訊。

所以我們以物品重量為差,先把黑色系列推出來,再推紅色系列,依此類推。

這個例子就是推三次,每組各元素之間差3.

這樣就不會出現構造一堆單調佇列的尷尬情況了。

在程式碼中繼續詳細解釋:

//輸入
int n;
int W;
int w[MAX_N];
int v[MAX_N];
int m[MAX_N];

 

int dp[MAX_N+1];//壓空間,本知識參考https://blog.csdn.net/hebtu666/article/details/79964233
int deq[MAX_N+1];//雙端佇列,儲存下標
int deqv[MAX_N+1];//雙端佇列,儲存值

佇列存的就是所有上一行能取到的範圍,比如對於J2,佇列裡存的就是G1-w[i],D1-2w[i],A1-3w[i]等等合法情況。(為了操作方便都是j,利用差實現最終的運算)

他們之中最大的就是隊頭,加上最多儲存個數就好。

 

 

 

void solve()
{
    for(int i=0;i<n;i++)//參考過那個網址第二題應該懂
    {
        for(int a=0;a<w[i];a++)//把每個分組都打一遍
        {
            int s=0;//初始化雙端佇列頭尾
            int t=0;
            for(int j=0;j*w[i]+a<=W;j++)//每組第j個元素
            {
                int val=dp[j*w[i]+a]-j*v[i];
                while(s<t && deqv[t-1]<=val)//直到不改變單調性
                    t--;
                deq[t]=j;
                deqv[t]=val;
                t++;
                //利用隊頭求出dp
                dp[j*w[i]+a]=deqv[s]+j*v[i];
                if(deq[s]==j-m[i])s++;//檢查過期
            }
        }
    }
}

 串的定長表示

思想和程式碼都不難,和線性表也差不多,串本來就是資料受限的線性表。

串連線:

 

#include <stdio.h>
#include <string.h>
//串的定長順序儲存表示
#define MAXSTRLEN 255							//使用者可在255以內定義最大串長
typedef unsigned char SString[MAXSTRLEN + 1];	//0號單元存放串的長度

int Concat(SString *T,SString S1,SString S2)
	//用T返回S1和S2聯接而成的新串。若未截斷返回1,若截斷返回0
{
	int i = 1,j,uncut = 0;
	if(S1[0] + S2[0] <= MAXSTRLEN)	//未截斷
	{
		for (i = 1; i <= S1[0]; i++)//賦值時等號不可丟
			(*T)[i] = S1[i];
		for (j = 1; j <= S2[0]; j++)
			(*T)[S1[0]+j] = S2[j];	//(*T)[i+j] = S2[j]
		(*T)[0] = S1[0] + S2[0];
		uncut = 1;
	}
	else if(S1[0] < MAXSTRLEN)		//截斷
	{
		for (i = 1; i <= S1[0]; i++)//賦值時等號不可丟
			(*T)[i] = S1[i];

		for (j = S1[0] + 1; j <= MAXSTRLEN; j++)
		{
			(*T)[j] = S2[j - S1[0] ];
			(*T)[0] = MAXSTRLEN;
			uncut = 0;
		}
	}
	else
	{
		for (i = 0; i <= MAXSTRLEN; i++)
			(*T)[i] = S1[i];
		/*或者分開賦值,先賦值內容,再賦值長度
		for (i = 1; i <= MAXSTRLEN; i++)
			(*T)[i] = S1[i];
		(*T)[0] = MAXSTRLEN;
		*/
		uncut = 0;
	}

	return uncut;
}

int SubString(SString *Sub,SString S,int pos,int len)
	//用Sub返回串S的第pos個字元起長度為len的子串
	//其中,1 ≤ pos ≤ StrLength(S)且0 ≤ len ≤ StrLength(S) - pos + 1(從pos開始到最後有多少字元)
	//第1個字元的下標為1,因為第0個字元存放字元長度
{
	int i;
	if(pos < 1 || pos > S[0] || len < 0 || len > S[0] - pos + 1)
		return 0;
	for (i = 1; i <= len; i++)
	{
		//S中的[pos,len]的元素 -> *Sub中的[1,len]
		(*Sub)[i] = S[pos + i - 1];//下標運運算元 > 定址運運算元的優先順序
	}
	(*Sub)[0] = len;
	return 1;
}
void PrintStr(SString S)
{
	int i;
	for (i = 1; i <= S[0]; i++)
		printf("%c",S[i]);
	printf("\n");
}


int main(void)
{
	/*定長順序儲存初始化和列印的方法
	SString s = {4,'a','b','c','d'};
	int i;
	//s = "abc";	//不可直接賦值
	for (i = 1; i <= s[0]; i++)
		printf("%c",s[i]);
	*/
	SString s1 = {4,'a','b','c','d'};
	SString s2 = {4,'e','f','g','h'},s3;
	SString T,Sub;
	int i;
	
	for (i = 1; i <= 255; i++)
	{
		s3[i] = 'a';
		if(i >= 248)
			s3[i] = 'K';
	}
	s3[0] = 255;
	SubString(&Sub,s3,247,8);
	PrintStr(Sub);
	



	return 0;
}

串的堆分配實現

 

今天,線性結構基本就這樣了,以後(至少是最近)就很少寫線性基礎結構的實現了。

串的型別定義

typedef struct
{
    char *str;
    int length;
}HeapString;


初始化串

InitString(HeapString *S)
{
    S->length=0;
    S->str='\0';
}

長度

int StrEmpty(HeapString S)
/*判斷串是否為空,串為空返回1,否則返回0*/
{
    if(S.length==0)         /*判斷串的長度是否等於0*/
        return 1;           /*當串為空時,返回1;否則返回0*/
    else
        return 0;
}
int StrLength(HeapString S)
/*求串的長度操作*/
{
    return S.length;
}


串的賦值

void StrAssign(HeapString *S,char cstr[])
/*串的賦值操作*/
{
    int i=0,len;
    if(S->str)
        free(S->str);
    for(i=0;cstr[i]!='\0';i++); /*求cstr字串的長度*/
        len=i;
    if(!i)
    {
        S->str=NULL;
        S->length=0;
    }
    else
    {
        S->str=(char*)malloc((len+1)*sizeof(char));
        if(!S->str)
            exit(-1);
        for(i=0;i<len;i++)
            S->str[i]=cstr[i];

        S->length=len;
    }
}


串的複製

void StrAssign(HeapString *S,char cstr[])
/*串的賦值操作*/
{
    int i=0,len;
    if(S->str)
        free(S->str);
    for(i=0;cstr[i]!='\0';i++); /*求cstr字串的長度*/
        len=i;
    if(!i)
    {
        S->str=NULL;
        S->length=0;
    }
    else
    {
        S->str=(char*)malloc((len+1)*sizeof(char));
        if(!S->str)
            exit(-1);
        for(i=0;i<len;i++)
            S->str[i]=cstr[i];

        S->length=len;
    }
}


串的插入

int StrInsert(HeapString *S,int pos,HeapString T)
/*串的插入操作。在S中第pos個位置插入T分為三種情況*/
{
    int i;
    if(pos<0||pos-1>S->length)      /*插入位置不正確,返回0*/
    {
        printf("插入位置不正確");
        return 0;
    }
    S->str=(char*)realloc(S->str,(S->length+T.length)*sizeof(char));
    if(!S->str)
    {
        printf("記憶體分配失敗");
        exit(-1);
    }

    for(i=S->length-1;i>=pos-1;i--)
        S->str[i+T.length]=S->str[i];
    for(i=0;i<T.length;i++)
        S->str[pos+i-1]=T.str[i];

    S->length=S->length+T.length;
    return 1;
}


串的刪除

int StrDelete(HeapString *S,int pos,int len)
/*在串S中刪除pos開始的len個字元*/
{
    int i;
    char *p;
    if(pos<0||len<0||pos+len-1>S->length)
    {
        printf("刪除位置不正確,引數len不合法");
        return 0;
    }
    p=(char*)malloc(S->length-len);             /*p指向動態分配的記憶體單元*/
    if(!p)
        exit(-1);
    for(i=0;i<pos-1;i++)                        /*將串第pos位置之前的字元複製到p中*/
        p[i]=S->str[i];
    for(i=pos-1;i<S->length-len;i++)                /*將串第pos+len位置以後的字元複製到p中*/
        p[i]=S->str[i+len];
    S->length=S->length-len;                    /*修改串的長度*/
    free(S->str);                           /*釋放原來的串S的記憶體空間*/
    S->str=p;                               /*將串的str指向p字串*/
    return 1;
}



串的比較

int StrCompare(HeapString S,HeapString T)
/*串的比較操作*/
{
int i;
for(i=0;i<S.length&&i<T.length;i++) /*比較兩個串中的字元*/
    if(S.str[i]!=T.str[i])          /*如果出現字元不同,則返回兩個字元的差值*/
        return (S.str[i]-T.str[i]);
return (S.length-T.length);             /*如果比較完畢,返回兩個串的長度的差值*/
}


串的連線

int StrCat(HeapString *T,HeapString S)
/*將串S連線在串T的後面*/
{
    int i;
    T->str=(char*)realloc(T->str,(T->length+S.length)*sizeof(char));
    if(!T->str)
    {
        printf("分配空間失敗");
        exit(-1);
    }
    else
    {
        for(i=T->length;i<T->length+S.length;i++)   /*串S直接連線在T的末尾*/
            T->str[i]=S.str[i-T->length];
        T->length=T->length+S.length;           /*修改串T的長度*/
    }
    return 1;
}


清空串

void StrClear(HeapString *S)
/*清空串,只需要將串的長度置為0即可*/
{

    S->str='\0';
    S->length=0;
}


銷燬串

void StrDestroy(HeapString *S)
{
    if(S->str)
        free(S->str);
}

列印

void StrPrint(HeapString S)
{
    int i;
    for(i=0;i<S.length;i++)
    {
        printf("%c",S.str[i]);
    }
    printf("\n");
}

KMP

Kmp操作、原理、拓展

 

 

注:雖然我是一隻菜,才大一。但我是想讓萌新們更容易的學會一些演演算法和思想,所以沒有什麼專業詞語,用的都是比較直白地表達,大佬們可能覺得煩,但是真的對不會的人更有幫助啊。我本人也是菜,大一上學期寫的,直接拿過來了,也沒檢查,有什麼錯誤大佬們趕緊告訴我

先上程式碼,大佬們可以別看下面了,就當複習一下

package advanced_001;

public class Code_KMP {

	public static int getIndexOf(String s, String m) {
		if (s == null || m == null || m.length() < 1 || s.length() < m.length()) {
			return -1;
		}
		char[] str1 = s.toCharArray();
		char[] str2 = m.toCharArray();
		int i1 = 0;
		int i2 = 0;
		int[] next = getNextArray(str2);
		while (i1 < str1.length && i2 < str2.length) {
			if (str1[i1] == str2[i2]) {
				i1++;
				i2++;
			} else if (next[i2] == -1) {
				i1++;
			} else {
				i2 = next[i2];
			}
		}
		return i2 == str2.length ? i1 - i2 : -1;
	}

	public static int[] getNextArray(char[] ms) {
		if (ms.length == 1) {
			return new int[] { -1 };
		}
		int[] next = new int[ms.length];
		next[0] = -1;
		next[1] = 0;
		int i = 2;
		int cn = 0;
		while (i < next.length) {
			if (ms[i - 1] == ms[cn]) {
				next[i++] = ++cn;
			} else if (cn > 0) {
				cn = next[cn];
			} else {
				next[i++] = 0;
			}
		}
		return next;
	}

	public static void main(String[] args) {
		String str = "abcabcababaccc";
		String match = "ababa";
		System.out.println(getIndexOf(str, match));

	}

}

 

問題:給定主串S和子串 T,如果在主串S中能夠找到子串 T,則匹配成功,返回第一個和子串 T 中第一個字元相等的字元在主串S中的序號;否則,稱匹配失敗,返回 0。

 

一、引子

原始演演算法:以主串中每一個位置為開頭,與子串第一個元素匹配,若相同,下一個位置和子串下一個位置匹配,如果子串元素全部匹配成功,則匹配成功,找到位置。

非常傻白甜,很明顯時間複雜度最差為o(len(s)*len(t))。效率很低,大佬請忽略:

 

引出KMP演演算法,概念如下:KMP演演算法是一種改進的字串匹配演演算法,由D.E.Knuth,J.H.Morris和V.R.Pratt同時發現,因此人們稱它為克努特——莫里斯——普拉特操作(簡稱KMP演演算法)。KMP演演算法的關鍵是利用匹配失敗後的資訊,儘量減少模式串與主串的匹配次數以達到快速匹配的目的。具體實現就是實現一個next()函數,函數本身包含了模式串的區域性匹配資訊。時間複雜度O(m+n)。(摘自百度百科)

 

 

其實就是說,人家kmp演演算法時間複雜度o(len(s)+len(t)),非常快了,畢竟你不遍歷一遍這倆字串,怎麼可能匹配出來呢?我不信還有時間複雜度更低的演演算法,包括優化也是常數範圍的優化,時間已經非常優秀了

二、分析總結

分析:首先,我們要搞明白,原始的演演算法為啥這麼慢呢?因為它在一遍一遍的遍歷s和t,做了很多重複工作,浪費了一些我們本該知道的資訊。大大降低了效率。

比如t長度為10,s匹配到位置5,如果t一直匹配到了t[8],到[9]才匹配錯誤,那s已經匹配到了位置14,下一步怎麼做呢?接著從位置6開始,和t[0]開始匹配,而s位置6和t[0]甚至後面很大一部分資訊我們其實都遍歷過,都知道了,原始演演算法卻還要重複匹配這些位置。所以效率極低。

(其實很多演演算法都是對一般方法中的重複運算、操作做了優化,當我們寫出暴力遞迴後,應分析出我們做了哪些重複運算,然後優化。具體優化思路我會在以後寫出來。當我們可以用少量的空間就能減少大量的時間時,何樂而不為呢?)

扯遠了,下面開始進入kmp正題。

三、基本操作

首先扯基本操作:

next陣列概念:一個字串中最長的相同前字尾的長度,加一。可能表達的不太好啊,舉例說明:abcabcabc

所以next[1]一直到next[9]計算的是a,ab,abc,abca,abcab直到abcabcabc的相同的最長字首和最長字尾,加一

注意,所謂字首,不能包含最後一個字元,而字尾,也不能包含第一個字元,如果包含,那所有的next都成了字串長度,也就沒意義了。

比如a,最長前字尾長度為0,原因上面剛說了,不包含。

abca最長前字尾長度為1,即第一個和最後一個。

abcab最長前字尾長度為2,即ab

abcabc最長前字尾長度為3,即abc

abcabca最長前字尾長度為4,即abca

abcabcabc最長前字尾長度為6,即abcabc

萌新可以把next陣列看成一個黑盒,我下面會寫怎麼求,不過現在先繼續講主體思路。

(感覺next陣列體現了一個挺重要的思想:預處理思想。當我們不能直接求解問題時,不妨先生成一個預處理的陣列,用來記錄我們需要的一些資訊。以後我會寫這方面的專題)

 

 

 

 

開始匹配了哦:假如主串從i位置開始和子串配,配到了i+j時配不下去了,按原來的方法,應該回到i+1,繼續配,而kmp演演算法是這樣的:

黑色部分就是配到目前為止,前面子串中的最長相同前字尾。匹配失敗以後,可以跳過我圈的那一部分開頭,從主串的第二塊黑色那裡開始配了,這些開頭肯定配不出來,這就是kmp核心的思想,至於為什麼敢跳,等會講,現在先說基本操作。

根據定義,主串第二塊黑部分和子串第一塊黑部分也一樣,所以直接從我劃線的地方往下配就好。

就這樣操作,直到最後或配出。

 

四、原理

原始的kmp操作就是這樣,下面講解原理,為什麼能確定跳過的那一段開頭肯定配不出來呢?

還是再畫一個圖來配合講解吧。(要不然我怕表達不好唉。。好氣喲)

(懶,就是剛才的圖改了改)

咱們看普遍情況(注意,是普遍情況,任意跳過的開頭位置),隨便一個咱們跳過的開頭,看看有沒有可能配出來呢?

豎線叫abc吧。

主串叫s,子串交t

請看ab線中間包含的t中的子串,它在t中是一個以t[0]為開頭,比黑塊更長的字首。

請看ab線中間包含的s中的子串,它在s中是一個以b線前一個元素為結尾,比黑塊更長的字尾。

請回想黑塊定義:這是目前位置之前的子串中,最長的相同前字尾。

請再想一想我們當初為什麼能配到這裡呢?

 

這個位置之前,我們全都一樣,所以多長的字尾都是相等的。

其實就是,主陣列字尾等於子陣列字尾,而子陣列字首不等於子陣列字尾,所以子陣列字首肯定不等與主陣列字尾,也就是說,當前位置肯定配不出來

 

這是比最長相同前字尾更長的前字尾啊兄弟。。。所以肯定不相等,如果相等,最長相同前字尾至少也是它了啊,對麼?這就是能跳過的原因,這輩子不可能在這裡面配出來了哦。

主要操作和原理就這些了。。不知道解釋清楚沒。

下面解釋如何求解next陣列:

 

當然,一個一個看也不是不可以,在子串很短的情況下演演算法總時間區別不大,但是。。各位有沒有一股似曾相識的感覺呢?計算next[x]還是要在t[0]-t[x-2]這個串裡找最大相同前字尾啊。還是串匹配問題啊。看操作:

(一切為了code簡潔好操作),之後每個位置看看p[i-1]和p[next[i-1]]是不是相等,請回去看圖,也就是第一個黑塊後面那個元素和第二個黑塊最後那個元素,相等,next[i]就等於next[i-1]+1。(求b,看前一個元素的最長前字尾,前一個元素和a看是不是相等。)

若不等,繼續往前看,p[i-1]是不是等於p[next[next[i-1]]],就這樣一直往前跳。其實現在一看,大家是不是感覺,和s與t匹配的時候kmp主體很像啊?只是反過來跳了嘛。。。原理也是基本一樣的,我就不解釋了,跳過的部分也不可能配出來,你們自己證吧,不想寫了。

 

五、複雜度分析

下面分析時間複雜度:

主體部分,在主串上的指標,兩種情況,要麼配了頭一個就不對,就往後走了,這時用o(1)排除了一個位置。要麼就是,配了n個位置以後配不對了,那不管next陣列是多少,主串上的指標總會向後走n個位置的,所以每個位置還是o(1),這樣看來,主串長度是len的話,時間複雜度就是o(len)啊。

再看next陣列求解的操作,一樣的啊,最多就是子串的長度那麼多唄。

所以總體時間複雜度o(m+n),原來是o(m*n)啊,向這位大神致敬,想出這麼強的演演算法。

六、kmp拓展題目

(本來想放到樹專題講,但是kmp提供了很好的思路,故在本章講述kmp方法,在樹專題講一般思路)

輸入兩棵二元樹A,B,判斷B是不是A的子結構。

Oj連結

https://www.nowcoder.com/practice/6e196c44c7004d15b1610b9afca8bd88?tpId=13&tqId=11170&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking

先說一般思路,就一個一個試唄,先在A裡找B的根,相等了接著往下配,全配上就行了。

需要注意的是,子結構的定義,好好理解,不要搞錯了,不太清楚定義的自己查資料。

 

下面說利用kmp解此題的思路

Kmp,解決字串匹配問題,而此題是二元樹匹配問題,所以一般思路是想把樹序列化,然後用kmp,但是我們有一個常識,一種遍歷不能確定唯一一顆樹,這是我們首先要解決的問題。

分析為什麼一個序列不能確定呢?給你個序列建立二元樹,比如1 2 3,先序吧(預設先左子樹),1是根沒問題,2就不一定了,可以是左子樹可以是右子樹,假如是左子樹,那三可放的位置更不確定,這就是原因,我們不知道左子樹是空,結束了,該建右子樹,還是說,填在左子樹。

怎麼解決這個問題?

我請教了敬愛的老師這方法對不對,所以肯定沒有問題滴。

只要把空也表示出來就好了比如最簡單的例子,先序的話就生成1 2 空 空 3 空 空

再舉一例1 2 4 空 空 空 3 空 空

在座的各位都是大佬,應該都懂吧。

(因為序列化和重建的方式一樣,知道左子樹什麼時候為空,所以可以確定唯一一顆結構確定的樹)

AB樹序列化以後,用kmp字串匹配就行啦

(當然要是為了過oj,就別秀kmp操作了,直接用系統函數,面試再自己寫)

 

 

 

整篇結束,code怎麼整合,如何操作、kmp的優化,以及篇中提到的演演算法思想怎麼養成以後可能會寫。

字數3170

 

初稿2017/12/20

 

 18/11/26新增網址和程式碼:

https://blog.csdn.net/hebtu666/article/details/84553147

public class T1SubtreeEqualsT2 {

	public static class Node {
		public int value;
		public Node left;
		public Node right;

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

	public static boolean isSubtree(Node t1, Node t2) {
		String t1Str = serialByPre(t1);
		String t2Str = serialByPre(t2);
		return getIndexOf(t1Str, t2Str) != -1;
	}

	public static String serialByPre(Node head) {
		if (head == null) {
			return "#!";
		}
		String res = head.value + "!";
		res += serialByPre(head.left);
		res += serialByPre(head.right);
		return res;
	}

	// KMP
	public static int getIndexOf(String s, String m) {
		if (s == null || m == null || m.length() < 1 || s.length() < m.length()) {
			return -1;
		}
		char[] ss = s.toCharArray();
		char[] ms = m.toCharArray();
		int[] nextArr = getNextArray(ms);
		int index = 0;
		int mi = 0;
		while (index < ss.length && mi < ms.length) {
			if (ss[index] == ms[mi]) {
				index++;
				mi++;
			} else if (nextArr[mi] == -1) {
				index++;
			} else {
				mi = nextArr[mi];
			}
		}
		return mi == ms.length ? index - mi : -1;
	}

	public static int[] getNextArray(char[] ms) {
		if (ms.length == 1) {
			return new int[] { -1 };
		}
		int[] nextArr = new int[ms.length];
		nextArr[0] = -1;
		nextArr[1] = 0;
		int pos = 2;
		int cn = 0;
		while (pos < nextArr.length) {
			if (ms[pos - 1] == ms[cn]) {
				nextArr[pos++] = ++cn;
			} else if (cn > 0) {
				cn = nextArr[cn];
			} else {
				nextArr[pos++] = 0;
			}
		}
		return nextArr;
	}

	public static void main(String[] args) {
		Node t1 = new Node(1);
		t1.left = new Node(2);
		t1.right = new Node(3);
		t1.left.left = new Node(4);
		t1.left.right = new Node(5);
		t1.right.left = new Node(6);
		t1.right.right = new Node(7);
		t1.left.left.right = new Node(8);
		t1.left.right.left = new Node(9);

		Node t2 = new Node(2);
		t2.left = new Node(4);
		t2.left.right = new Node(8);
		t2.right = new Node(5);
		t2.right.left = new Node(9);

		System.out.println(isSubtree(t1, t2));

	}

}

 

Manacher

Manacher's Algorithm 馬拉車演演算法操作及原理 

package advanced_001;

public class Code_Manacher {

	public static char[] manacherString(String str) {
		char[] charArr = str.toCharArray();
		char[] res = new char[str.length() * 2 + 1];
		int index = 0;
		for (int i = 0; i != res.length; i++) {
			res[i] = (i & 1) == 0 ? '#' : charArr[index++];
		}
		return res;
	}

	public static int maxLcpsLength(String str) {
		if (str == null || str.length() == 0) {
			return 0;
		}
		char[] charArr = manacherString(str);
		int[] pArr = new int[charArr.length];
		int C = -1;
		int R = -1;
		int max = Integer.MIN_VALUE;
		for (int i = 0; i != charArr.length; i++) {
			pArr[i] = R > i ? Math.min(pArr[2 * C - i], R - i) : 1;
			while (i + pArr[i] < charArr.length && i - pArr[i] > -1) {
				if (charArr[i + pArr[i]] == charArr[i - pArr[i]])
					pArr[i]++;
				else {
					break;
				}
			}
			if (i + pArr[i] > R) {
				R = i + pArr[i];
				C = i;
			}
			max = Math.max(max, pArr[i]);
		}
		return max - 1;
	}

	public static void main(String[] args) {
		String str1 = "abc1234321ab";
		System.out.println(maxLcpsLength(str1));
	}

}

問題:查詢一個字串的最長迴文子串

首先敘述什麼是迴文子串:迴文:就是對稱的字串,或者說是正反一樣的

小問題一:請問,子串和子序列一樣麼?請思考一下再往下看

 當然,不一樣。子序列可以不連續,子串必須連續。

舉個例子,123的子串包括1,2,3,12,23,123(一個字串本身是自己的最長子串),而它的子序列是任意選出元素組成,他的子序列有1,2,3,12,13,23,123,」」,空其實也算,但是本文主要是想敘述迴文,沒意義。

小問題二:長度為n的字串有多少個子串?多少個子序列?

 子序列,每個元素都可以選或者不選,所以有2的n次方個子序列(包括空)

子串:以一位置開頭,有n個子串,以二位置開頭,有n-1個子串,以此類推,我們發現,這是一個等差數列,而等差序列求和,有n*(n+1)/2個子串(不包括空)。

(這裡有一個思想需要注意,遇到等差數列求和,基本都是o(n^2)級別的)

一、分析列舉的效率

好,我們來分析一下暴力列舉的時間複雜度,上文已經提到過,一個字串的所有子串,數量是o(n^2)級別,所以光是列舉出所有情況時間就是o(n^2),每一種情況,你要判斷他是不是迴文的話,還需要o(n),情況數和每種情況的時間,應該乘起來,也就是說,列舉時間要o(n^3),效率太低。

二、初步優化

思路:我們知道,迴文全是對稱的,每個迴文串都會有自己的對稱軸,而兩邊都對稱。我們如果從對稱軸開始, 向兩邊闊,如果總相等,就是迴文,擴到兩邊不相等的時候,以這個對稱軸向兩邊擴的最長迴文串就找到了。

舉例:1 2 1 2 1 2 1 1 1

我們用每一個元素作為對稱軸,向兩邊擴

0位置,左邊沒東西,只有自己;

1位置,判斷左邊右邊是否相等,1=1所以接著擴,然後左邊沒了,所以以1位置為對稱軸的最長迴文長度就是3;

2位置,左右都是2,相等,繼續,左右都是1,繼續,左邊沒了,所以最長為5

3位置,左右開始擴,1=1,2=2,1=1,左邊沒了,所以長度是7

如此把每個對稱軸擴一遍,最長的就是答案,對麼?

你要是點頭了。。。自己扇自己兩下。

還有偶迴文呢,,比如1221,123321.這是什麼情況呢?這個對稱軸不是一個具體的數,因為人家是偶迴文。

問題三:怎麼用對稱軸向兩邊擴的方法找到偶迴文?(容易操作的)

我們可以在元素間加上一些符號,比如/1/2/1/2/1/2/1/1/1/,這樣我們再以每個元素為對稱軸擴就沒問題了,每個你加進去的符號都是一個可能的偶數迴文對稱軸,此題可解。。。因為我們沒有錯過任何一個可能的對稱軸,不管是奇數迴文還是偶數迴文。

那麼請問,加進去的符號,有什麼要求麼?是不是必須在原字元中沒出現過?請思考

 

其實不需要的,大家想一下,不管怎麼擴,原來的永遠和原來的比較,加進去的永遠和加進去的比較。(不舉例子說明了,自己思考一下)

好,分析一波時間效率吧,對稱軸數量為o(n)級別,每個對稱軸向兩邊能擴多少?最多也就o(n)級別,一共長度才n; 所以n*n是o(n^2)   (最大能擴的位置其實也是兩個等差數列,這麼理解也是o(n^2),用到剛講的知識)

 

小結:

這種方法把原來的暴力列舉o(n^3)變成了o(n^2),大家想一想為什麼這樣更快呢?

我在kmp一文中就提到過,我們寫出暴力列舉方法後應想一想自己做出了哪些重複計算,錯過了哪些資訊,然後進行優化。

看我們的暴力方法,如果按一般的順序列舉,012345,012判斷完,接著判斷0123,我是沒想到可以利用前面資訊的方法,因為對稱軸不一樣啊,右邊加了一個元素,左邊沒加。所以剛開始,老是想找一種方法,左右都加一個元素,這樣就可以對上一次的資訊加以利用了。

暴力為什麼效率低?永遠是因為重複計算,舉個例子:12121211,下標從0開始,判斷1212121是否為迴文串的時候,其實21212和121等串也就判斷出來了,但是我們並沒有記下結果,當列舉到21212或者121時,我們依舊是重新嘗試了一遍。(假設主串長度為n,對稱軸越在中間,長度越小的子串,被重複嘗試的越多。中間那些點甚至重複了n次左右,本來一次搞定的事)

還是這個例子,我換一個角度敘述一下,比較直觀,如果從3號開始向兩邊擴,121,21212,最後擴到1212121,時間複雜度o(n),用列舉的方法要多少時間?如果主串長度為n,列舉嘗試的子串長度為,3,5,7....n,等差數列,大家讀到這裡應該都知道了,等差數列求和,o(n^2)。

三、Manacher原理

首先告訴大家,這個演演算法時間可以做到o(n),空間o(n).

好的,開始講解這個神奇的演演算法。

首先明白兩個概念:

最右迴文邊界R:挺好理解,就是目前發現的迴文串能延伸到的最右端的位置(一個變數解決)

中心c:第一個取得最右迴文邊界的那個中心對稱軸;舉個例子:12121,二號元素可以擴到12121,三號元素 可以擴到121,右邊界一樣,我們的中心是二號元素,因為它第一個到達最右邊界

當然,我們還需要一個陣列p來記錄每一個可能的對稱軸最後擴到了哪裡。

有了這麼幾個東西,我們就可以開始這個神奇的演演算法了。

為了容易理解,我分了四種情況,依次講解:

 

假設遍歷到位置i,如何操作呢

 

1)i>R:也就是說,i以及i右邊,我們根本不知道是什麼,因為從來沒擴到那裡。那沒有任何優化,直接往右暴力 擴唄。

(下面我們做i關於c的對稱點,i

2)i<R:,

三種情況:

i’的迴文左邊界在c迴文左邊界的裡面

i迴文左邊界在整體迴文的外面

i左邊界和c左邊界是一個元素

(怕你忘了概念,c是對稱中心,c它當初擴到了R,R是目前擴到的最右的地方,現在咱們想以i為中心,看能擴到哪裡。)

按原來o(n^2)的方法,直接向兩邊暴力擴。好的,魔性的優化來了。咱們為了好理解,分情況說。首先,大家應該知道的是,i’其實有人家自己的迴文長度,我們用陣列p記錄了每個位置的情況,所以我們可以知道以i為中心的迴文串有多長。

2-1)i’的迴文左邊界在c迴文的裡面:看圖

我用這兩個括號括起來的就是這兩個點向兩邊擴到的位置,也就是i和i’的迴文串,為什麼敢確定i迴文只有這麼長?和i一樣?我們看c,其實這個圖整體是一個迴文串啊。

串內完全對稱(1是括號左邊相鄰的元素,2是右括號右邊相鄰的元素,34同理),

 由此得出結論1:

由整體迴文可知,點2=點3,點1=點4

 

當初i’為什麼沒有繼續擴下去?因為點1!=點2。

由此得出結論2:點1!=點2 

 

因為前面兩個結論,所以3!=4,所以i也就到這裡就擴不動了。而34中間肯定是迴文,因為整體迴文,和12中間對稱。

 

2-2)i迴文左邊界在整體迴文的外面了:看圖

這時,我們也可以直接確定i能擴到哪裡,請聽分析:

當初c的大回文,擴到R為什麼就停了?因為點2!=點4----------結論1;

2為2關於i的對稱點,當初i左右為什麼能繼續擴呢?說明點2=點2’---------結論2;

由c迴文可知2’=3,由結論2可知點2=點2’,所以2=3;

但是由結論一可知,點2!=點4,所以推出3!=4,所以i擴到34為止了,34不等。

而34中間那一部分,因為c迴文,和i在內部的部分一樣,是迴文,所以34中間部分是迴文。

 

2-3)最後一種當然是i左邊界和c左邊界是一個元素

點1!=點2,點2=點3,就只能推出這些,只知道34中間肯定是迴文,外邊的呢?不知道啊,因為不知道3和4相不相等,所以我們得出結論:點3點4內肯定是,繼續暴力擴。

原理及操作敘述完畢,不知道我講沒講明白。。。

四、程式碼及複雜度分析

 看程式碼大家是不是覺得不像o(n)?其實確實是的,來分析一波。。

首先,我們的i依次往下遍歷,而R(最右邊界)從來沒有回退過吧?其實當我們的R到了最右邊,就可以結束了。再不濟i自己也能把R一個一個懟到最右

我們看情況一和四,R都是以此判斷就向右一個,移動一次需要o(1)

我們看情況二和三,直接確定了p[i],根本不用擴,直接遍歷下一個元素去了,每個元素o(1).

綜上,由於i依次向右走,而R也沒有回退過,最差也就是i和R都到了最右邊,而讓它們移動一次的代價都是o(1)的,所以總體o(n)

可能大家看程式碼依舊有點懵,其實就是code整合了一下,我們對於情況23,雖然知道了它肯定擴不動,但是我們還是給它一個起碼是迴文的範圍,反正它擴一下就沒擴動,不影響時間效率的。而情況四也一樣,給它一個起碼是迴文,不用驗證的區域,然後接著擴,四和二三的區別就是。二三我們已經心中有B樹,它肯定擴不動了,而四確實需要接著嘗試。

(要是寫四種情況當然也可以。。但是我懶的寫,太多了。便於理解分了四種情況解釋,code整合後就是這樣子)

 

字數3411

2017/12/22

 

 

字首樹

是一種雜湊樹的變種。典型應用是用於統計,排序和儲存大量的字元串(但不僅限於字串),所以經常被搜尋引擎系統用於文字詞頻統計。它的優點是:利用字串的公共字首來減少查詢時間,最大限度地減少無謂的字串比較,查詢效率比雜湊樹高。

字典樹又稱為字首樹或Trie樹,是處理字串常見的資料結構。假設組成所有單詞的字元僅是「a」~"z",請實現字典樹結構,幷包含以下四個主要功能:

void insert(String word):新增word,可重複新增。
void delete(String word):刪除word,如果word新增過多次,僅刪除一次。
boolean search(String word):查詢word是否在字典樹中。
int prefixNumber(String pre):返回以字串pre為字首的單詞數量。
思考:

字典樹的介紹。字典樹是一種樹形結構,優點是利用字串的公共字首來節約儲存空間。

 

基本性質:

字典樹的基本性質如下:

  • 根節點沒有字元路徑。除根節點外,每一個節點都被一個字元路徑找到。
  • 從根節點到某一節點,將路徑上經過的字元連線起來,為掃過的對應字串。
  • 每個節點向下所有的字元路徑上的字元都不同。

也不需要記,看了實現,很自然的性質就理解了。

每個結點內有一個指標陣列,裡面有二十六個指標,分別指向二十六個字母。

如果指向某個字母的指標為空,那就是以前沒有遇到過這個字首。

 

搜尋的方法為:

(1) 從根結點開始一次搜尋;

(2) 取得要查詢關鍵詞的第一個字母,並根據該字母選擇對應的子樹並轉到該子樹繼續進行檢索;

(3) 在相應的子樹上,取得要查詢關鍵詞的第二個字母,並進一步選擇對應的子樹進行檢索。

(4) 迭代過程……

(5) 在某個結點處,關鍵詞的所有字母已被取出,則讀取附在該結點上的資訊,即完成查詢。

其他操作類似處理

插入也一樣,只是轉到某個子樹時,沒有子樹,那就建立一個新節點,然後對應指標指向新節點即可。

我們給出定義就更清楚了:

public static class TrieNode {
	public int path; //表示由多少個字串共用這個節點
	public int end;//表示有多少個字串是以這個節點結尾的
	public TrieNode[] map;
    //雜湊表結構,key代表該節點的一條字元路徑,value表示字元路徑指向的節點
	public TrieNode() {
	    path = 0;
	    end = 0;
	    map = new TrieNode[26];
	}
}

path和end都是有用的,接下來會說明

insert:

	    public static class Trie {
	        private TrieNode root;//頭
	 
	        public Trie() {
	            root = new TrieNode();
	        }
	 
	        public void insert(String word) {
	            if (word == null) {
	                return;
	            }//空串
	            char[] chs = word.toCharArray();
	            TrieNode node = root;
	            int index = 0; //哪條路
	            for (int i = 0; i < chs.length; i++) {
	                index = chs[i] - 'a'; //0~25
	                if (node.map[index] == null) {
	                    node.map[index] = new TrieNode();
	                }//建立,繼續
	                node = node.map[index];//指向子樹
	                node.path++;//經過加1
	            }
	            node.end++;//本單詞個數加1
	        }
	        public boolean search(String word) {
	            if (word == null) {
	                return false;
	            }
	            char[] chs = word.toCharArray();
	            TrieNode node = root;
	            int index = 0;
	            for (int i = 0; i < chs.length; i++) {
	                index = chs[i] - 'a';
	                if (node.map[index] == null) {
	                    return false;//找不到
	                }
	                node = node.map[index];
	            }
	            return node.end != 0;//end標記有沒有以這個字元為結尾的字串
	        }

delete: 

	        public void delete(String word) {
                  //如果有
	            if (search(word)) {
	                char[] chs = word.toCharArray();
	                TrieNode node = root;
	                int index = 0;
	                for (int i = 0; i < chs.length; i++) {
	                    index = chs[i] - 'a';
	                    if (node.map[index].path-- == 1) {//path減完之後為0
	                        node.map[index] = null;
	                        return;
	                    }
	                    node = node.map[index];//去子樹
	                }
	                node.end--;//次數減1
	            }
	        }

prefixNumber:

 public int prefixNumber(String pre) {
	            if (pre == null) {
	                return 0;
	            }
	            char[] chs = pre.toCharArray();
	            TrieNode node = root;
	            int index = 0;
	            for (int i = 0; i < chs.length; i++) {
	                index = chs[i] - 'a';
	                if (node.map[index] == null) {
	                    return 0;//找不到
	                }
	                node = node.map[index];
	            }
	            return node.path;//返回經過的次數即可
	        }

好處:

1.利用字串的公共字首來節約儲存空間。

2.最大限度地減少無謂的字串比較,查詢效率比較高。例如:若要查詢的字元長度是5,而總共有單詞的數目是26^5=11881376,利用trie樹,利用5次比較可以從11881376個可能的關鍵字中檢索出指定的關鍵字,而利用二叉查詢樹時間複雜度是O( log2n ),所以至少要進行log211881376=23.5次比較。可以看出來利用字典樹進行查詢速度是比較快的。

 

應用:

<1.字串的快速檢索

<2.字串排序

<3.最長公共字首:abdh和abdi的最長公共字首是abd,遍歷字典樹到字母d時,此時這些單詞的公共字首是abd。

<4.自動匹配字首顯示字尾

我們使用辭典或者是搜尋引擎的時候,輸入appl,後面會自動顯示一堆字首是appl的東東吧。

那麼有可能是通過字典樹實現的,前面也說了字典樹可以找到公共字首,我們只需要把剩餘的字尾遍歷顯示出來即可。

 

相關題目:

一個字串型別的陣列arr1,另一個字串型別的陣列arr2。

arr2中有哪些字元,是arr1中出現的?請列印。

arr2中有哪些字元,是作為arr1中某個字串字首出現的?請列印。

arr2中有哪些字元,是作為arr1中某個字串字首出現的?請列印arr2中出現次數最大的字首。

 

字尾樹/字尾陣列

字典樹:https://blog.csdn.net/hebtu666/article/details/83141560

字尾樹:字尾樹,就是把一串字元的所有字尾儲存並且壓縮的字典樹。

 

相對於字典樹來說,字尾樹並不是針對大量字串的,而是針對一個或幾個字串來解決問題。比如字串的迴文子串,兩個字串的最長公共子串等等。

比如單詞banana,它的所有字尾顯示到下面的。0代表從第一個字元為起點,終點不用說都是字串的末尾。

以上面的字尾,我們建立一顆字尾樹。如下圖,為了方便看到字尾,我沒有合併相同的字首。

把非公共部分壓縮:

字尾樹的應用:

(1)查詢某個字串s1是否在另外一個字串s2中:如果s1在字串s2中,那麼s1必定是s2中某個字尾串的字首。

(2)指定字串s1在字串s2中重複的次數:比如說banana是s1,an是s2,那麼計算an出現的次數實際上就是看an是幾個字尾串的字首。

(3)兩個字串S1,S2的最長公共部分(廣義字尾樹)

(4)最長迴文串(廣義字尾樹)

 

關於字尾樹的實現和應用以後再寫,這次主要寫字尾陣列。

在字串處理當中,字尾樹和字尾陣列都是非常有力的工具。其實字尾陣列是字尾樹的一個非常精巧的替代品,它比字尾樹容易程式設計實現,能夠實現字尾樹的很多功能而時間複雜度也不太遜色,並且,它比字尾樹所佔用的空間小很多。可以說,在資訊學競賽中字尾陣列比字尾樹要更為實用。

 

字尾陣列:就是把某個字串的所有字尾按照字典序排序後的陣列。(陣列中儲存起始位置就好了,結束位置一定是最後)

先說如何計算字尾陣列:

倍增的思想,我們先把每個長度為2的子串排序,再利用結果把每個長度為4的字串排序,再利用結果排序長度為8的子串。。。直到長度大於等於串長。

設定sa[]陣列來記錄排名:sa[i]代表排第i名的是第幾個串。

結果用rank[]陣列返回,rank[i]記錄的是起始位置為第i個字元的字尾排名第幾小。

我們開始執行過程:

比如字串abracadabra

長度為2的排名:a ab ab ac ad br br ca da ra ra,他們分別排第0,1,2,2,3,4,5,5,6,7,8,8名

sa陣列就是11(空串),10(a),0(ab),7,3,5,1,8,4,6,2,9(ra排名最後)

這樣,所有長度為2的子串的排名就出來了,我們如何利用排名把長度為4的排名搞出來呢?

abracadabra中,ab,br,ra這些串排名知道了。我們把他們兩兩合併為長度為4的串,進行排名。

比如abra和brac怎麼比較呢?

用原來排名的數對來表示

abra=ab+ra=1+8

brac=br+ac=4+2

對於字串的字典序,這個例子比1和4就比出來了。

如果第一個數一樣,也就是前兩個字元一樣,那再比後面就可以了。

簡單說就是先比前一半字元的排名,再比後一半的排名。

具體實現,我們可以用系統sort,傳一個比較器就好了。

 

還有需要注意,長度不可能那麼湊巧是2^n,所以 一般的,k=n時,rank[i]表示從位置i開始向後n個字元的排名第幾小,而剩下不足看個字元,rank[i]代表從第i個字元到最後的串的排名第幾小,也就是字尾。

保證了每一個字尾都能正確表示並排序。比如k=4時,就表示出了長度為1,2,3的字尾:a,ra,bra.這就保證了k=8時,長度為5,6,7的字尾也能被表示出來:4+1,4+2,4+3

還有,sa[0]永遠是空串,空串的排名rank[sa[0]]永遠是最大。

int n;
int k;
int rank[MAX_N+1];//結果(排名)陣列
int tmp[MAX_N+1];//臨時陣列
//定義比較器
bool compare(int i,int j)
{
    if(rank[i]!=rank[j])return rank[i]<rank[j];
    //長度為k的子串的比較
    int ri=i+k<=n ? rank[i+k] : -1;
    int rj=j+k<=n ? rank[j+k] : -1;
    return ri<rj;
}

void solve(string s,int *sa)
{
    n=s.length;
    //長度為1時,按字元碼即可,長度為2時就可以直接用
    for(int i=0;i<=n;i++)
    {
        sa[i]=i;
        rank[i]=i<n ? s[i] : -1;//注意空串為最大
    }
    //由k對2k排序,直到超範圍
    for(k=1;k<=n;k*=2)
    {
        sort(sa,sa+n+1,compare);
        tmp[sa[0]=0;//空串
        for(int i=1;i<=n;i++)
        {
            tmp[sa[i]]=tmp[sa[i-1]]+(compare(sa[i-1],sa[i]) ? 1 : 0);//注意有相同的
        }
        for(int i=0;i<=n;i++)
        {
            rank[i]=tmp[i];
        }
    }
}

具體應用以後再寫。。。。。

 

AC自動機

今天寫一下基本的AC自動機的思想原理和實現。

Aho-Corasick automation,該演演算法在1975年產生於貝爾實驗室,是著名的多模匹配演演算法之一。一個常見的例子就是給出n個單詞,再給出一段包含m個字元的文章,讓你找出有多少個單詞在文章裡出現過。要搞懂AC自動機,先得有模式樹(字典樹)Trie和KMP模式匹配演演算法的基礎知識。

KMP演演算法是單模式串的字元匹配演演算法,AC自動機是多模式串的字元匹配演演算法。

首先我們回憶一下KMP演演算法:失配之後,子串通過next陣列找到應該匹配的位置,也就是最長相等前字尾。

AC自動機也是一樣,只不過是匹配到當前失配之後,找到當前字串的字尾,和所有字串的字首,找出最長相等前字尾。

就這麼簡單。

當然,字典樹的知識是需要了解的。

我就預設讀者都會字典樹了。

我們操作的第一步就是把那些單詞做一個字典樹出來,這個好理解。

 

在AC自動機中,我們也有類似next陣列的東西就是fail指標,當發現失配的字元失配的時候,跳轉到fail指標指向的位置,然後再次進行匹配操作

當前節點t有fail指標,其fail指標所指向的節點和t所代表的字元是相同的。因為t匹配成功後,我們需要去匹配t->child,發現失配,那麼就從t->fail這個節點開始再次去進行匹配。

KMP裡有詳細講解過程,我就不佔篇幅敘述了。

然後說一下fail指標如何建立:

和next陣列大同小異。如果你很熟悉next陣列的建立,fail指標也是一樣的。

假設當前節點為father,其孩子節點記為child。求child的Fail指標時,首先我們要找到其father的Fail指標所指向的節點,假如是t的話,我們就要看t的孩子中有沒有和child節點所表示的字母相同的節點,如果有的話,這個節點就是child的fail指標,如果發現沒有,則需要找father->fail->fail這個節點,然後重複上面過程,如果一直找都找不到,則child的Fail指標就要指向root。

KMP也是一樣的的操作:p[next[i-1]]p[next[next[i-1]]]這樣依次往前跳啊。

 

如果跳轉,跳轉後的串的字首,必為跳轉前的模式串的字尾並且跳轉的新位置的深度(匹配字元個數)一定小於跳之前的節點。所以我們可以利用 bfs在 Trie上面進行 fail指標的求解。流程和NEXT陣列類似。

 

匹配的時候流程也是基本一樣的,請參考KMP或者直接看程式碼:

HDU 2222 Keywords Search    最基本的入門題了

就是求目標串中出現了幾個模式串。

很基礎了。使用一個int型的end陣列記錄,查詢一次。

#include <stdio.h>
#include <algorithm>
#include <iostream>
#include <string.h>
#include <queue>
using namespace std;

struct Trie
{
    int next[500010][26],fail[500010],end[500010];
    int root,L;
    int newnode()
    {
        for(int i = 0;i < 26;i++)
            next[L][i] = -1;
        end[L++] = 0;
        return L-1;
    }
    void init()
    {
        L = 0;
        root = newnode();
    }
    void insert(char buf[])
    {
        int len = strlen(buf);
        int now = root;
        for(int i = 0;i < len;i++)
        {
            if(next[now][buf[i]-'a'] == -1)
                next[now][buf[i]-'a'] = newnode();
            now = next[now][buf[i]-'a'];
        }
        end[now]++;
    }
    void build()//建樹
    {
        queue<int>Q;
        fail[root] = root;
        for(int i = 0;i < 26;i++)
            if(next[root][i] == -1)
                next[root][i] = root;
            else
            {
                fail[next[root][i]] = root;
                Q.push(next[root][i]);
            }
        while( !Q.empty() )//建fail
        {
            int now = Q.front();
            Q.pop();
            for(int i = 0;i < 26;i++)
                if(next[now][i] == -1)
                    next[now][i] = next[fail[now]][i];
                else
                {
                    fail[next[now][i]]=next[fail[now]][i];
                    Q.push(next[now][i]);
                }
        }
    }
    int query(char buf[])//匹配
    {
        int len = strlen(buf);
        int now = root;
        int res = 0;
        for(int i = 0;i < len;i++)
        {
            now = next[now][buf[i]-'a'];
            int temp = now;
            while( temp != root )
            {
                res += end[temp];
                end[temp] = 0;
                temp = fail[temp];
            }
        }
        return res;
    }
    void debug()
    {
        for(int i = 0;i < L;i++)
        {
            printf("id = %3d,fail = %3d,end = %3d,chi = [",i,fail[i],end[i]);
            for(int j = 0;j < 26;j++)
                printf("%2d",next[i][j]);
            printf("]\n");
        }
    }
};
char buf[1000010];
Trie ac;
int main()
{
    int T;
    int n;
    scanf("%d",&T);
    while( T-- )
    {
        scanf("%d",&n);
        ac.init();
        for(int i = 0;i < n;i++)
        {
            scanf("%s",buf);
            ac.insert(buf);
        }
        ac.build();
        scanf("%s",buf);
        printf("%d\n",ac.query(buf));
    }
    return 0;
}

 

陣列缺失

 

二元樹遍歷

二元樹:二元樹是每個節點最多有兩個子樹的樹結構。

 

本文介紹二元樹的遍歷相關知識。

我們學過的基本遍歷方法,無非那麼幾個:前序,中序,後序,還有按層遍歷等等。

設L、D、R分別表示遍歷左子樹、存取根結點和遍歷右子樹, 則對一棵二元樹的遍歷有三種情況:DLR(稱為先根次序遍歷),LDR(稱為中根次序遍歷),LRD (稱為後根次序遍歷)。

首先我們定義一顆二元樹

typedef char ElementType;
typedef struct TNode *Position;
typedef Position BinTree;
struct TNode{
    ElementType Data;
    BinTree Left;
    BinTree Right;
};

前序

首先存取根,再先序遍歷左(右)子樹,最後先序遍歷右(左)子樹

思路:

就是利用函數,先列印本個節點,然後對左右子樹重複此過程即可。

void PreorderTraversal( BinTree BT )
{
    if(BT==NULL)return ;
    printf(" %c", BT->Data);
    PreorderTraversal(BT->Left);
    PreorderTraversal(BT->Right);
}

 

中序

首先中序遍歷左(右)子樹,再存取根,最後中序遍歷右(左)子樹

思路:

還是利用函數,先對左邊重複此過程,然後列印根,然後對右子樹重複。

void InorderTraversal( BinTree BT )
{
    if(BT==NULL)return ;
    InorderTraversal(BT->Left);
    printf(" %c", BT->Data);
    InorderTraversal(BT->Right);
}

後序

首先後序遍歷左(右)子樹,再後序遍歷右(左)子樹,最後存取根

思路:

先分別對左右子樹重複此過程,然後列印根

void PostorderTraversal(BinTree BT)
{
    if(BT==NULL)return ;
    PostorderTraversal(BT->Left);
    PostorderTraversal(BT->Right);
    printf(" %c", BT->Data);
}

進一步思考

看似好像很容易地寫出了三種遍歷。。。。。

 

但是你真的理解為什麼這麼寫嗎?

比如前序遍歷,我們真的是按照定義裡所講的,首先存取根,再先序遍歷左(右)子樹,最後先序遍歷右(左)子樹。這種過程來遍歷了一遍二元樹嗎?

仔細想想,其實有一絲不對勁的。。。

再看程式碼:

void Traversal(BinTree BT)//遍歷
{
//1111111111111
    Traversal(BT->Left);
//22222222222222
    Traversal(BT->Right);
//33333333333333
}

為了敘述清楚,我給三個位置編了號 1,2,3

我們憑什麼能前序遍歷,或者中序遍歷,後序遍歷?

我們看,前序中序後序遍歷,實現的程式碼其實是類似的,都是上面這種格式,只是我們分別在位置1,2,3列印出了當前節點而已啊。我們憑什麼認為,在1列印,就是前序,在2列印,就是中序,在3列印,就是後序呢?不管在位置1,2,3哪裡操作,做什麼操作,我們利用函數遍歷樹的順序變過嗎?當然沒有啊。。。

都是三次返回到當前節點的過程:先到本個節點,也就是位置1,然後呼叫了其他函數,最後呼叫完了,我們開到了位置2。然後又呼叫別的函數,呼叫完了,我們來到了位置3.。然後,最後操作完了,這個函數才結束。程式碼裡的三個位置,每個節點都被存取了三次。

而且不管位置1,2,3列印了沒有,操作了沒有,這個順序是永遠存在的,不會因為你在位置1列印了,順序就改為前序,你在位置2列印了,順序就成了中序。

 

為了有更直觀的印象,我們做個試驗:在位置1,2,3全都放入列印操作;

我們會發現,每個節點都被列印了三次。而把每個數第一次出現拿出來,就組成了前序遍歷的序列;所有數位第二次出現拿出來,就組成了中序遍歷的序列。。。。

 

其實,遍歷是利用了一種資料結構:棧

而我們這種寫法,只是通過函數,來讓系統幫我們壓了棧而已。為什麼能實現遍歷?為什麼我們存取完了左子樹,能返回到當前節點?這都是棧的功勞啊。我們把當前節點(對於函數就是當時的現場資訊)存到了棧裡,記錄下來,後來才能把它拿了出來,能回到以前的節點。

 

想到這裡,可能就有更深刻的理解了。

我們能否不用函數,不用系統幫我們壓棧,而是自己做一個棧,來實現遍歷呢?

先序實現思路:拿到一個節點的指標,先判斷是否為空,不為空就先存取(列印)該結點,然後直接進棧,接著遍歷左子樹;為空則要從棧中彈出一個節點來,這個時候彈出的結點就是其父親,然後存取其父親的右子樹,直到當前節點為空且棧為空時,結束。

核心思路程式碼實現:

*p=root;
while(p || !st.empty())
{
    if(p)//非空
    {
        //visit(p);進行操作
        st.push(p);//入棧
        p = p->lchild;左
    } 
    else//空
    {
        p = st.top();//取出
        st.pop();
        p = p->rchild;//右
    }
}

中序實現思路:和前序遍歷一樣,只不過在存取節點的時候順序不一樣,存取節點的時機是從棧中彈出元素時存取,如果從棧中彈出元素,就意味著當前節點父親的左子樹已經遍歷完成,這時候存取父親,就是中序遍歷.

(對應遞迴是第二次遇到)

核心程式碼實現:

*p=root;
while(p || !st.empty())
{
    if(p)//非空
    {
        st.push(p);//壓入
        p = p->lchild;
    }
    else//空
    {
        p = st.top();//取出
        //visit(p);操作
        st.pop();
        p = p->rchild;
    }
}

後序遍歷是最難的。因為要保證左孩子和右孩子都已被存取並且左孩子在右孩子前存取才能存取根結點,這就為流程的控制帶來了難點。

因為我們原來說了,後序是第三次遇到才進行操作的,所以我們很容易有這種和遞迴函數類似的思路:對於任一結點,將其入棧,然後沿其左子樹一直往下走,一直走到沒有左孩子的結點,此時該結點在棧頂,但是不能出棧存取, 因此右孩子還沒存取。所以接下來按照相同的規則對其右子樹進行相同的處理。存取完右孩子,該結點又出現在棧頂,此時可以將其出棧並存取。這樣就保證了正確的存取順序。可以看出,在這個過程中,每個結點都兩次出現在棧頂,只有在第二次出現在棧頂時,才能存取它。因此需要多設定一個變數標識該結點是否是第一次出現在棧頂。

第二種思路:對於任一結點P,先將其入棧。如果P不存在左孩子和右孩子,或者左孩子和右孩子都已被存取過了,就可以直接存取該結點。如果有孩子未存取,將P的右孩子和左孩子依次入棧。

網上的思路大多是第一種,所以我在這裡給出第二種的大概實現吧

首先初始化cur,pre兩個指標,代表存取的當前節點和之前存取的節點。把根放入,開始執行。

s.push(root);
while(!s.empty())
{
    cur=s.top();
    if((cur->lchild==NULL && cur->rchild==NULL)||(pre!=NULL && (pre==cur->lchild||pre==cur->rchild)))
    {
        //visit(cur);  如果當前結點沒有孩子結點或者孩子節點都已被存取過 
        s.pop();//彈出
        pre=cur; //記錄
    }
    else//分別放入右左孩子
    {
        if(cur->rchild!=NULL)
            s.push(cur->rchild);
        if(cur->lchild!=NULL)    
            s.push(cur->lchild);
    }
}

這兩種方法,都是利用棧結構來實現的遍歷,需要一定的棧空間,而其實存在一種時間O(N),空間O(1)的遍歷方式,下次寫了我再放連結。

 

鬥個小機靈:後序是LRD,我們其實已經知道先序是DLR,那其實我們可以用先序來實現後序啊,我們只要先序的時候把左右子樹換一下:DRL(這一步很好做到),然後倒過來不就是DRL了嘛。。。。。就把先序程式碼改的左右反過來,然後放棧裡倒過來就好了,不需要上面介紹的那些複雜的方法。。。。

 

二元樹序列化/反序列化

二元樹被記錄成檔案的過程,為二元樹的序列化

通過檔案重新建立原來的二元樹的過程,為二元樹的反序列化

設計方案並實現。

(已知結點型別為32位元整型)

 

思路:先序遍歷實現。

因為要寫入檔案,我們要把二元樹序列化為一個字串。

首先,我們要規定,一個結點結束後的標誌:「!」

然後就可以通過先序遍歷生成先序序列了。

 

但是,眾所周知,只靠先序序列是無法確定一個唯一的二元樹的,原因分析如下:

比如序列1!2!3!

我們知道1是根,但是對於2,可以作為左孩子,也可以作為右孩子:

對於3,我們仍然無法確定,應該作為左孩子還是右孩子,情況顯得更加複雜:

原因:我們對於當前結點,插入新結點是無法判斷插入位置,是應該作為左孩子,還是作為右孩子。

因為我們的NULL並未表示出來。

如果我們把NULL也用一個符號表示出來:

比如

1!2!#!#!3!#!#!

我們再按照先序遍歷的順序重建:

對於1,插入2時,就確定要作為左孩子,因為左孩子不為空。

然後接下來兩個#,我們就知道了2的左右孩子為空,然後重建1的右子樹即可。

 

我們定義結點:

	public static class Node {
		public int value;
		public Node left;
		public Node right;

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

序列化:

	public static String serialByPre(Node head) {
		if (head == null) {
			return "#!";
		}
		String res = head.value + "!";
		res += serialByPre(head.left);
		res += serialByPre(head.right);
		return res;
	}

 

	public static Node reconByPreString(String preStr) {
        //先把字串轉化為結點序列
		String[] values = preStr.split("!");
		Queue<String> queue = new LinkedList<String>();
		for (int i = 0; i != values.length; i++) {
			queue.offer(values[i]);
		}
		return reconPreOrder(queue);
	}

	public static Node reconPreOrder(Queue<String> queue) {
		String value = queue.poll();
		if (value.equals("#")) {
			return null;//遇空
		}
		Node head = new Node(Integer.valueOf(value));
		head.left = reconPreOrder(queue);
		head.right = reconPreOrder(queue);
		return head;
	}

這樣並未改變先序遍歷的時空複雜度,解決了先序序列確定唯一一顆樹的問題,實現了二元樹序列化和反序列化。

 

先序中序後序兩兩結合重建二元樹

遍歷是對樹的一種最基本的運算,所謂遍歷二元樹,就是按一定的規則和順序走遍二元樹的所有結點,使每一個結點都被存取一次,而且只被存取一次。由於二元樹是非線性結構,因此,樹的遍歷實質上是將二元樹的各個結點轉換成為一個線性序列來表示。

設L、D、R分別表示遍歷左子樹、存取根結點和遍歷右子樹, 則對一棵二元樹的遍歷有三種情況:DLR(稱為先根次序遍歷),LDR(稱為中根次序遍歷),LRD (稱為後根次序遍歷)。

先序遍歷

首先存取根,再先序遍歷左(右)子樹,最後先序遍歷右(左)子樹,C語言程式碼如下:

1

2

3

4

5

6

7

void XXBL(tree *root){

    //DoSomethingwithroot

    if(root->lchild!=NULL)

        XXBL(root->lchild);

    if(root->rchild!=NULL)

        XXBL(root->rchild);

}

中序遍歷

首先中序遍歷左(右)子樹,再存取根,最後中序遍歷右(左)子樹,C語言程式碼如下

1

2

3

4

5

6

7

8

void ZXBL(tree *root)

{

    if(root->lchild!=NULL)

        ZXBL(root->lchild);

        //Do something with root

    if(root->rchild!=NULL)

        ZXBL(root->rchild);

}

後序遍歷

首先後序遍歷左(右)子樹,再後序遍歷右(左)子樹,最後存取根,C語言程式碼如下

1

2

3

4

5

6

7

void HXBL(tree *root){

    if(root->lchild!=NULL)

        HXBL(root->lchild);

    if(root->rchild!=NULL)

        HXBL(root->rchild);

        //Do something with root

}

層次遍歷

即按照層次存取,通常用佇列來做。存取根,存取子女,再存取子女的子女(越往後的層次越低)(兩個子女的級別相同)

 

輸入某二元樹的前序遍歷和中序遍歷的結果,請重建出該二元樹。假設輸入的前序遍歷和中序遍歷的結果中都不含重複的數位。例如輸入前序遍歷序列{1,2,4,7,3,5,6,8}和中序遍歷序列{4,7,2,1,5,3,8,6},則重建二元樹並返回。

 

我們首先找到根結點:一定是先序遍歷序列的第一個元素:1

然後,在中序序列尋找根,把中序序列分為兩個序列左子樹4,7,2和右子樹5,3,8,6

把先序序列也分為兩個:                                           左子樹2,4,7和右子樹3,5,6,8

對左右重複同樣的過程:

先看左子樹:先序序列4,7,2,說明4一定是左子樹的根

把2,4,7分為2和7兩個序列,再重複過程,左邊確定完畢。

右子樹同樣:中序序列為5,3,8,6,先序序列為:3,5,6,8

取先序頭,3.一定是根

把中序序列分為     5和8,6兩個序列

對應的先序序列為 5和6,8兩個序列

 

然後確定了5是3的左孩子

對於先序序列6,8和中序序列8,6

還是先取先序的頭,6

 

現在只有8,中序序列8在左邊,是左孩子。

結束。

我們總結一下這種方法的過程:

1、根據先序序列確定當前樹的根(第一個元素)。

2、在中序序列中找到根,並以根為分界分為兩個序列。

3、這樣,確定了左子樹元素個數,把先序序列也分為兩個。

對左右子樹(對應的序列)重複相同的過程。

 

我們把思路用程式碼實現:

# -*- coding:utf-8 -*-
# class TreeNode:
#     def __init__(self, x):
#         self.val = x
#         self.left = None
#         self.right = None
class Solution:
    # 返回構造的TreeNode根節點
    def reConstructBinaryTree(self, pre, tin):
        # write code here/
        #pre-先序陣列   tin->中序陣列
        if len(pre) == 0:
            return None
        root = TreeNode(pre[0])//第一個元素為根
        pos = tin.index(pre[0])//劃分左右子樹
        root.left = self.reConstructBinaryTree( pre[1:1+pos], tin[:pos])
        root.right = self.reConstructBinaryTree( pre[pos+1:], tin[pos+1:])
        return root

輸入某二元樹的後序遍歷和中序遍歷的結果,請重建出該二元樹。假設輸入的前序遍歷和中序遍歷的結果中都不含重複的數位

 

思路是類似的,只是我們確定根的時候,取後序序列的最後一個元素即可。

 

輸入某二元樹的後序遍歷和先序遍歷的結果,請重建出該二元樹。假設輸入的前序遍歷和中序遍歷的結果中都不含重複的數位

 

我們直白的表述一下,前序是中左右,後序是左右中。

所以,我們憑先序和後序序列其實是無法判斷根的孩子到底是左孩子還是右孩子。

比如先序序列1,5,後序序列是5,1

我們只知道1是這棵樹的根,但是我們不知道5是1的左孩子還是右孩子。

我們的中序序列是左中右,才可以明確的劃分出左右子樹,而先序後序不可以。

 

綜上,只有,只含葉子結點或者同時有左右孩子的結點的樹,才可以被先序序列後序序列確定唯一一棵樹。

最後不斷劃分先序和後序序列完成重建。

 

先序中序陣列推後序陣列

二元樹遍歷

所謂遍歷(Traversal)是指沿著某條搜尋路線,依次對樹中每個結點均做一次且僅做一次存取。存取結點所做的操作依賴於具體的應用問 題。 遍歷是二元樹上最重要的運算之一,是二元樹上進行其它運算之基礎。

 

從二元樹的遞迴定義可知,一棵非空的二元樹由根結點及左、右子樹這三個基本部分組成。因此,在任一給定結點上,可以按某種次序執行三個操作:

⑴存取結點本身(N),

⑵遍歷該結點的左子樹(L),

⑶遍歷該結點的右子樹(R)。

以上三種操作有六種執行次序:

NLR、LNR、LRN、NRL、RNL、RLN。

注意:

前三種次序與後三種次序對稱,故只討論先左後右的前三種次序。

遍歷命名

根據存取結點操作發生位置命名:

① NLR:前序遍歷(Preorder Traversal 亦稱(先序遍歷))

——存取根結點的操作發生在遍歷其左右子樹之前。

② LNR:中序遍歷(Inorder Traversal)

——存取根結點的操作發生在遍歷其左右子樹之中(間)。

③ LRN:後序遍歷(Postorder Traversal)

——存取根結點的操作發生在遍歷其左右子樹之後。

注意:

由於被存取的結點必是某子樹的根,所以N(Node)、L(Left subtree)和R(Right subtree)又可解釋為根、根的左子樹和根的右子樹。NLR、LNR和LRN分別又稱為先根遍歷、中根遍歷和後根遍歷。

 

給出某棵樹的先序遍歷結果和中序遍歷結果(無重複值),求後序遍歷結果。

比如

先序序列為:1,2,4,5,3,6,7,8,9

中序序列為:4,2,5,1,6,3,7,9,8

方法1:我們可以重建整棵樹:

https://blog.csdn.net/hebtu666/article/details/84322113

建議好好看這個網址,對理解這個方法有幫助。

 

如圖

然後後序遍歷得出後序序列。

 

方法2:我們可以不用重建,直接得出:

過程:

1)根據當前先序陣列,設定後序陣列最右邊的值

2)劃分出左子樹的先序、中序陣列和右子樹的先序、中序陣列

3)對右子樹重複同樣的過程

4)對左子樹重複同樣的過程

 

原因:我們的後序遍歷是左右中的,也就是先左子樹,再右子樹,再根

舉個例子:

比如這是待填充序列:

我們確定了根,並且根據根和中序序列劃分出了左右子樹,黃色部分為左子樹:

先處理右子樹(其實左右中反過來就是中右左,順著填就好了):

我們又確定了右子樹的右子樹為黑色區域,然後接著填右子樹的右子樹的根(N)即可。

 

 

舉例說明:

a[]先序序列為:1,2,4,5,3,6,7,8,9

b[]中序序列為:4,2,5,1,6,3,7,9,8

c[]後序序列為:0,0,0,0,0,0,0,0,0(0代表未確定)

我們根據先序序列,知道根一定是1,所以後序序列:0,0,0,0,0,0,0,0,1

從b[]中找到1,並劃分陣列:

          左子樹的先序:2,4,5,

          中序:4,2,5

          右子樹的先序:3,6,7,8,9,

          中序:6,3,7,9,8

 

我們繼續對右子樹重複相同的過程:

(圖示為當前操作的樹,我們是不知道這棵樹的樣子的,我是為了方便敘述,圖片表達一下當前處理的位置)

當前樹的根一定為先序序列的第一個元素,3,所以我們知道後序序列:0,0,0,0,0,0,0,3,1

我們繼續對左右子樹進行劃分,中序序列為6,3,7,9,8,我們在序列中找到2,並劃分為左右子樹:

左子樹:

先序序列:6

中序序列:6

右子樹:

先序序列:7,8,9

中序序列:7,9,8

我們繼續對右子樹重複相同的過程,也就是如圖所示的這棵樹:

現在我們的後序序列為0,0,0,0,0,0,0,3,1

這時我們繼續取當前的根(先序第一個元素)放在下一個後序位置:0,0,0,0,0,0,7,3,1

劃分左右子樹:

左子樹:空,也就是它

右子樹:先序8,9,中序9,8,也就是這個樹

我們繼續處理右子樹:先序序列為8,9,所以根為8,我們繼續填後序陣列0,0,0,0,0,8,7,3,1

然後劃分左右子樹:

左子樹:先序:9,中序:9

右子樹:空

對於左子樹,一樣,我們取頭填後序陣列0,0,0,0,9,8,7,3,1,然後發現左右子樹都為空.

我們就把這個小框框處理完了

然後這棵樹的右子樹就處理完了,處理左子樹,發現為空。這棵樹也處理完了。

這一堆就完了。我們處理以3為根的二元樹的左子樹。繼續填後序陣列:

0,0,0,6,9,8,7,3,1

整棵樹的右子樹處理完了,左子樹同樣重複這個過程。

最後4,5,2,6,9,8,7,3,1

 

好累啊。。。。。。挺簡單個事寫了這麼多。

回憶一下過程:

1)根據當前先序陣列,設定後序陣列最右邊的值

2)劃分出左子樹的先序、中序陣列和右子樹的先序、中序陣列

3)對右子樹重複同樣的過程

4)對左子樹重複同樣的過程

就這麼簡單

 

先填右子樹是為了陣列連續填充,容易理解,先處理左子樹也可以。

最後放上程式碼吧

a=[1,2,4,5,3,6,7,8,9]
b=[4,2,5,1,6,3,7,9,8]
l=[0,0,0,0,0,0,0,0,0]

def f(pre,tin,x,y):
    #x,y為樹在後序陣列中對應的範圍
    if pre==[]:return
    l[y]=pre[0]#根
    pos=tin.index(pre[0])#左子樹元素個數
    f(pre[pos+1:],tin[pos+1:],x+pos,y-1)#處理右子樹
    f(pre[1:pos+1],tin[:pos],x,x+pos-1)#處理左子樹
    
f(a,b,0,len(l)-1)
print(l)

根據陣列建立平衡二元搜尋樹

它是一 棵空樹或它的左右兩個子樹的高度差的絕對值不超過1,並且左右兩個子樹都是一棵平衡二叉(搜尋)樹。

 

二分:用有序陣列中中間的數生成搜尋二元樹的頭節點,然後對陣列的左右部分分別生成左右子樹即可(重複過程)。

生成的二元樹中序遍歷一定還是這個序列。

 

非常簡單,不過多敘述:

public class SortedArrayToBalancedBST {

	public static class Node {
		public int value;
		public Node left;
		public Node right;

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

	public static Node generateTree(int[] sortArr) {
		if (sortArr == null) {
			return null;
		}
		return generate(sortArr, 0, sortArr.length - 1);
	}

	public static Node generate(int[] sortArr, int start, int end) {
		if (start > end) {
			return null;
		}
		int mid = (start + end) / 2;
		Node head = new Node(sortArr[mid]);
		head.left = generate(sortArr, start, mid - 1);
		head.right = generate(sortArr, mid + 1, end);
		return head;
	}

	// for test -- print tree
	public static void printTree(Node head) {
		System.out.println("Binary Tree:");
		printInOrder(head, 0, "H", 17);
		System.out.println();
	}

	public static void printInOrder(Node head, int height, String to, int len) {
		if (head == null) {
			return;
		}
		printInOrder(head.right, height + 1, "v", len);
		String val = to + head.value + to;
		int lenM = val.length();
		int lenL = (len - lenM) / 2;
		int lenR = len - lenM - lenL;
		val = getSpace(lenL) + val + getSpace(lenR);
		System.out.println(getSpace(height * len) + val);
		printInOrder(head.left, height + 1, "^", len);
	}

	public static String getSpace(int num) {
		String space = " ";
		StringBuffer buf = new StringBuffer("");
		for (int i = 0; i < num; i++) {
			buf.append(space);
		}
		return buf.toString();
	}

	public static void main(String[] args) {
		int[] arr = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
		printTree(generateTree(arr));

	}

}

java整體列印二元樹

一個調的很好的列印二元樹的程式碼。

用空格和^v來表示節點之間的關係。

效果是這樣:

Binary Tree:
                                         v7v       
                        v6v       
                                         ^5^       
       H4H       
                                         v3v       
                        ^2^       
                                         ^1^  

 

對於每個節點,先列印右子樹,然後列印本身,然後列印左子樹。

 

public class fan {
	public static class Node {
		public int value;
		Node left;
		Node right;

		public Node(int data) {
			this.value = data;
		}
	}
	
	public static void printTree(Node head) {
		System.out.println("Binary Tree:");
		printInOrder(head, 0, "H", 17);
		System.out.println();
	}
	
	public static void printInOrder(Node head, int height, String to, int len) {
		if (head == null) {
			return;
		}
		printInOrder(head.right, height + 1, "v", len);
		String val = to + head.value + to;
		int lenM = val.length();
		int lenL = (len - lenM) / 2;
		int lenR = len - lenM - lenL;
		val = getSpace(lenL) + val + getSpace(lenR);
		System.out.println(getSpace(height * len) + val);
		printInOrder(head.left, height + 1, "^", len);
	}

	public static String getSpace(int num) {
		String space = " ";
		StringBuffer buf = new StringBuffer("");
		for (int i = 0; i < num; i++) {
			buf.append(space);
		}
		return buf.toString();
	}

	public static void main(String[] args) {
		Node head = new Node(4);
		head.left = new Node(2);
		head.right = new Node(6);
		head.left.left = new Node(1);
		head.left.right = new Node(3);
		head.right.left = new Node(5);
		head.right.right = new Node(7);
		printTree(head);

	}

}

判斷平衡二元樹

平衡二元樹(Balanced Binary Tree)具有以下性質:它是一棵空樹或它的左右兩個子樹的高度差的絕對值不超過1。並且左右兩個子樹都是一棵平衡二元樹

(不是我們平時意義上的必須為搜尋樹)

判斷一棵樹是否為平衡二元樹:

 

可以暴力判斷:每一顆樹是否為平衡二元樹。

 

分析:

如果左右子樹都已知是平衡二元樹,而左子樹和右子樹高度差絕對值不超過1,本樹就是平衡的。

 

為此我們需要的資訊:左右子樹是否為平衡二元樹。左右子樹的高度。

 

我們需要給父返回的資訊就是:本棵樹是否是平衡的、本棵樹的高度。

 

定義結點和返回值:

	public static class Node {
		public int value;
		public Node left;
		public Node right;

		public Node(int data) {
			this.value = data;
		}
	}
	public static class ReturnType {
		public int level;   //深度
		public boolean isB;//本樹是否平衡
		
		public ReturnType(int l, boolean is) {
			level = l;
			isB = is;
		}
	}

我們把程式碼寫出來:

	// process(head, 1)
	
	public static ReturnType process(Node head, int level) {
		if (head == null) {
			return new ReturnType(level, true);
		}
		//取資訊
		ReturnType leftSubTreeInfo = process(head.left, level + 1);
		if(!leftSubTreeInfo.isB) {
			return new ReturnType(level, false);     //左子樹不是->返回
		}
		ReturnType rightSubTreeInfo = process(head.right, level + 1);
		if(!rightSubTreeInfo.isB) {
			return new ReturnType(level, false);     //右子樹不是->返回
		}
		if (Math.abs(rightSubTreeInfo.level - leftSubTreeInfo.level) > 1) {
			return new ReturnType(level, false);     //左右高度差大於1->返回
		}
		
		return new ReturnType(Math.max(leftSubTreeInfo.level, rightSubTreeInfo.level), true);
		//返回高度和true(當前樹是平衡的)
	}

我們不需要每次都返回高度,用一個全域性變數記錄即可。

對於其它二元樹問題,可能不止一個變數資訊,所以,全域性記錄最好都養成定義陣列的習慣。

下面貼出完整程式碼:

import java.util.LinkedList;
import java.util.Queue;

public class Demo {
	public static class Node {
		public int value;
		public Node left;
		public Node right;

		public Node(int data) {
			this.value = data;
		}
	}
	public static boolean isBalance(Node head) {
		boolean[] res = new boolean[1];
		res[0] = true;
		getHeight(head, 1, res);
		return res[0];
	}
	
	public static class ReturnType {
		public int level;   //深度
		public boolean isB;//本樹是否平衡
		
		public ReturnType(int l, boolean is) {
			level = l;
			isB = is;
		}
	}
	
	// process(head, 1)
	
	public static ReturnType process(Node head, int level) {
		if (head == null) {
			return new ReturnType(level, true);
		}
		//取資訊
		ReturnType leftSubTreeInfo = process(head.left, level + 1);
		if(!leftSubTreeInfo.isB) {
			return new ReturnType(level, false);     //左子樹不是->返回
		}
		ReturnType rightSubTreeInfo = process(head.right, level + 1);
		if(!rightSubTreeInfo.isB) {
			return new ReturnType(level, false);     //右子樹不是->返回
		}
		if (Math.abs(rightSubTreeInfo.level - leftSubTreeInfo.level) > 1) {
			return new ReturnType(level, false);     //左右高度差大於1->返回
		}
		
		return new ReturnType(Math.max(leftSubTreeInfo.level, rightSubTreeInfo.level), true);
		//返回高度和true(當前樹是平衡的
	}

	public static int getHeight(Node head, int level, boolean[] res) {
		if (head == null) {
			return level;//返回高度
		}
		//取資訊
		//相同邏輯
		int lH = getHeight(head.left, level + 1, res);
		if (!res[0]) {
			return level;
		}
		int rH = getHeight(head.right, level + 1, res);
		if (!res[0]) {
			return level;
		}
		if (Math.abs(lH - rH) > 1) {
			res[0] = false;
		}
		return Math.max(lH, rH);//返回高度
	}

	public static void main(String[] args) {
		Node head = new Node(1);
		head.left = new Node(2);
		head.right = new Node(3);
		head.left.left = new Node(4);
		head.left.right = new Node(5);
		head.right.left = new Node(6);
		head.right.right = new Node(7);

		System.out.println(isBalance(head));

	}

}

判斷完全二元樹

完全二元樹的定義: 一棵二元樹,除了最後一層之外都是完全填充的,並且最後一層的葉子結點都在左邊。

https://baike.baidu.com/item/%E5%AE%8C%E5%85%A8%E4%BA%8C%E5%8F%89%E6%A0%91/7773232?fr=aladdin

百度定義

 

思路:層序遍歷二元樹

如果一個結點,左右孩子都不為空,則pop該節點,將其左右孩子入佇列

如果一個結點,左孩子為空,右孩子不為空,則該樹一定不是完全二元樹

如果一個結點,左孩子不為空,右孩子為空;或者左右孩子都為空:::::則該節點之後的佇列中的結點都為葉子節點;該樹才是完全二元樹,否則返回false。

非完全二元樹的例子(對應方法的正確性和必要性):

下面寫程式碼:

定義結點:

    public static class Node {
		public int value;
		public Node left;
		public Node right;

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

方法:

	public static boolean isCBT(Node head) {
		if (head == null) {
			return true;
		}
		Queue<Node> queue = new LinkedList<Node>();
		boolean leaf = false;
		Node l = null;
		Node r = null;
		queue.offer(head);
		while (!queue.isEmpty()) {
			head = queue.poll();
			l = head.left;
			r = head.right;
			if ((leaf && (l != null || r != null)) || (l == null && r != null)) {
				return false;//當前結點不是葉子結點且之前結點有葉子結點 || 當前結點有右孩子無左孩子
			}
			if (l != null) {
				queue.offer(l);
			}
			if (r != null) {
				queue.offer(r);
			} else {
				leaf = true;//無孩子即為葉子結點
			}
		}
		return true;
	}

判斷二元搜尋樹

二叉查詢樹(Binary Search Tree),(又:二元搜尋樹,二叉排序樹)它或者是一棵空樹,或者是具有下列性質的二元樹: 若它的左子樹不空,則左子樹上所有結點的值均小於它的根結點的值; 若它的右子樹不空,則右子樹上所有結點的值均大於它的根結點的值; 它的左、右子樹也分別為二叉排序樹

 

判斷某棵樹是否為二元搜尋樹

 

單純判斷每個結點比左孩子大比右孩子小是不對的。如圖:

15推翻了這種方法。

 

思路:

1)可以根據定義判斷,遞迴進行,如果左右子樹都為搜尋二元樹,且左子樹最大值小於根,右子樹最小值大於根。成立。

2)根據定義,中序遍歷為遞增序列,我們中序遍歷後判斷是否遞增即可。

3)我們可以在中序遍歷過程中判斷之前節點和當前結點的關係,不符合直接返回false即可。

4)進一步通過morris遍歷優化

morris遍歷:https://blog.csdn.net/hebtu666/article/details/83093983

 

	public static class Node {
		public int value;
		public Node left;
		public Node right;

		public Node(int data) {
			this.value = data;
		}
	}
	public static boolean isBST(Node head) {
		if (head == null) {
			return true;
		}
		boolean res = true;
		Node pre = null;
		Node cur1 = head;
		Node cur2 = null;
		while (cur1 != null) {
			cur2 = cur1.left;
			if (cur2 != null) {
				while (cur2.right != null && cur2.right != cur1) {
					cur2 = cur2.right;
				}
				if (cur2.right == null) {
					cur2.right = cur1;
					cur1 = cur1.left;
					continue;
				} else {
					cur2.right = null;
				}
			}
			if (pre != null && pre.value > cur1.value) {
				res = false;
			}
			pre = cur1;
			cur1 = cur1.right;
		}
		return res;
	}

二元搜尋樹實現

本文給出二元搜尋樹介紹和實現

 

首先說它的性質:所有的節點都滿足,左子樹上所有的節點都比自己小,右邊的都比自己大。

 

那這個結構有什麼有用呢?

首先可以快速二分查詢。還可以中序遍歷得到升序序列,等等。。。

基本操作:

1、插入某個數值

2、查詢是否包含某個數值

3、刪除某個數值

 

根據實現不同,還可以實現其他很多種操作。

 

實現思路思路:

前兩個操作很好想,就是不斷比較,大了往左走,小了往右走。到空了插入,或者到空都沒找到。

而刪除稍微複雜一些,有下面這幾種情況:

1、需要刪除的節點沒有左兒子,那就把右兒子提上去就好了。

2、需要刪除的節點有左兒子,這個左兒子沒有右兒子,那麼就把左兒子提上去

3、以上都不滿足,就把左兒子子孫中最大節點提上來。

 

當然,反過來也是成立的,比如右兒子子孫中最小的節點。

 

下面來敘述為什麼可以這麼做。

下圖中A為待刪除節點。

第一種情況:

 

1、去掉A,把c提上來,c也是小於x的沒問題。

2、根據定義可知,x左邊的所有點都小於它,把c提上來不影響規則。

 

第二種情況

 

3、B<A<C,所以B<C,根據剛才的敘述,B可以提上去,c可以放在b右邊,不影響規則

4、同理

 

第三種情況

 

5、注意:是把黑色的提升上來,不是所謂的最右邊的那個,因為當初向左拐了,他一定小。

因為黑色是最大,比B以及B所有的孩子都大,所以讓B當左孩子沒問題

而黑點小於A,也就小於c,所以可以讓c當右孩子

大概證明就這樣。。

下面我們用程式碼實現並通過註釋理解

上次連結串列之類的用的c,迴圈來寫的。這次就c++函數遞迴吧,不同方式練習。

定義

struct node
{
    int val;//資料
    node *lch,*rch;//左右孩子
};

插入

 node *insert(node *p,int x)
 {
     if(p==NULL)//直到空就建立節點
     {
         node *q=new node;
         q->val=x;
         q->lch=q->rch=NULL;
         return p;
     }
     if(x<p->val)p->lch=insert(p->lch,x);
     else p->lch=insert(p->rch,x);
     return p;//依次返回自己,讓上一個函數執行。
 }

查詢

 bool find(node *p,int x)
 {
     if(p==NULL)return false;
     else if(x==p->val)return true;
     else if(x<p->val)return find(p->lch,x);
     else return find(p->rch,x);
 }

刪除

 node *remove(node *p,int x)
 {
      if(p==NULL)return NULL;
      else if(x<p->val)p->lch=remove(p->lch,x);
      else if(x>p->val)p->lch=remove(p->rch,x);
      //以下為找到了之後
      else if(p->lch==NULL)//情況1
      {
          node *q=p->rch;
          delete p;
          return q;
      }
      else if(p->lch->rch)//情況2
      {
          node *q=p->lch;
          q->rch=p->rch;
          delete p;
          return q;
      }
      else
      {
          node *q;
          for(q=p->lch;q->rch->rch!=NULL;q=q->rch);//找到最大節點的前一個
          node *r=q->rch;//最大節點
          q->rch=r->lch;//最大節點左孩子提到最大節點位置
          r->lch=p->lch;//調整黑點左孩子為B
          r->rch=p->rch;//調整黑點右孩子為c
          delete p;//刪除
          return r;//返回給父
      }
      return p;
 }

堆的簡單實現

關於堆不做過多介紹

堆就是兒子的值一定不小於父親的值並且樹的節點都是按照從上到下,從左到右緊湊排列的樹。

(本文為二元堆積)

具體實現並不需要指標二元樹,用陣列儲存並且利用公式找到父子即可。

父:(i-1)/2

子:i*2+1,i*2+2

插入:首先把新數位放到堆的末尾,也就是右下角,然後檢視父的數值,需要交換就交換,重複上述操作直到不需交換

刪除:把堆的第一個節點賦值為最後一個節點的值,然後刪除最後一個節點,不斷向下交換。

(兩個兒子:嚴格來說要選擇數值較小的那一個)

時間複雜度:和深度成正比,所以n個節點是O(logN)

int heap[MAX_N],sz=0;
//定義陣列和記錄個數的變數

插入程式碼:

void push(int x)
{//節點編號
    int i=sz++;
    while(i>0)
    {
        int p=(i-1)/2;//父
        if(heap[p]<=x)break;//直到大小順序正確跳出迴圈
        heap[i]=heap[p];//把父節點放下來
        i=p;
    }
    heap[i]=x;//最後把自己放上去
    
}

彈出:

int pop()
{
    int ret=heap[0];//儲存好值,最後返回
    int x=heap[--sz];
    while(i*2+1<sz)
    {
        int a=i*2+1;//左孩子
        int b=i*2+2;//右孩子
        if(b<sz && heap[b]<heap[a])a=b;//找最小
        if(heap[a]>=x)break;//直到不需要交換就退出
        heap[i]=heap[a];//把兒子放上來
        i=a;
    }
    head[i]=x;//下沉到正確位置
    return ret;//返回
}

堆應用例題三連

一個資料流中,隨時可以取得中位數。


題目描述:有一個源源不斷地吐出整數的資料流,假設你有足夠的空間來儲存吐出的數。請設計一個名叫MedianHolder的結構,MedianHolder可以隨時取得之前吐出所有樹的中位數。

要求:

1.如果MedianHolder已經儲存了吐出的N個數,那麼任意時刻將一個新的數加入到MedianHolder的過程中,時間複雜度O(logN)。

2.取得已經吐出的N個數整體的中位數的過程,時間複雜度O(1).

 

看這要求就應該感覺到和堆相關吧?

但是進一步沒那麼好想。

設計的MedianHolder中有兩個堆,一個是大根堆,一個是小根堆。大根堆中含有接收的所有數中較小的一半,並且按大根堆的方式組織起來,那麼這個堆的堆頂就是較小一半的數中最大的那個。小根堆中含有接收的所有數中較大的一半,並且按小根堆的方式組織起來,那麼這個堆的堆頂就是較大一半的數中最小的那個。

例如,如果已經吐出的數為6,1,3,0,9,8,7,2.

較小的一半為:0,1,2,3,那麼3就是這一半的陣列成的大根堆的堆頂

較大的一半為:6,7,8,9,那麼6就是這一半的陣列成的小根堆的堆頂

因為此時數的總個數為偶數,所以中位數就是兩個堆頂相加,再除以2.

如果此時新加入一個數10,那麼這個數應該放進較大的一半里,所以此時較大的一半數為6,7,8,9,10,此時6依然是這一半的陣列成的小根堆的堆頂,因為此時數的總個數為奇數,所以中位數應該是正好處在中間位置的數,而此時大根堆有4個數,小根堆有5個數,那麼小根堆的堆頂6就是此時的中位數。

如果此時又新加入一個數11,那麼這個數也應該放進較大的一半里,此時較大一半的數為:6,7,8,9,10,11.這個小根堆大小為6,而大根堆的大小為4,所以要進行如下調整:

1.如果大根堆的size比小根堆的size大2,那麼從大根堆裡將堆頂元素彈出,並放入小根堆裡

2,如果小根堆的size比大根堆的size大2,那麼從小根堆裡將堆頂彈出,並放入大根堆裡。

經過這樣的調整之後,大根堆和小根堆的size相同。

總結如下:

大根堆每時每刻都是較小的一半的數,堆頂為這一堆數的最大值
小根堆每時每刻都是較大的一半的數,堆頂為這一堆數的最小值
新加入的數根據與兩個堆堆頂的大小關係,選擇放進大根堆或者小根堆裡(或者放進任意一個堆裡)
當任何一個堆的size比另一個size大2時,進行如上調整的過程。


這樣隨時都可以知道已經吐出的所有數處於中間位置的兩個數是什麼,取得中位數的操作時間複雜度為O(1),同時根據堆的性質,向堆中加一個新的數,並且調整堆的代價為O(logN)。
 

import java.util.Arrays;
import java.util.Comparator;
import java.util.PriorityQueue;
 
/**
 * 隨時找到資料流的中位數
 * 思路:
 * 利用一個大根堆和一個小根堆去儲存資料,保證前一半的數放在大根堆,後一半的數放在小根堆
 * 在新增資料的時候,不斷地調整兩個堆的大小,使得兩個堆保持平衡
 * 要取得的中位數就是兩個堆堆頂的元素
 */
public class MedianQuick {
    public static class MedianHolder {
        private PriorityQueue<Integer> maxHeap = new PriorityQueue<Integer>(new MaxHeapComparator());
        private PriorityQueue<Integer> minHeap = new PriorityQueue<Integer>(new MinHeapComparator());
 
        /**
         * 調整堆的大小
         * 當兩個堆的大小差值變大時,從資料多的堆中彈出一個資料進入另一個堆中
         */
        private void modifyTwoHeapsSize() {
            if (this.maxHeap.size() == this.minHeap.size() + 2) {
                this.minHeap.add(this.maxHeap.poll());
            }
            if (this.minHeap.size() == this.maxHeap.size() + 2) {
                this.maxHeap.add(this.minHeap.poll());
            }
        }
 
        /**
         * 新增資料的過程
         *
         * @param num
         */
        public void addNumber(int num) {
            if (this.maxHeap.isEmpty()) {
                this.maxHeap.add(num);
                return;
            }
            if (this.maxHeap.peek() >= num) {
                this.maxHeap.add(num);
            } else {
                if (this.minHeap.isEmpty()) {
                    this.minHeap.add(num);
                    return;
                }
                if (this.minHeap.peek() > num) {
                    this.maxHeap.add(num);
                } else {
                    this.minHeap.add(num);
                }
            }
            modifyTwoHeapsSize();
        }
 
        /**
         * 獲取中位數
         *
         * @return
         */
        public Integer getMedian() {
            int maxHeapSize = this.maxHeap.size();
            int minHeapSize = this.minHeap.size();
            if (maxHeapSize + minHeapSize == 0) {
                return null;
            }
            Integer maxHeapHead = this.maxHeap.peek();
            Integer minHeapHead = this.minHeap.peek();
            if (((maxHeapSize + minHeapSize) & 1) == 0) {
                return (maxHeapHead + minHeapHead) / 2;
            }
            return maxHeapSize > minHeapSize ? maxHeapHead : minHeapHead;
        }
    }
 
    /**
     * 大根堆比較器
     */
    public static class MaxHeapComparator implements Comparator<Integer> {
        @Override
        public int compare(Integer o1, Integer o2) {
            if (o2 > o1) {
                return 1;
            } else {
                return -1;
            }
        }
    }
 
    /**
     * 小根堆比較器
     */
    public static class MinHeapComparator implements Comparator<Integer> {
        @Override
        public int compare(Integer o1, Integer o2) {
            if (o2 < o1) {
                return 1;
            } else {
                return -1;
            }
        }
    }
 
    // for test
    public static int[] getRandomArray(int maxLen, int maxValue) {
        int[] res = new int[(int) (Math.random() * maxLen) + 1];
        for (int i = 0; i != res.length; i++) {
            res[i] = (int) (Math.random() * maxValue);
        }
        return res;
    }
 
    // for test, this method is ineffective but absolutely right
    public static int getMedianOfArray(int[] arr) {
        int[] newArr = Arrays.copyOf(arr, arr.length);
        Arrays.sort(newArr);
        int mid = (newArr.length - 1) / 2;
        if ((newArr.length & 1) == 0) {
            return (newArr[mid] + newArr[mid + 1]) / 2;
        } else {
            return newArr[mid];
        }
    }
 
    public static void printArray(int[] arr) {
        for (int i = 0; i != arr.length; i++) {
            System.out.print(arr[i] + " ");
        }
        System.out.println();
    }
 
    public static void main(String[] args) {
        boolean err = false;
        int testTimes = 200000;
        for (int i = 0; i != testTimes; i++) {
            int len = 30;
            int maxValue = 1000;
            int[] arr = getRandomArray(len, maxValue);
            MedianHolder medianHold = new MedianHolder();
            for (int j = 0; j != arr.length; j++) {
                medianHold.addNumber(arr[j]);
            }
            if (medianHold.getMedian() != getMedianOfArray(arr)) {
                err = true;
                printArray(arr);
                break;
            }
        }
        System.out.println(err ? "Oops..what a fuck!" : "today is a beautiful day^_^");
 
    }
}

金條

 

一塊金條切成兩半,是需要花費和長度數值一樣的銅板的。比如長度為20的金條,不管切成長度多大的兩半,都要花費20個銅板。一群人想整分整塊金條,怎麼分最省銅板?
例如,給定陣列{10,20,30},代表一共三個人,整塊金條長度為10+20+30=60,金條要分成10,20,30三個部分。如果,先把長度60的金條分成10和50,花費60,再把長度為50的金條分成20和30,花費50,一共花費110個銅板。

但是如果,先把長度60的金條分成30和30,花費60,再把長度30金條分成10和30,花費30,一共花費90個銅板。

輸入一個陣列,返回分割的最小代價。

首先我們要明白一點:不管合併策略是什麼我們一共會合並n-1次,這個次數是不會變的。

我們要做的就是每一次都做最優選擇。

合為最優?

最小的兩個數合併就是最優。

所以

1)首先構造小根堆

2)每次取最小的兩個數(小根堆),使其代價最小。並將其和加入到小根堆中

3)重複(2)過程,直到最後堆中只剩下一個節點。

 

花費為每次花費的累加。

程式碼略。

 

專案最大收益(貪心問題)


輸入:引數1,正數陣列costs,引數2,正數陣列profits,引數3,正數k,引數4,正數m

costs[i]表示i號專案的花費profits[i]表示i號專案在扣除花費之後還能掙到的錢(利潤),k表示你不能並行,只能序列的最多做k個專案,m表示你初始的資金。

說明:你每做完一個專案,馬上獲得的收益,可以支援你去做下一個專案。

輸出:你最後獲得的最大錢數。

思考:給定一個初始化投資資金,給定N個專案,想要獲得其中最大的收益,並且一次只能做一個專案。這是一個貪心策略的問題,應該在能做的專案中選擇收益最大的。

按照花費的多少放到一個小根堆裡面,然後要是小根堆裡面的頭節點的花費少於給定資金,就將頭節點一個個取出來,放到按照收益的大根堆裡面。然後做大根堆頂的專案即可。

 並查集實現

並查集是什麼東西?

它是用來管理元素分組情況的一種資料結構。

他可以高效進行兩個操作:

  1. 查詢a,b是否在同一組
  2. 合併a和b所在的組

萌新可能不知所云,這個結構到底有什麼用?

經分析,並查集效率之高超乎想象,對n個元素的並查集進行一次操作的複雜度低於O(logn)

 

我們先說並查集是如何實現的:

也是使用樹形結構,但不是二元樹。

每個元素就是一個結點,每組都是一個樹。

無需關注它的形狀,或哪個節點具體在哪個位置。

 

初始化:

我們現在有n個結點,也就是n個元素。

 

合併:

然後我們就可以合併了,合併方法就是把一個根放到另一顆樹的下面,也就是整棵樹作為人家的一個子樹。

 

查詢:

查詢兩個結點是否是同一組,需要知道這兩個結點是不是在一棵樹上,讓他們分別沿著樹向根找,如果兩個元素最後走到一個根,他們就在一組。

 

當然,樹形結構都存在退化的缺點,對於每種結構,我們都有自己的優化方法,下面我們說明如何避免退化。

  1. 記錄每一棵樹的高度,合併操作時,高度小的變為高度大的子樹即可。
  2. 路徑壓縮:對於一個節點,只要走到了根節點,就不必再在很深的地方,直接改為連著根即可。進一步優化:其實每一個經過的節點都可以直接連根。

這樣查詢的時候就能很快地知道根是誰了。

 

下面上程式碼實現:

和很多樹結構一樣,我們沒必要真的模擬出來,陣列中即可。

int p[MAX_N];//父親
int rank[MAX_N];//高度
//初始化
void gg(int n)
{
    for(int i=0;i<n;i++)
    {
        p[i]=i;//父是自己代表是根
        rank[i]=0;
    }
}
//查詢根
int find(int x)
{
    if(p[x]==x)return x;
    return p[x]=find(p[x])//不斷把經過的結點連在根
}
//判斷是否屬於同一組
bool judge(int x,int y)
{
    return find(x)==find(y);//查詢結果一樣就在一組
}
//合併
void unite(int x,int y)
{
    if(x==y)return;
    if(rank[x]<rank[y])p[x]=y;//深度小,放在大的下面
    else
    {
        p[y]=x;
        if(rank[x]=rank[y])rank[x]++;//一樣,y放x後,x深度加一
    }
}

實現很簡單,應用有難度,以後有時間更新題。

並查集入門三連:HDU1213 POJ1611 POJ2236

HDU1213

http://acm.hdu.edu.cn/showproblem.php?pid=1213

問題描述

今天是伊格納修斯的生日。他邀請了很多朋友。現在是晚餐時間。伊格納修斯想知道他至少需要多少桌子。你必須注意到並非所有的朋友都互相認識,而且所有的朋友都不想和陌生人呆在一起。

這個問題的一個重要規則是,如果我告訴你A知道B,B知道C,那意味著A,B,C彼此瞭解,所以他們可以留在一個表中。

例如:如果我告訴你A知道B,B知道C,D知道E,所以A,B,C可以留在一個表中,D,E必須留在另一個表中。所以Ignatius至少需要2張桌子。

輸入

輸入以整數T(1 <= T <= 25)開始,表示測試用例的數量。然後是T測試案例。每個測試用例以兩個整數N和M開始(1 <= N,M <= 1000)。N表示朋友的數量,朋友從1到N標記。然後M行跟隨。每一行由兩個整數A和B(A!= B)組成,這意味著朋友A和朋友B彼此瞭解。兩個案例之間會有一個空白行。

 

對於每個測試用例,只輸出Ignatius至少需要多少個表。不要列印任何空白。

樣本輸入

2

5 3

1 2

2 3

4 5

 

5 1

2 5

樣本輸出

2

4

並查集基礎題

#include<cstdio>
#include<iostream>
using namespace std;
int fa[1005];
int n,m;
void init()//初始化
{
    for(int i=0;i<1005;i++)
        fa[i]=i;
}
int find(int x)//尋根
{
    if(fa[x]!=x)
        fa[x]=find(fa[x]);
    return fa[x];
}
void union(int x,int y)//判斷、合併
{
    int a=find(x),b=find(y);
    if(a!=b)
         fa[b]=a;
}
int main()
{
    int t;
    scanf("%d",&t);
    while(t--)
    {
        int a,b,cnt=0;
        scanf("%d%d",&n,&m);
        init();
        for(int i=1;i<=m;i++)//合併
        {
            scanf("%d%d",&a,&b);
            union(a,b);
        }
        for(int i=1;i<=n;i++)//統計
        {
            find(i);
            if(find(i)==i)
                cnt++;
        }
        printf("%d\n",cnt);
    }
    return 0;
}

POJ1611

http://poj.org/problem?id=1611

描述

嚴重急性呼吸系統綜合症(SARS)是一種病因不明的非典型肺炎,在2003年3月中旬被認為是一種全球性威脅。為了儘量減少對他人的傳播,最好的策略是將嫌疑人與其他嫌疑人分開。 
在Not-Spreading-Your-Sickness University(NSYSU),有許多學生團體。同一組中的學生經常互相交流,學生可以加入幾個小組。為了防止可能的SARS傳播,NSYSU收集所有學生組的成員列表,並在其標準操作程式(SOP)中制定以下規則。 
一旦組中的成員是嫌疑人,該組中的所有成員都是嫌疑人。 
然而,他們發現,當學生被認定為嫌疑人時,識別所有嫌疑人並不容易。你的工作是編寫一個找到所有嫌疑人的程式。

輸入

輸入檔案包含幾種情況。每個測試用例以一行中的兩個整數n和m開始,其中n是學生數,m是組的數量。您可以假設0 <n <= 30000且0 <= m <= 500.每個學生都使用0到n-1之間的唯一整數進行編號,並且最初學生0在所有情況下都被識別為嫌疑人。該行後面是組的m個成員列表,每組一行。每行以整數k開頭,表示組中的成員數。在成員數量之後,有k個整數代表該組中的學生。一行中的所有整數由至少一個空格分隔。 


n = 0且m = 0的情況表示輸入結束,無需處理。

 

對於每種情況,輸出一行中的嫌疑人數量。

樣本輸入

100 4
2 1 2
5 10 13 11 12 14
2 0 1
2 99 2
200 2
1 5
5 1 2 3 4 5
1 0
0 0

樣本輸出

4
1
1

 

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include <string>
using namespace std;
int a[30001],pre[30001];
int find(int x)//尋根
{
	 if(pre[x]==x)
        return x;
    else
        return pre[x]=find(pre[x]);
}
void union(int x, int y)//合併
{
	int fx = find(x), fy = find(y);
	if (fx != fy)
		pre[fy] = fx;
}

int main()
{
	int n,m;
	while (scanf("%d%d", &n, &m) != EOF && (n || m))
	{
		int sum = 0;
		for (int i = 0; i < n; i++)//初始化
			pre[i] = i;
		for (int i = 0; i < m; i++)
		{
			int k;
			scanf("%d", &k);
			if (k >= 1)
			{
				scanf("%d", &a[0]);
				for (int j = 1; j < k; j++)
				{
					scanf("%d", &a[j]);//接收
					union(a[0], a[j]);//和0號一組
				}
			}
		}
		for (int i = 0; i < n; i++)//統計
			if (find(i) ==pre[0])
				sum++;
		printf("%d\n", sum);
	}
	return 0;
}

 POJ2236

http://poj.org/problem?id=2236

描述

地震發生在東南亞。ACM(亞洲合作醫療團隊)已經與膝上電腦建立了無線網路,但是一次意外的餘震襲擊,網路中的所有計算機都被打破了。計算機一個接一個地修復,網路逐漸開始工作。由於硬體限制,每臺計算機只能直接與距離它不遠的計算機進行通訊。但是,每臺計算機都可以被視為兩臺計算機之間通訊的中介,也就是說,如果計算機A和計算機B可以直接通訊,或者計算機C可以與A和A進行通訊,則計算機A和計算機B可以進行通訊。 B. 

在修復網路的過程中,工作人員可以隨時進行兩種操作,修復計算機或測試兩臺計算機是否可以通訊。你的工作是回答所有的測試操作。 

輸入

第一行包含兩個整數N和d(1 <= N <= 1001,0 <= d <= 20000)。這裡N是計算機的數量,編號從1到N,D是兩臺計算機可以直接通訊的最大距離。在接下來的N行中,每行包含兩個整數xi,yi(0 <= xi,yi <= 10000),這是N臺計算機的座標。從第(N + 1)行到輸入結束,有一些操作,這些操作是一個接一個地執行的。每行包含以下兩種格式之一的操作: 
1。「O p」(1 <= p <= N),表示修復計算機p。 
2.「S p q」(1 <= p,q <= N),這意味著測試計算機p和q是否可以通訊。 

輸入不會超過300000行。 

產量

對於每個測試操作,如果兩臺計算機可以通訊則列印「SUCCESS」,否則列印「FAIL」。

樣本輸入

4 1
0 1
0 2
0 3
0 4
O 1
O 2
O 4
S 1 4
O 3
S 1 4

樣本輸出

FAIL
SUCCESS

 思路:對每次修好的電腦對其它已經修好的電腦遍歷,如果距離小於等於最大通訊距離就將他們合併。

注意

  1、座標之後給出的計算機編號都是n+1的。例如O 3,他實際上修理的是編號為2的計算機,因為計算機是從0開始編號的。

  2、比較距離的時候注意要用浮點數比較,否則會WA。

  3、"FAIL"不要寫成"FALL"。

  4、字串輸入的時候注意處理好回車,空格等情況。

  5、注意N的範圍(1 <= N <= 1001),最大是1001,不是1000。是個小坑,陣列開小了可能會錯哦。

 

#include <iostream>
#include <stdio.h>
#include <cmath>
using namespace std;

#define MAXN 1010

int dx[MAXN],dy[MAXN];    //座標
int par[MAXN];    //x的父節點
int repair[MAXN] ={0};
int n;

void Init()//初始化
{
    int i;
    for(i=0;i<=n;i++)
        par[i] = i;
}

int Find(int x)//尋根
{
    if(par[x]!=x)
        par[x] = Find(par[x]);
    return par[x];
}

void Union(int x,int y)//合併
{
    par[Find(x)] = Find(y);
}

int Abs(int n)//絕對值
{
    return n>0?n:-n;
}

double Dis(int a,int b)//座標
{
    return sqrt( double(dx[a]-dx[b])*(dx[a]-dx[b]) + (dy[a]-dy[b])*(dy[a]-dy[b]) );
}

int main()
{
    int d,i;

    //初始化
    scanf("%d%d",&n,&d);
    Init();

    //輸入座標
    for(i=0;i<n;i++){
        scanf("%d%d",&dx[i],&dy[i]);
    }
    
    //操作
    char cmd[2];
    int p,q,len=0;
    while(scanf("%s",cmd)!=EOF)
    {
        switch(cmd[0])
        {
            case 'O':
                scanf("%d",&p);
                p--;
                repair[len++] = p;
                for(i=0;i<len-1;i++)    //遍歷所有修過的計算機,看能否聯通
                    if( repair[i]!=p && Dis(repair[i],p)<=double(d) )
                        Union(repair[i],p);
                break;
            case 'S':
                scanf("%d%d",&p,&q);
                p--,q--;
                if(Find(p)==Find(q))    //判斷
                    printf("SUCCESS\n");
                else 
                    printf("FAIL\n");
            default:
                break;
        }
    }

    return 0;
}

線段樹簡單實現

首先,線段樹是一棵滿二元樹。(每個節點要麼有兩個孩子,要麼是深度相同的葉子節點)

每個節點維護某個區間,根維護所有的。

 轉存失敗重新上傳取消 

如圖,區間是二分父的區間。

當有n個元素,初始化需要o(n)時間,對區間操作需要o(logn)時間。

下面給出維護區間最小值的思路和程式碼

功能:一樣的,依舊是查詢和改值。

查詢[s,t]之間最小的數。修改某個值。

 

從下往上,每個節點的值為左右區間較小的那一個即可。

這算是簡單動態規劃思想,做到了o(n),因為每個節點就存取一遍,而葉子節點一共n個,所以存取2n次即可。

如果利用深搜初始化,會到o(nlogn)。

https://blog.csdn.net/hebtu666/article/details/81777273

有介紹

那我們繼續說,如何查詢。

不要以為它是二分割區間就只能查二分的那些區間,它能查任意區間。

比如上圖,求1-7的最小值,查詢1-4,5-6,7-7即可。

下面說過程:

遞迴實現:

如果要查詢的區間和本節點區間沒有重合,返回一個特別大的數即可,不要影響其他結果。

如果要查詢的區間完全包含了本節點區間,返回自身的值

都不滿足,對左右兒子做遞迴,返回較小的值。

 

如何更新?

更新ai,就要更新所有包含ai的區間。

可以從下往上不斷更新,把節點的值更新為左右孩子較小的即可。

 

程式碼實現和相關注釋:

注:沒有具體的初始化,dp思路寫過了,實在不想寫了

初始全為INT_MAX

const int MAX_N=1<<7;
int n;
int tree[2*MAX_N-1];
//初始化
void gg(int nn)
{
    n=1;
    while(n<nn)n*=2;//把元素個數變為2的n次方
    for(int i=0;i<2*n-1;i++)tree[i]=INTMAX;//所有值初始化為INTMAX
}

//查詢區間最小值
int get(int a,int b,int k,int l,int r)//l和r是區間,k是節點下標,求[a,b)最小值
{
    if(a>=r || b<=l)return INTMAX;//情況1
    if(a<=l || b<=b)return tree[k];//情況2
    int ll=get(a,b,k*2+1,l,(l+r)/2);//以前寫過,左孩子公式
    int rr=get(a,b,k*2+2,(l+r)/2,r);//右孩子
    return min(ll,rr);
}

//更新
void update(int k,int a)//第k個值更新為a
{
    //本身
    k+=n-1;//加上前面一堆節點數
    tree[k]=a;
    //開始向上
    while(k>0)
    {
        tree[k]=min(tree[2*k+1],tree[2*k+2]);
        k=(k-1)/2//父的公式,也寫過
    }
}

 樹狀陣列實現

樹狀陣列能夠完成如下操作:

給一個序列a0-an

計算前i項和

對某個值加x

時間o(logn)

 

注意:有人覺得字首和就行了,但是你還要維護啊,改變某個值,一個一個改變字首和就是o(n)了。

線段樹樹狀陣列的題就是這樣,維護一個樹,比較容易看出來。

 

 

線段樹:

https://blog.csdn.net/hebtu666/article/details/82691008

如果使用線段樹,只需要對網址中的實現稍微修改即可。以前維護最小值,現在維護和而已。

注意:要求只是求出前i項,而並未給定一個區間,那我們就能想出更快速、方便的方法。

對於任意一個節點,作為右孩子,如果求和時被用到,那它的左兄弟一定也會被用到,那我們就沒必要再用右孩子,因為用他們的父就可以了。

這樣一來,我們就可以把所有有孩子全部去掉

把剩下的節點編號。

 轉存失敗重新上傳取消 

如圖,可以發現一些規律:1,3,5,7,9等奇數,區間長度都為1

6,10,14等長度為2

........................

如果我們吧編號換成二進位制,就能發現,二進位制以1結尾的數位區間長度為1,最後有一個零的區間為2,兩個零的區間為4.

我們利用二進位制就能很容易地把編號和區間對應起來。

 

計算前i項和。

需要把當前編號i的數值加進來,把i最右邊的1減掉,直到i變為0.

二進位制最後一個1可以通過i&-i得到。

 

更新:

不斷把當前位置i加x,把i的二進位制最低非零位對應的冪加到i上。

下面是程式碼:

思想想出來挺麻煩,程式碼實現很簡單,我都不知道要註釋點啥

向發明這些東西的大佬們致敬

int bit[MAX_N+1]
int n;

int sum(int i)
{
    int gg=0;
    while(i>0)
    {
        gg+=bit[i];
        i-=i&-i;
    }
    return gg;
}

void add(int i,int x)
{
    while(i<=n)
    {
        bit[i]+=x;
        i+=i&-i;
    }
}

最大搜尋子樹

給定一個二元樹的頭結點,返回最大搜尋子樹的大小。

 

我們先定義結點:

    public static class Node {
		public int value;
		public Node left;
		public Node right;

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

分析:

直接判斷每個節點左邊小右邊大是不對滴

 

可以暴力判斷所有的子樹,就不說了。

 

最大搜尋子樹可能性:

第一種可能性,以node為頭的結點的最大二叉搜尋子樹可能來自它左子樹;
第二種可能性,以node為頭的結點的最大二叉搜尋子樹可能來自它右子樹;
第三種可能性,左樹整體是搜尋二元樹,右樹整體也是搜尋二元樹,而且左樹的頭是node.left,右樹的頭是node.right,且左樹的最大值< node.value,右樹的最小值 > node.value, 那麼以我為頭的整棵樹都是搜尋二元樹;
 

第三種可能性的判斷,需要的資訊有:左子樹的最大值、右子樹的最小值、左子樹是不是搜尋二元樹、右子樹是不是搜尋二元樹

還有左右搜尋二元樹的最大深度。

我們判斷了自己,並不知道自己是哪邊的子樹,我們要返回自己的最大值和最小值。

這樣,定義一個返回型別:

    public static class ReturnType{
		public int size;//最大搜尋子樹深度
		public Node head;//最大搜尋子樹的根
		public int min;//子樹最小
		public int max;//子樹最大
		
		public ReturnType(int a, Node b,int c,int d) {
			this.size =a;
			this.head = b;
			this.min = c;
			this.max = d;
		}
	}

然後開始寫程式碼:

注意:

1)NULL返回深度0,頭為NULL,最大值最小值返回系統最大和最小,這樣才不會影響別的判斷。

	public static ReturnType process(Node head) {
		if(head == null) {
			return new ReturnType(0,null,Integer.MAX_VALUE, Integer.MIN_VALUE);
		}
		
		Node left = head.left;//取資訊
		ReturnType leftSubTressInfo = process(left);
		Node right = head.right;
		ReturnType rightSubTressInfo = process(right);
		
		int includeItSelf = 0;
		if(leftSubTressInfo.head == left //            左子樹為搜尋樹
				&&rightSubTressInfo.head == right//    右子樹為搜尋樹
				&& head.value > leftSubTressInfo.max// 左子樹最大值小於當前節點
				&& head.value < rightSubTressInfo.min//右子樹最小值大於當前節點
				) {
			includeItSelf = leftSubTressInfo.size + 1 + rightSubTressInfo.size;//當前節點為根的二元樹為搜尋樹
		}
		
		int p1 = leftSubTressInfo.size;
		int p2 = rightSubTressInfo.size;
		
		int maxSize = Math.max(Math.max(p1, p2), includeItSelf);//最大搜尋樹深度
		
		Node maxHead = p1 > p2 ? leftSubTressInfo.head : rightSubTressInfo.head;

		if(maxSize == includeItSelf) {
			maxHead = head;
		}//最大搜尋樹的根:來自左子樹、來自右子樹、本身
		
		return new ReturnType(
				maxSize,                                                                     //深度
				maxHead,                                                                     //根
				Math.min(Math.min(leftSubTressInfo.min,rightSubTressInfo.min),head.value),    //最小
				Math.max(Math.max(leftSubTressInfo.max,rightSubTressInfo.max),head.value));	//最大
	}

可以進一步改進:

空間浪費比較嚴重

其實返回值為三個int,一個node,我們可以把三個int合起來,用全域性陣列記錄,函數只返回node(搜尋樹的根)即可。

給出完整程式碼:

public class BiggestSubBSTInTree {

	public static class Node {
		public int value;
		public Node left;
		public Node right;

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

	public static Node biggestSubBST(Node head) {
		int[] record = new int[3]; // 0->size, 1->min, 2->max
		return posOrder(head, record);
	}
	
	public static class ReturnType{
		public int size;//最大搜尋子樹深度
		public Node head;//最大搜尋子樹的根
		public int min;//子樹最小
		public int max;//子樹最大
		
		public ReturnType(int a, Node b,int c,int d) {
			this.size =a;
			this.head = b;
			this.min = c;
			this.max = d;
		}
	}
	
	public static ReturnType process(Node head) {
		if(head == null) {
			return new ReturnType(0,null,Integer.MAX_VALUE, Integer.MIN_VALUE);
		}
		
		Node left = head.left;//取資訊
		ReturnType leftSubTressInfo = process(left);
		Node right = head.right;
		ReturnType rightSubTressInfo = process(right);
		
		int includeItSelf = 0;
		if(leftSubTressInfo.head == left //            左子樹為搜尋樹
				&&rightSubTressInfo.head == right//    右子樹為搜尋樹
				&& head.value > leftSubTressInfo.max// 左子樹最大值小於當前節點
				&& head.value < rightSubTressInfo.min//右子樹最小值大於當前節點
				) {
			includeItSelf = leftSubTressInfo.size + 1 + rightSubTressInfo.size;//當前節點為根的二元樹為搜尋樹
		}
		
		int p1 = leftSubTressInfo.size;
		int p2 = rightSubTressInfo.size;
		
		int maxSize = Math.max(Math.max(p1, p2), includeItSelf);//最大搜尋樹深度
		
		Node maxHead = p1 > p2 ? leftSubTressInfo.head : rightSubTressInfo.head;
		if(maxSize == includeItSelf) {
			maxHead = head;
		}//最大搜尋樹的根:來自左子樹、來自右子樹、本身
		
		return new ReturnType(
				maxSize,                                                                     //深度
				maxHead,                                                                     //根
				Math.min(Math.min(leftSubTressInfo.min,rightSubTressInfo.min),head.value),   //最小
				Math.max(Math.max(leftSubTressInfo.max,rightSubTressInfo.max),head.value));	 //最大
	}
	
	
	

	public static Node posOrder(Node head, int[] record) {
		if (head == null) {
			record[0] = 0;
			record[1] = Integer.MAX_VALUE;
			record[2] = Integer.MIN_VALUE;
			return null;
		}
		int value = head.value;
		Node left = head.left;
		Node right = head.right;
		Node lBST = posOrder(left, record);
		int lSize = record[0];
		int lMin = record[1];
		int lMax = record[2];
		Node rBST = posOrder(right, record);
		int rSize = record[0];
		int rMin = record[1];
		int rMax = record[2];
		record[1] = Math.min(rMin, Math.min(lMin, value)); // lmin, value, rmin -> min 
		record[2] = Math.max(lMax, Math.max(rMax, value)); // lmax, value, rmax -> max
		if (left == lBST && right == rBST && lMax < value && value < rMin) {
			record[0] = lSize + rSize + 1;//修改深度
			return head;                  //返回根
		}//滿足當前構成搜尋樹的條件
		record[0] = Math.max(lSize, rSize);//較大深度
		return lSize > rSize ? lBST : rBST;//返回較大搜尋樹的根
	}

	// for test -- print tree
	public static void printTree(Node head) {
		System.out.println("Binary Tree:");
		printInOrder(head, 0, "H", 17);
		System.out.println();
	}

	public static void printInOrder(Node head, int height, String to, int len) {
		if (head == null) {
			return;
		}
		printInOrder(head.right, height + 1, "v", len);
		String val = to + head.value + to;
		int lenM = val.length();
		int lenL = (len - lenM) / 2;
		int lenR = len - lenM - lenL;
		val = getSpace(lenL) + val + getSpace(lenR);
		System.out.println(getSpace(height * len) + val);
		printInOrder(head.left, height + 1, "^", len);
	}

	public static String getSpace(int num) {
		String space = " ";
		StringBuffer buf = new StringBuffer("");
		for (int i = 0; i < num; i++) {
			buf.append(space);
		}
		return buf.toString();
	}

	public static void main(String[] args) {

		Node head = new Node(6);
		head.left = new Node(1);
		head.left.left = new Node(0);
		head.left.right = new Node(3);
		head.right = new Node(12);
		head.right.left = new Node(10);
		head.right.left.left = new Node(4);
		head.right.left.left.left = new Node(2);
		head.right.left.left.right = new Node(5);
		head.right.left.right = new Node(14);
		head.right.left.right.left = new Node(11);
		head.right.left.right.right = new Node(15);
		head.right.right = new Node(13);
		head.right.right.left = new Node(20);
		head.right.right.right = new Node(16);

		printTree(head);
		Node bst = biggestSubBST(head);
		printTree(bst);

	}

}

morris遍歷

通常,實現二元樹的前序(preorder)、中序(inorder)、後序(postorder)遍歷有兩個常用的方法:一是遞迴(recursive),二是使用棧實現的迭代版本(stack+iterative)。這兩種方法都是O(n)的空間複雜度(遞迴本身佔用stack空間或者使用者自定義的stack)。

本文介紹空間O(1)的遍歷方法。

上次文章講到,我們經典遞迴遍歷其實有三次存取當前節點的機會,就看你再哪次進行操作,而分成了三種遍歷。

https://blog.csdn.net/hebtu666/article/details/82853988

morris有兩次存取節點的機會。

它省空間的原理是利用了大量葉子節點的沒有用的空間,記錄之前的節點,做到了返回之前節點這件事情。

我們不說先序中序後序,先說morris遍歷的原則:

1、如果沒有左孩子,繼續遍歷右子樹

2、如果有左孩子,找到左子樹最右節點。

    1)如果最右節點的右指標為空(說明第一次遇到),把它指向當前節點,當前節點向左繼續處理。

    2)如果最右節點的右指標不為空(說明它指向之前結點),把右指標設為空,當前節點向右繼續處理。

 

這就是morris遍歷。

請手動模擬深度至少為3的樹的morris遍歷來熟悉流程。

 

先看程式碼:

定義結點:

	public static class Node {
		public int value;
		Node left;
		Node right;

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

先序:

 (完全按規則寫就好。)

//列印時機(第一次遇到):發現左子樹最右的孩子右指標指向空,或無左子樹。
	public static void morrisPre(Node head) {
		if (head == null) {
			return;
		}
		Node cur1 = head;
		Node cur2 = null;
		while (cur1 != null) {
			cur2 = cur1.left;
			if (cur2 != null) {
				while (cur2.right != null && cur2.right != cur1) {
					cur2 = cur2.right;
				}
				if (cur2.right == null) {
					cur2.right = cur1;
					System.out.print(cur1.value + " ");
					cur1 = cur1.left;
					continue;
				} else {
					cur2.right = null;
				}
			} else {
				System.out.print(cur1.value + " ");
			}
			cur1 = cur1.right;
		}
		System.out.println();
	}

morris在發表文章時只寫出了中序遍歷。而先序遍歷只是列印時機不同而已,所以後人改進出了先序遍歷。至於後序,是通過列印所有的右邊界來實現的:對每個有邊界逆序,列印,再逆序回去。注意要原地逆序,否則我們morris遍歷的意義也就沒有了。

完整程式碼: 

public class MorrisTraversal {

	
	
	public static void process(Node head) {
		if(head == null) {
			return;
		}
		
		// 1
		//System.out.println(head.value);
		
		
		process(head.left);
		
		// 2
		//System.out.println(head.value);
		
		
		process(head.right);
		
		// 3
		//System.out.println(head.value);
	}
	
	
	public static class Node {
		public int value;
		Node left;
		Node right;

		public Node(int data) {
			this.value = data;
		}
	}
//列印時機:向右走之前
	public static void morrisIn(Node head) {
		if (head == null) {
			return;
		}
		Node cur1 = head;//當前節點
		Node cur2 = null;//最右
		while (cur1 != null) {
			cur2 = cur1.left;
			//左孩子不為空
			if (cur2 != null) {
				while (cur2.right != null && cur2.right != cur1) {
					cur2 = cur2.right;
				}//找到最右
				//右指標為空,指向cur1,cur1向左繼續
				if (cur2.right == null) {
					cur2.right = cur1;
					cur1 = cur1.left;
					continue;
				} else {
					cur2.right = null;
				}//右指標不為空,設為空
			}
			System.out.print(cur1.value + " ");
			cur1 = cur1.right;
		}
		System.out.println();
	}
//列印時機(第一次遇到):發現左子樹最右的孩子右指標指向空,或無左子樹。
	public static void morrisPre(Node head) {
		if (head == null) {
			return;
		}
		Node cur1 = head;
		Node cur2 = null;
		while (cur1 != null) {
			cur2 = cur1.left;
			if (cur2 != null) {
				while (cur2.right != null && cur2.right != cur1) {
					cur2 = cur2.right;
				}
				if (cur2.right == null) {
					cur2.right = cur1;
					System.out.print(cur1.value + " ");
					cur1 = cur1.left;
					continue;
				} else {
					cur2.right = null;
				}
			} else {
				System.out.print(cur1.value + " ");
			}
			cur1 = cur1.right;
		}
		System.out.println();
	}
//逆序列印所有右邊界
	public static void morrisPos(Node head) {
		if (head == null) {
			return;
		}
		Node cur1 = head;
		Node cur2 = null;
		while (cur1 != null) {
			cur2 = cur1.left;
			if (cur2 != null) {
				while (cur2.right != null && cur2.right != cur1) {
					cur2 = cur2.right;
				}
				if (cur2.right == null) {
					cur2.right = cur1;
					cur1 = cur1.left;
					continue;
				} else {
					cur2.right = null;
					printEdge(cur1.left);
				}
			}
			cur1 = cur1.right;
		}
		printEdge(head);
		System.out.println();
	}
//逆序列印
	public static void printEdge(Node head) {
		Node tail = reverseEdge(head);
		Node cur = tail;
		while (cur != null) {
			System.out.print(cur.value + " ");
			cur = cur.right;
		}
		reverseEdge(tail);
	}
//逆序(類似連結串列逆序)
	public static Node reverseEdge(Node from) {
		Node pre = null;
		Node next = null;
		while (from != null) {
			next = from.right;
			from.right = pre;
			pre = from;
			from = next;
		}
		return pre;
	}
	public static void main(String[] args) {
		Node head = new Node(4);
		head.left = new Node(2);
		head.right = new Node(6);
		head.left.left = new Node(1);
		head.left.right = new Node(3);
		head.right.left = new Node(5);
		head.right.right = new Node(7);

		morrisIn(head);
		morrisPre(head);
		morrisPos(head);
	}

}

最小生成樹

 

問題提出:
    要在n個城市間建立通訊聯絡網。頂點:表示城市,權:城市間通訊線路的花費代價。希望此通訊網花費代價最小。
問題分析:
    答案只能從生成樹中找,因為要做到任何兩個城市之間有線路可達,通訊網必須是連通的;但對長度最小的要求可以知道網中顯然不能有圈,如果有圈,去掉一條邊後,並不破壞連通性,但總代價顯然減少了,這與總代價最小的假設是矛盾的。
結論:
    希望找到一棵生成樹,它的每條邊上的權值之和(即建立該通訊網所需花費的總代價)最小 —— 最小代價生成樹。
    構造最小生成樹的演演算法很多,其中多數演演算法都利用了一種稱之為 MST 的性質。
    MST 性質:設 N = (V, E)  是一個連通網,U是頂點集 V的一個非空子集。若邊 (u, v) 是一條具有最小權值的邊,其中u∈U,v∈V-U,則必存在一棵包含邊 (u, v) 的最小生成樹。


(1)普里姆 (Prim) 演演算法

演演算法思想: 
    ①設 N=(V, E)是連通網,TE是N上最小生成樹中邊的集合。
    ②初始令 U={u_0}, (u_0∈V), TE={ }。
    ③在所有u∈U,u∈U-V的邊(u,v)∈E中,找一條代價最小的邊(u_0,v_0 )。
    ④將(u_0,v_0 )併入集合TE,同時v_0併入U。
    ⑤重複上述操作直至U = V為止,則 T=(V,TE)為N的最小生成樹。

 
程式碼實現:

void MiniSpanTree_PRIM(MGraph G,VertexType u)
    //用普里姆演演算法從第u個頂點出發構造網G的最小生成樹T,輸出T的各條邊。
    //記錄從頂點集U到V-U的代價最小的邊的輔助陣列定義;
    //closedge[j].lowcost表示在集合U中頂點與第j個頂點對應最小權值
{
    int k, j, i;
    k = LocateVex(G,u);
    for (j = 0; j < G.vexnum; ++j)    //輔助陣列的初始化
        if(j != k)
        {
            closedge[j].adjvex = u;
            closedge[j].lowcost = G.arcs[k][j].adj;    
//獲取鄰接矩陣第k行所有元素賦給closedge[j!= k].lowcost
        }
    closedge[k].lowcost = 0;        
//初始,U = {u};  
    PrintClosedge(closedge,G.vexnum);
    for (i = 1; i < G.vexnum; ++i)    \
//選擇其餘G.vexnum-1個頂點,因此i從1開始迴圈
    {
        k = minimum(G.vexnum,closedge);        
//求出最小生成樹的下一個結點:第k頂點
        PrintMiniTree_PRIM(G, closedge, k);     //輸出生成樹的邊
        closedge[k].lowcost = 0;                //第k頂點併入U集
        PrintClosedge(closedge,G.vexnum);
        for(j = 0;j < G.vexnum; ++j)
        {                                           
            if(G.arcs[k][j].adj < closedge[j].lowcost)    
//比較第k個頂點和第j個頂點權值是否小於closedge[j].lowcost
            {
                closedge[j].adjvex = G.vexs[k];//替換closedge[j]
                closedge[j].lowcost = G.arcs[k][j].adj;
                PrintClosedge(closedge,G.vexnum);
            }
        }
    }
}


(2)克魯斯卡爾 (Kruskal) 演演算法

演演算法思想: 
    ①設連通網  N = (V, E ),令最小生成樹初始狀態為只有n個頂點而無邊的非連通圖,T=(V, { }),每個頂點自成一個連通分量。
    ②在 E 中選取代價最小的邊,若該邊依附的頂點落在T中不同的連通分量上(即:不能形成環),則將此邊加入到T中;否則,捨去此邊,選取下一條代價最小的邊。
③依此類推,直至 T 中所有頂點都在同一連通分量上為止。
      
    最小生成樹可能不惟一!

 

拓撲排序

 

(1)有向無環圖

    無環的有向圖,簡稱 DAG (Directed Acycline Graph) 圖。
 
有向無環圖在工程計劃和管理方面的應用:除最簡單的情況之外,幾乎所有的工程都可分為若干個稱作「活動」的子工程,並且這些子工程之間通常受著一定條件的約束,例如:其中某些子工程必須在另一些子工程完成之後才能開始。
對整個工程和系統,人們關心的是兩方面的問題: 
①工程能否順利進行; 
②完成整個工程所必須的最短時間。

對應到有向圖即為進行拓撲排序和求關鍵路徑。 
AOV網: 
    用一個有向圖表示一個工程的各子工程及其相互制約的關係,其中以頂點表示活動,弧表示活動之間的優先制約關係,稱這種有向圖為頂點表示活動的網,簡稱AOV網(Activity On Vertex network)。
例如:排課表
      
AOV網的特點:
①若從i到j有一條有向路徑,則i是j的前驅;j是i的後繼。
②若< i , j >是網中有向邊,則i是j的直接前驅;j是i的直接後繼。
③AOV網中不允許有迴路,因為如果有迴路存在,則表明某項活動以自己為先決條件,顯然這是荒謬的。


問題:    
    問題:如何判別 AOV 網中是否存在迴路?
    檢測 AOV 網中是否存在環方法:對有向圖構造其頂點的拓撲有序序列,若網中所有頂點都在它的拓撲有序序列中,則該AOV網必定不存在環。


拓撲排序的方法:
    ①在有向圖中選一個沒有前驅的頂點且輸出之。
    ②從圖中刪除該頂點和所有以它為尾的弧。
    ③重複上述兩步,直至全部頂點均已輸出;或者當圖中不存在無前驅的頂點為止。
        
    一個AOV網的拓撲序列不是唯一的!
程式碼實現:

Status TopologicalSort(ALGraph G)
    //有向圖G採用鄰接表儲存結構。
    //若G無迴路,則輸出G的頂點的一個拓撲序列並返回OK,否則返回ERROR.
    //輸出次序按照棧的後進先出原則,刪除頂點,輸出遍歷
{
    SqStack S;
    int i, count;
    int *indegree1 = (int *)malloc(sizeof(int) * G.vexnum);
    int indegree[12] = {0};
    FindInDegree(G, indegree);    //求個頂點的入度下標從0開始
    InitStack(&S);
    PrintStack(S);
    for(i = 0; i < G.vexnum; ++i)
        if(!indegree[i])        //建0入度頂點棧S
            push(&S,i);        //入度為0者進棧
    count = 0;                //對輸出頂點計數
    while (S.base != S.top)
    {
        ArcNode* p;
        pop(&S,&i);
        VisitFunc(G,i);//第i個輸出棧頂元素對應的頂點,也就是最後進來的頂點    
        ++count;          //輸出i號頂點並計數
        for(p = G.vertices[i].firstarc; p; p = p->nextarc)
        {    //通過迴圈遍歷第i個頂點的表結點,將表結點中入度都減1
            int k = p->adjvex;    //對i號頂點的每個鄰接點的入度減1
            if(!(--indegree[k]))
                push(&S,k);        //若入度減為0,則入棧
        }//for
    }//while
    if(count < G.vexnum)
    {
        printf("\n該有向圖有迴路!\n");
        return ERROR;    //該有向圖有迴路
    }
    else
    {
        printf("\n該有向圖沒有迴路!\n");
        return OK;
    }
}


關鍵路徑

    把工程計劃表示為有向圖,用頂點表示事件,弧表示活動,弧的權表示活動持續時間。每個事件表示在它之前的活動已經完成,在它之後的活動可以開始。稱這種有向圖為邊表示活動的網,簡稱為 AOE網 (Activity On Edge)。
例如:
設一個工程有11項活動,9個事件。
事件v_1——表示整個工程開始(源點) 
事件v_9——表示整個工程結束(匯點)

 
對AOE網,我們關心兩個問題:  
①完成整項工程至少需要多少時間? 
②哪些活動是影響工程進度的關鍵?
關鍵路徑——路徑長度最長的路徑。
路徑長度——路徑上各活動持續時間之和。
v_i——表示事件v_i的最早發生時間。假設開始點是v_1,從v_1到〖v�i〗的最長路徑長度。ⅇ(ⅈ)——表示活動a_i的最早發生時間。
l(ⅈ)——表示活動a_i最遲發生時間。在不推遲整個工程完成的前提下,活動a_i最遲必須開始進行的時間。
l(ⅈ)-ⅇ(ⅈ)意味著完成活動a_i的時間餘量。
我們把l(ⅈ)=ⅇ(ⅈ)的活動叫做關鍵活動。顯然,關鍵路徑上的所有活動都是關鍵活動,因此提前完成非關鍵活動並不能加快工程進度。
    例如上圖中網,從從v_1到v_9的最長路徑是(v_1,v_2,v_5,v_8,ν_9 ),路徑長度是18,即ν_9的最遲發生時間是18。而活動a_6的最早開始時間是5,最遲開始時間是8,這意味著:如果a_6推遲3天或者延遲3天完成,都不會影響整個工程的完成。因此,分析關鍵路徑的目的是辨別哪些是關鍵活動,以便爭取提高關鍵活動的工效,縮短整個工期。
    由上面介紹可知:辨別關鍵活動是要找l(ⅈ)=ⅇ(ⅈ)的活動。為了求ⅇ(ⅈ)和l(ⅈ),首先應求得事件的最早發生時間vⅇ(j)和最遲發生時間vl(j)。如果活動a_i由弧〈j,k〉表示,其持續時間記為dut(〈j,k〉),則有如下關係:
ⅇ(ⅈ)= vⅇ(j)
l(ⅈ)=vl(k)-dut(〈j,k〉)
    求vⅇ(j)和vl(j)需分兩步進行:
第一步:從vⅇ(0)=0開始向前遞推
vⅇ(j)=Max{vⅇ(i)+dut(〈j,k〉)}   〈i,j〉∈T,j=1,2,…,n-1
其中,T是所有以第j個頂點為頭的弧的集合。
第二步:從vl(n-1)=vⅇ(n-1)起向後遞推
vl(i)=Min{vl(j)-dut(〈i,j〉)}  〈i,j〉∈S,i=n-2,…,0
其中,S是所有以第i個頂點為尾的弧的集合。
下面我們以上圖AOE網為例,先求每個事件v_i的最早發生時間,再逆向求每個事件對應的最晚發生時間。再求每個活動的最早發生時間和最晚發生時間,如下面表格:
          
在活動的統計表中,活動的最早發生時間和最晚發生時間相等的,就是關鍵活動


關鍵路徑的討論:

①若網中有幾條關鍵路徑,則需加快同時在幾條關鍵路徑上的關鍵活動。      如:a11、a10、a8、a7。 
②如果一個活動處於所有的關鍵路徑上,則提高這個活動的速度,就能縮短整個工程的完成時間。如:a1、a4。
③處於所有關鍵路徑上的活動完成時間不能縮短太多,否則會使原關鍵路徑變成非關鍵路徑。這時必須重新尋找關鍵路徑。如:a1由6天變成3天,就會改變關鍵路徑。

關鍵路徑演演算法實現:

int CriticalPath(ALGraph G)
{    //因為G是有向網,輸出G的各項關鍵活動
    SqStack T;
    int i, j;    ArcNode* p;
    int k , dut;
    if(!TopologicalOrder(G,T))
        return 0;
    int vl[VexNum];
    for (i = 0; i < VexNum; i++)
        vl[i] = ve[VexNum - 1];        //初始化頂點事件的最遲發生時間
    while (T.base != T.top)            //按拓撲逆序求各頂點的vl值
    {
 
        for(pop(&T, &j), p = G.vertices[j].firstarc; p; p = p->nextarc)
        {
            k = p->adjvex;    dut = *(p->info);    //dut<j, k>
            if(vl[k] - dut < vl[j])
                vl[j] = vl[k] - dut;
        }//for
    }//while
    for(j = 0; j < G.vexnum; ++j)    //求ee,el和關鍵活動
    {
        for (p = G.vertices[j].firstarc; p; p = p->nextarc)
        {
            int ee, el;        char tag;
            k = p->adjvex;    dut = *(p->info);
            ee = ve[j];    el = vl[k] - dut;
            tag = (ee == el) ? '*' : ' ';
            PrintCriticalActivity(G,j,k,dut,ee,el,tag);
        }
    }
    return 1;
}

最短路

 

最短路

    典型用途:交通網路的問題——從甲地到乙地之間是否有公路連通?在有多條通路的情況下,哪一條路最短?
 
    交通網路用有向網來表示:頂點——表示城市,弧——表示兩個城市有路連通,弧上的權值——表示兩城市之間的距離、交通費或途中所花費的時間等。
    如何能夠使一個城市到另一個城市的運輸時間最短或運費最省?這就是一個求兩座城市間的最短路徑問題。
    問題抽象:在有向網中A點(源點)到達B點(終點)的多條路徑中,尋找一條各邊權值之和最小的路徑,即最短路徑。最短路徑與最小生成樹不同,路徑上不一定包含n個頂點,也不一定包含n - 1條邊。
   常見最短路徑問題:單源點最短路徑、所有頂點間的最短路徑
(1)如何求得單源點最短路徑?
    窮舉法:將源點到終點的所有路徑都列出來,然後在其中選最短的一條。但是,當路徑特別多時,特別麻煩;沒有規律可循。
    迪傑斯特拉(Dijkstra)演演算法:按路徑長度遞增次序產生各頂點的最短路徑。
路徑長度最短的最短路徑的特點:
    在此路徑上,必定只含一條弧 <v_0, v_1>,且其權值最小。由此,只要在所有從源點出發的弧中查詢權值最小者。
下一條路徑長度次短的最短路徑的特點:
①、直接從源點到v_2<v_0, v_2>(只含一條弧);
②、從源點經過頂點v_1,再到達v_2<v_0, v_1>,<v_1, v_2>(由兩條弧組成)
再下一條路徑長度次短的最短路徑的特點:
    有以下四種情況:
    ①、直接從源點到v_3<v_0, v_3>(由一條弧組成);
    ②、從源點經過頂點v_1,再到達v_3<v_0, v_1>,<v_1, v_3>(由兩條弧組成);
    ③、從源點經過頂點v_2,再到達v_3<v_0, v_2>,<v_2, v_3>(由兩條弧組成);
    ④、從源點經過頂點v_1  ,v_2,再到達v_3<v_0, v_1>,<v_1, v_2>,<v_2, v_3>(由三條弧組成);
其餘最短路徑的特點:    
    ①、直接從源點到v_i<v_0, v_i>(只含一條弧);
    ②、從源點經過已求得的最短路徑上的頂點,再到達v_i(含有多條弧)。
Dijkstra演演算法步驟:
    初始時令S={v_0},  T={其餘頂點}。T中頂點對應的距離值用輔助陣列D存放。
    D[i]初值:若<v_0, v_i>存在,則為其權值;否則為∞。 
    從T中選取一個其距離值最小的頂點v_j,加入S。對T中頂點的距離值進行修改:若加進v_j作中間頂點,從v_0到v_i的距離值比不加 vj 的路徑要短,則修改此距離值。
    重複上述步驟,直到 S = V 為止。

演演算法實現:

void ShortestPath_DIJ(MGraph G,int v0,PathMatrix &P,ShortPathTable &D)
{ // 用Dijkstra演演算法求有向網 G 的 v0 頂點到其餘頂點v的最短路徑P[v]及帶權長度D[v]。
    // 若P[v][w]為TRUE,則 w 是從 v0 到 v 當前求得最短路徑上的頂點。  P是存放最短路徑的矩陣,經過頂點變成TRUE
    // final[v]為TRUE當且僅當 v∈S,即已經求得從v0到v的最短路徑。
    int v,w,i,j,min;
    Status final[MAX_VERTEX_NUM];
    for(v = 0 ;v < G.vexnum ;++v)
    {
        final[v] = FALSE;
        D[v] = G.arcs[v0][v].adj;        //將頂點陣列中下標對應是 v0 和 v的距離給了D[v]
        for(w = 0;w < G.vexnum; ++w)
            P[v][w] = FALSE;            //設空路徑
        if(D[v] < INFINITY)
        {
            P[v][v0] = TRUE;
            P[v][v] = TRUE;
        }
    }
    D[v0]=0;
    final[v0]= TRUE; /* 初始化,v0頂點屬於S集 */
    for(i = 1;i < G.vexnum; ++i) /* 其餘G.vexnum-1個頂點 */
    { /* 開始主迴圈,每次求得v0到某個v頂點的最短路徑,並加v到S集 */
        min = INFINITY; /* 當前所知離v0頂點的最近距離 */
        for(w = 0;w < G.vexnum; ++w)
            if(!final[w]) /* w頂點在V-S中 */
                if(D[w] < min)
                {
                    v = w;
                    min = D[w];
                } /* w頂點離v0頂點更近 */
                final[v] = TRUE; /* 離v0頂點最近的v加入S集 */
                for(w = 0;w < G.vexnum; ++w) /* 更新當前最短路徑及距離 */
                {
                    if(!final[w] && min < INFINITY && G.arcs[v][w].adj < INFINITY && (min + G.arcs[v][w].adj < D[w]))
                    { /* 修改D[w]和P[w],w∈V-S */
                        D[w] = min + G.arcs[v][w].adj;
                        for(j = 0;j < G.vexnum;++j)
                            P[w][j] = P[v][j];
                        P[w][w] = TRUE;
                    }
                }
    }
}

簡單迷宮問題

迷宮實驗是取自心理學的一個古典實驗。在該實驗中,把一隻老鼠從一個無頂大盒子的門放入,在盒子中設定了許多牆,對行進方向形成了多處阻擋。盒子僅有一個出口,在出口處放置一塊乳酪,吸引老鼠在迷宮中尋找道路以到達出口。對同一只老鼠重複進行上述實驗,一直到老鼠從入口到出口,而不走錯一步。老鼠經過多次試驗終於得到它學習走通迷宮的路線。設計一個計算機程式對任意設定的迷宮,求出一條從入口到出口的通路,或得出沒有通路的結論。
陣列元素值為1表示該位置是牆壁,不能通行;元素值為0表示該位置是通路。假定從mg[1][1]出發,出口位於mg[n][m]

用一種標誌在二維陣列中標出該條通路,並在螢幕上輸出二維陣列。

m=[[1,1,1,0,1,1,1,1,1,1],
   [1,0,0,0,0,0,0,0,1,1],
   [1,0,1,1,1,1,1,0,0,1],
   [1,0,1,0,0,0,0,1,0,1],
   [1,0,1,0,1,1,0,0,0,1],
   [1,0,0,1,1,0,1,0,1,1],
   [1,1,1,1,0,0,0,0,1,1],
   [1,0,0,0,0,1,1,1,0,0],
   [1,0,1,1,0,0,0,0,0,1],
   [1,1,1,1,1,1,1,1,1,1]]
sta1=0;sta2=3;fsh1=7;fsh2=9;success=0
def LabyrinthRat():
    print('顯示迷宮:')
    for i in range(len(m)):print(m[i])
    print('入口:m[%d][%d]:出口:m[%d][%d]'%(sta1,sta2,fsh1,fsh2))
    if (visit(sta1,sta2))==0:	print('沒有找到出口')
    else:
        print('顯示路徑:')
        for i in range(10):print(m[i])
def visit(i,j):
    m[i][j]=2
    global success
    if(i==fsh1)and(j==fsh2): success=1
    if(success!=1)and(m[i-1][j]==0): visit(i-1,j)
    if(success!=1)and(m[i+1][j]==0): visit(i+1,j)
    if(success!=1)and(m[i][j-1]==0): visit(i,j-1)
    if(success!=1)and(m[i][j+1]==0): visit(i,j+1)
    if success!=1: m[i][j]=3
    return success
LabyrinthRat()

深搜DFS\廣搜BFS 

首先,不管是BFS還是DFS,由於時間和空間的侷限性,它們只能解決資料量比較小的問題。

深搜,顧名思義,它從某個狀態開始,不斷的轉移狀態,直到無法轉移,然後退回到上一步的狀態,繼續轉移到其他狀態,不斷重複,直到找到最終的解。從實現上來說,棧結構是後進先出,可以很好的儲存上一步狀態並利用。所以根據深搜和棧結構的特點,深度優先搜尋利用遞迴函數(棧)來實現,只不過這個棧是系統幫忙做的,不太明顯罷了。

 

廣搜和深搜的搜尋順序不同,它是先搜尋離初始狀態比較近的狀態,搜尋順序是這樣的:初始狀態---------->一步能到的狀態--------->兩步能到的狀態......從實現上說,它是通過佇列實現的,並且是我們自己做佇列。一般解決最短路問題,因為第一個搜到的一定是最短路。

下面通過兩道簡單例題簡單的入個門。

深搜例題

poj2386

http://poj.org/problem?id=2386

題目大意:上下左右斜著挨著都算一個池子,看圖中有幾個池子。

W........WW.
.WWW.....WWW
....WW...WW.
.........WW.
.........W..
..W......W..
.W.W.....WW.
W.W.W.....W.
.W.W......W.
..W.......W.例如本圖就是有三個池子

採用深度優先搜尋,從任意的w開始,不斷把鄰接的部分用'.'代替,1次DFS後與初始這個w連線的所有w就全都被替換成'.',因此直到圖中不再存在W為止。

核心程式碼:

char field[maxn][maxn];//圖
int n,m;長寬
void dfs(int x,int y)
{
    field[x][y]='.';//先做了標記
    //迴圈遍歷八個方向
    for(int dx=-1;dx<=1;dx++){
        for(int dy=-1;dy<=1;dy++){
            int nx=x+dx,ny=y+dy;
            //判斷(nx,ny)是否在園子裡,以及是否有積水
            if(0<=nx&&nx<n&&0<=ny&&ny<m&&field[nx][ny]=='W'){
                dfs(nx,ny);
            }
        }
    }
}
void solve()
{
    int res=0;
    for(int i=0;i<n;i++){
        for(int j=0;j<m;j++){
            if(field[i][j]=='W'){
                //從有積水的地方開始搜
                dfs(i,j);
                res++;//搜幾次就有幾個池子
            }
        }
    }
    printf("%d\n",res);
}

廣搜例題:

迷宮的最短路徑

  給定一個大小為N×M的迷宮。迷宮由通道和牆壁組成,每一步可以向鄰接的上下左右四個的通道移動。請求出從起點到終點所需的最小步數。請注意,本題假定從起點一定可以移動到終點。(N,M≤100)('#', '.' , 'S', 'G'分別表示牆壁、通道、起點和終點)

輸入:

10 10

#S######.#
......#..#
.#.##.##.#
.#........
##.##.####
....#....#
.#######.#
....#.....
.####.###.
....#...G#

輸出:

22

小白書上部分程式碼:

typedef pair<int, int> P;
char maze[maxn][maxn];
int n, m, sx, sy, gx, gy,d[maxn][maxn];//到各個位置的最短距離的陣列
int dx[4] = { 1,0,-1,0 }, dy[4]= { 0,1,0,-1 };//4個方向移動的向量
int bfs()//求從(sx,sy)到(gx,gy)的最短距離,若無法到達則是INF
{
    queue<P> que; 
    for (int i = 0; i < n; i++)
        for (int j = 0; j < m; j++)
            d[i][j] = INF;//所有的位置都初始化為INF
    que.push(P(sx, sy));//將起點加入佇列中
    d[sx][sy] = 0;//並把起點的距離設定為0
    while (que.size())//不斷迴圈直到佇列的長度為0
    {
        P p = que.front();// 從佇列的最前段取出元素
        que.pop();//刪除該元素
        if (p.first == gx&&p.second == gy)//是終點結束
            break;
        for (int i = 0; i < 4; i++)//四個方向的迴圈
        {
            int nx = p.first + dx[i],ny = p.second + dy[i];//移動後的位置標記為(nx,ny)
            if (0 <= nx&&nx < n && 0 <= ny&&ny < m&&maze[nx][ny] != '#'&&d[nx][ny] == INF)//判斷是否可以移動以及是否存取過(即d[nx][ny]!=INF)
            {
                que.push(P(nx, ny));//可以移動,新增到佇列
                d[nx][ny] = d[p.first][p.second] + 1;//到該位置的距離為到p的距離+1
            }
        }
    }
    return d[gx][gy];
}

經典了兩個題結束了,好題連結持續更新。。。。。。

 皇后問題

 

八皇后問題是一個以國際象棋為背景的問題:如何能夠在 8×8 的國際象棋棋盤上放置八個皇后,使得任何一個皇后都無法直接吃掉其他的皇后?為了達到此目的,任兩個皇后都不能處於同一條橫行、縱行或斜線上。八皇后問題可以推廣為更一般的n皇后擺放問題:這時棋盤的大小變為n1×n1,而皇后個數也變成n2。而且僅當 n2 ≥ 1 或 n1 ≥ 4 時問題有解。

皇后問題是非常著名的問題,作為一個棋盤類問題,毫無疑問,用暴力搜尋的方法來做是一定可以得到正確答案的,但在有限的執行時間內,我們很難寫出速度可以忍受的搜尋,部分棋盤問題的最優解不是搜尋,而是動態規劃,某些棋盤問題也很適合作為狀態壓縮思想的解釋例題。

進一步說,皇后問題可以用人工智慧相關演演算法和遺傳演演算法求解,可以用多執行緒技術縮短執行時間。本文不做討論。

(本文不展開講狀態壓縮,以後再說)

 

一般思路:

 

N*N的二維陣列,在每一個位置進行嘗試,在當前位置上判斷是否滿足放置皇后的條件(這一點的行、列、對角線上,沒有皇后)。

 

優化1:

 

既然知道多個皇后不能在同一行,我們何必要在同一行的不同位置放多個來嘗試呢?

我們生成一維陣列record,record[i]表示第i行的皇后放在了第幾列。對於每一行,確定當前record值即可,因為每行只能且必須放一個皇后,放了一個就無需繼續嘗試。那麼對於當前的record[i],檢視record[0...i-1]的值,是否有j = record[k](同列)、|record[k] - j| = | k-i |(同一斜線)的情況。由於我們的策略,無需檢查行(每行只放一個)。

public class NQueens {
	public static int num1(int n) {
		if (n < 1) {
			return 0;
		}
		int[] record = new int[n];
		return process1(0, record, n);
	}
	public static int process1(int i, int[] record, int n) {
		if (i == n) {
			return 1;
		}
		int res = 0;
		for (int j = 0; j < n; j++) {
			if (isValid(record, i, j)) {
				record[i] = j;
				res += process1(i + 1, record, n);
			}
		}//對於當前行,依次嘗試每列
		return res;
	}
//判斷當前位置是否可以放置
	public static boolean isValid(int[] record, int i, int j) {
		for (int k = 0; k < i; k++) {
			if (j == record[k] || Math.abs(record[k] - j) == Math.abs(i - k)) {
				return false;
			}
		}
		return true;
	}
	public static void main(String[] args) {
		int n = 8;
		System.out.println(num1(n));
	}
}

優化2:

 

分析:棋子對後續過程的影響範圍:本行、本列、左右斜線。

黑色棋子影響區域為紅色

本行影響不提,根據優化一已經避免

本列影響,一直影響D列,直到第一行在D放棋子的所有情況結束。

 

左斜線:每向下一行,實際上對當前行的影響區域就向左移動

比如:

嘗試第二行時,黑色棋子影響的是我們的第三列;

嘗試第三行時,黑色棋子影響的是我們的第二列;

嘗試第四行時,黑色棋子影響的是我們的第一列;

嘗試第五行及以後幾行,黑色棋子對我們並無影響。

 

右斜線則相反:

隨著行序號增加,影響的列序號也增加,直到影響的列序號大於8就不再影響。

 

我們對於之前棋子影響的區域,可以用二進位制數位來表示,比如:

每一位,用01代表是否影響。

比如上圖,對於第一行,就是00010000

嘗試第二行時,數位變為00100000

第三行:01000000

第四行:10000000

 

對於右斜線的數位,同理:

第一行00010000,之後向右移:00001000,00000100,00000010,00000001,直到全0不影響。

 

同理,我們對於多行資料,也同樣可以記錄了

比如在第一行我們放在了第四列:

第二行放在了G列,這時左斜線記錄為00100000(第一個棋子的影響)+00000010(當前棋子的影響)=00100010。

到第三行數位繼續左移:01000100,然後繼續加上我們的選擇,如此反覆。

 

這樣,我們對於當前位置的判斷,其實可以通過左斜線變數、右斜線變數、列變數,按位元或運算求出(每一位中,三個數有一個是1就不能再放)。

具體看程式碼:

注:怎麼排版就炸了呢。。。貼一張圖吧

public class NQueens {
	public static int num2(int n) {
		// 因為本方法中位運算的載體是int型變數,所以該方法只能算1~32皇后問題
		// 如果想計算更多的皇后問題,需使用包含更多位的變數
		if (n < 1 || n > 32) {
			return 0;
		}
		int upperLim = n == 32 ? -1 : (1 << n) - 1;
        //upperLim的作用為棋盤大小,比如8皇后為00000000 00000000 00000000 11111111
        //32皇后為11111111 11111111 11111111 11111111
		return process2(upperLim, 0, 0, 0);
	}

	public static int process2(int upperLim, int colLim, int leftDiaLim,
			int rightDiaLim) {
		if (colLim == upperLim) {
			return 1;
		}
		int pos = 0;            //pos:所有的合法位置
		int mostRightOne = 0;   //所有合法位置的最右位置

        //所有記錄按位元或之後取反,並與全1按位元與,得出所有合法位置
		pos = upperLim & (~(colLim | leftDiaLim | rightDiaLim));
		int res = 0;//計數
		while (pos != 0) {
			mostRightOne = pos & (~pos + 1);//取最右的合法位置
			pos = pos - mostRightOne;       //去掉本位置並嘗試
			res += process2(
                     upperLim,                             //全域性
                     colLim | mostRightOne,                //列記錄
                     //之前列+本位置
					(leftDiaLim | mostRightOne) << 1,      //左斜線記錄
                     //(左斜線變數+本位置)左移             
					(rightDiaLim | mostRightOne) >>> 1);   //右斜線記錄
                     //(右斜線變數+本位置)右移(高位補零)
		}
		return res;
	}

	public static void main(String[] args) {
		int n = 8;
		System.out.println(num2(n));
	}
}

完整測試程式碼:

32皇后:結果/時間

暴力搜:時間就太長了,懶得測。。。

public class NQueens {

	public static int num1(int n) {
		if (n < 1) {
			return 0;
		}
		int[] record = new int[n];
		return process1(0, record, n);
	}

	public static int process1(int i, int[] record, int n) {
		if (i == n) {
			return 1;
		}
		int res = 0;
		for (int j = 0; j < n; j++) {
			if (isValid(record, i, j)) {
				record[i] = j;
				res += process1(i + 1, record, n);
			}
		}
		return res;
	}

	public static boolean isValid(int[] record, int i, int j) {
		for (int k = 0; k < i; k++) {
			if (j == record[k] || Math.abs(record[k] - j) == Math.abs(i - k)) {
				return false;
			}
		}
		return true;
	}

	public static int num2(int n) {
		if (n < 1 || n > 32) {
			return 0;
		}
		int upperLim = n == 32 ? -1 : (1 << n) - 1;
		return process2(upperLim, 0, 0, 0);
	}

	public static int process2(int upperLim, int colLim, int leftDiaLim,
			int rightDiaLim) {
		if (colLim == upperLim) {
			return 1;
		}
		int pos = 0;
		int mostRightOne = 0;
		pos = upperLim & (~(colLim | leftDiaLim | rightDiaLim));
		int res = 0;
		while (pos != 0) {
			mostRightOne = pos & (~pos + 1);
			pos = pos - mostRightOne;
			res += process2(upperLim, colLim | mostRightOne,
					(leftDiaLim | mostRightOne) << 1,
					(rightDiaLim | mostRightOne) >>> 1);
		}
		return res;
	}

	public static void main(String[] args) {
		int n = 32;

		long start = System.currentTimeMillis();
		System.out.println(num2(n));
		long end = System.currentTimeMillis();
		System.out.println("cost time: " + (end - start) + "ms");

		start = System.currentTimeMillis();
		System.out.println(num1(n));
		end = System.currentTimeMillis();
		System.out.println("cost time: " + (end - start) + "ms");
	}
}

二元搜尋樹實現

本文給出二元搜尋樹介紹和實現

 

首先說它的性質:所有的節點都滿足,左子樹上所有的節點都比自己小,右邊的都比自己大。

 

那這個結構有什麼有用呢?

首先可以快速二分查詢。還可以中序遍歷得到升序序列,等等。。。

基本操作:

1、插入某個數值

2、查詢是否包含某個數值

3、刪除某個數值

 

根據實現不同,還可以實現其他很多種操作。

 

實現思路思路:

前兩個操作很好想,就是不斷比較,大了往左走,小了往右走。到空了插入,或者到空都沒找到。

而刪除稍微複雜一些,有下面這幾種情況:

1、需要刪除的節點沒有左兒子,那就把右兒子提上去就好了。

2、需要刪除的節點有左兒子,這個左兒子沒有右兒子,那麼就把左兒子提上去

3、以上都不滿足,就把左兒子子孫中最大節點提上來。

 

當然,反過來也是成立的,比如右兒子子孫中最小的節點。

 

下面來敘述為什麼可以這麼做。

下圖中A為待刪除節點。

第一種情況:

 

1、去掉A,把c提上來,c也是小於x的沒問題。

2、根據定義可知,x左邊的所有點都小於它,把c提上來不影響規則。

 

第二種情況

 

3、B<A<C,所以B<C,根據剛才的敘述,B可以提上去,c可以放在b右邊,不影響規則

4、同理

 

第三種情況

 

5、注意:是把黑色的提升上來,不是所謂的最右邊的那個,因為當初向左拐了,他一定小。

因為黑色是最大,比B以及B所有的孩子都大,所以讓B當左孩子沒問題

而黑點小於A,也就小於c,所以可以讓c當右孩子

大概證明就這樣。。

下面我們用程式碼實現並通過註釋理解

上次連結串列之類的用的c,迴圈來寫的。這次就c++函數遞迴吧,不同方式練習。

定義

struct node
{
    int val;//資料
    node *lch,*rch;//左右孩子
};

插入

 node *insert(node *p,int x)
 {
     if(p==NULL)//直到空就建立節點
     {
         node *q=new node;
         q->val=x;
         q->lch=q->rch=NULL;
         return p;
     }
     if(x<p->val)p->lch=insert(p->lch,x);
     else p->lch=insert(p->rch,x);
     return p;//依次返回自己,讓上一個函數執行。
 }

查詢

 bool find(node *p,int x)
 {
     if(p==NULL)return false;
     else if(x==p->val)return true;
     else if(x<p->val)return find(p->lch,x);
     else return find(p->rch,x);
 }

刪除

 node *remove(node *p,int x)
 {
      if(p==NULL)return NULL;
      else if(x<p->val)p->lch=remove(p->lch,x);
      else if(x>p->val)p->lch=remove(p->rch,x);
      //以下為找到了之後
      else if(p->lch==NULL)//情況1
      {
          node *q=p->rch;
          delete p;
          return q;
      }
      else if(p->lch->rch)//情況2
      {
          node *q=p->lch;
          q->rch=p->rch;
          delete p;
          return q;
      }
      else
      {
          node *q;
          for(q=p->lch;q->rch->rch!=NULL;q=q->rch);//找到最大節點的前一個
          node *r=q->rch;//最大節點
          q->rch=r->lch;//最大節點左孩子提到最大節點位置
          r->lch=p->lch;//調整黑點左孩子為B
          r->rch=p->rch;//調整黑點右孩子為c
          delete p;//刪除
          return r;//返回給父
      }
      return p;
 }

Abstract Self-Balancing Binary Search Tree

 

二元搜尋樹

 

二叉查詢樹(Binary Search Tree),(又:二元搜尋樹,二叉排序樹)它或者是一棵空樹,或者是具有下列性質的二元樹: 若它的左子樹不空,則左子樹上所有結點的值均小於它的根結點的值; 若它的右子樹不空,則右子樹上所有結點的值均大於它的根結點的值; 它的左、右子樹也分別為二叉排序樹。
具體介紹和實現:https://blog.csdn.net/hebtu666/article/details/81741034

我們知道,對於一般的二元搜尋樹(Binary Search Tree),其期望高度(即為一棵平衡樹時)為log2n,其各操作的時間複雜度(O(log2n))同時也由此而決定。但是,在某些極端的情況下(如在插入的序列是有序的時),二元搜尋樹將退化成近似鏈或鏈,

此時,其操作的時間複雜度將退化成線性的,即O(n)。我們可以通過隨機化建立二元搜尋樹來儘量的避免這種情況,但是在進行了多次的操作之後,由於在刪除時,我們總是選擇將待刪除節點的後繼代替它本身,這樣就會造成總是右邊的節點數目減少,以至於樹向左偏沉。這同時也會造成樹的平衡性受到破壞,提高它的操作的時間複雜度

 

概念引入

 

Abstract Self-Balancing Binary Search Tree:自平衡二元搜尋樹

顧名思義:它在面對任意節點插入和刪除時自動保持其高度

常用演演算法有紅黑樹、AVL、Treap、伸展樹、SB樹等。在平衡二元搜尋樹中,我們可以看到,其高度一般都良好地維持在O(log(n)),大大降低了操作的時間複雜度。這些結構為可變有序列表提供了有效的實現,並且可以用於其他抽象資料結構,例如關聯陣列優先順序佇列集合

對於這些結構,他們都有自己的平衡性,比如:

AVL樹

具有以下性質:它是一棵空樹或它的左右兩個子樹的高度差的絕對值不超過1,並且左右兩個子樹都是一棵平衡二元樹。

根據定義可知,這是根據深度最嚴苛的標準了,左右子樹高度不能差的超過1.

具體介紹和實現:https://blog.csdn.net/hebtu666/article/details/85047648

 

紅黑樹

特性:
(1)每個節點或者是黑色,或者是紅色。
(2)根節點是黑色。
(3)每個葉子節點(NIL)是黑色。 [注意:這裡葉子節點,是指為空(NIL或NULL)的葉子節點!]
(4)如果一個節點是紅色的,則它的子節點必須是黑色的。
(5)從一個節點到該節點的子孫節點的所有路徑上包含相同數目的黑節點。

根據定義,確保沒有一條路徑會比其他路徑長出2倍。

 

size balance tree

Size Balanced Tree(簡稱SBT)是一自平衡二叉查詢樹,是在電腦科學中用到的一種資料結構。它是由中國廣東中山紀念中學的陳啟峰發明的。陳啟峰於2006年底完成論文《Size Balanced Tree》,並在2007年的全國青少年資訊學奧林匹克競賽冬令營中發表。由於SBT的拼寫很容易找到中文諧音,它常被中國的資訊學競賽選手和ACM/ICPC選手們戲稱為「傻B樹」、「Super BT」等。相比紅黑樹、AVL樹等自平衡二叉查詢樹,SBT更易於實現。據陳啟峰在論文中稱,SBT是「目前為止速度最快的高階二元搜尋樹」。SBT能在O(log n)的時間內完成所有二元搜尋樹(BST)的相關操作,而與普通二元搜尋樹相比,SBT僅僅加入了簡潔的核心操作Maintain。由於SBT賴以保持平衡的是size域而不是其他「無用」的域,它可以很方便地實現動態順序統計中的selectrank操作。

對於SBT的每一個結點 t,有如下性質:
   性質(a) s[ right[t] ]≥s[ left [ left[ t ] ] ], s[ right [ left[t] ] ]
   性質(b) s[ left[t] ]≥s[right[ right[t] ] ], s[ left[ right[t] ] ]
即.每棵子樹的大小不小於其兄弟的子樹大小。

 

伸展樹

伸展樹(Splay Tree)是一種二叉排序樹,它能在O(log n)內完成插入、查詢和刪除操作。它由Daniel Sleator和Robert Tarjan創造。它的優勢在於不需要記錄用於平衡樹的冗餘資訊。在伸展樹上的一般操作都基於伸展操作。

 

Treap

Treap是一棵二叉排序樹,它的左子樹和右子樹分別是一個Treap,和一般的二叉排序樹不同的是,Treap紀錄一個額外的資料,就是優先順序。Treap在以關鍵碼構成二叉排序樹的同時,還滿足的性質(在這裡我們假設節點的優先順序大於該節點的孩子的優先順序)。但是這裡要注意的是Treap二元堆積有一點不同,就是二元堆積必須是完全二元樹,而Treap並不一定是。

 

 

 

 

對比可以發現,AVL樹對平衡性的要求比較嚴苛,每插入一個節點就很大概率面臨調整。

而紅黑樹對平衡性的要求沒有那麼嚴苛。可能是多次插入攢夠了一下調整。。。

 

把每一個樹的細節都扣清楚是一件挺無聊的事。。雖然據說紅黑樹都成了面試必問內容,但是實在是不想深究那些細節,這些樹的基本操作也無非是那麼兩種:左旋,右旋。這些樹的所有操作和情況,都是這兩種動作的組合罷了。

所以本文先介紹這兩種基本操作,等以後有時間(可能到找工作時),再把紅黑樹等結構的細節補上。

 

最簡單的旋轉

 

最簡單的例子:

這棵樹,左子樹深度為2,右子樹深度為0,所以,根據AVL樹或者紅黑樹的標準,它都不平衡。。

那怎麼辦?轉過來:

是不是就平衡了?

這就是我們的順時針旋轉,又叫,右旋,因為是以2為軸,把1轉下來了。

左旋同理。

 

帶子樹旋轉

問題是,真正轉起來可沒有這麼簡單:

這才是一顆搜尋樹的樣子啊

ABCD都代表是一顆子樹。我們這三個點轉了可不能不管這些子樹啊對不對。

好,我們想想這些子樹怎麼辦。

首先,AB子樹沒有關係,放在原地即可。

D作為3的右子樹,也可以不動,那剩下一個位置,會不會就是放C子樹呢?

我們想想能否這樣做。

原來:

1)C作為2的右子樹,內任何元素都比2大。

2)C作為3左子樹的一部分,內任何元素都比3小。

轉之後:

1)C作為2的右子樹的一部分,內任何元素都比2大。

2)C作為3左子樹,內任何元素都比3小。

所以,C子樹可以作為3的左子樹,沒有問題。

這樣,我們的操作就介紹完了。

這種基本的變換達到了看似把樹變的平衡的效果。

左右旋轉類似

 

程式碼實現

對於Abstract BinarySearchTree類,上面網址已經給出了思路和c++程式碼實現,把java再貼出來也挺無趣的,所以希望大家能自己實現。

抽象自平衡二元搜尋樹(AbstractSelfBalancingBinarySearchTree)的所有操作都是建立在二元搜尋樹(BinarySearchTree )操作的基礎上來進行的。

各種自平衡二元搜尋樹(AVL、紅黑樹等)的操作也是由Abstract自平衡二元搜尋樹的基本操作:左旋、右旋構成。這個文章只寫了左旋右旋基本操作,供以後各種selfBalancingBinarySearchTree使用。

public abstract class AbstractSelfBalancingBinarySearchTree extends AbstractBinarySearchTree {
    protected Node rotateRight(Node node) {
        Node temp = node.left;//節點2
        temp.parent = node.parent;
        //節點3的父(旋轉後節點2的父)
        node.left = temp.right;
        //節點3接收節點2的右子樹
        if (node.left != null) {
            node.left.parent = node;
        }

        temp.right = node;
        //節點3變為節點2的右孩子
        node.parent = temp;

        //原來節點3的父(若存在),孩子變為節點2
        if (temp.parent != null) {
            if (node == temp.parent.left) {
                temp.parent.left = temp;
            } else {
                temp.parent.right = temp;
            }
        } else {
            root = temp;
        }
        return temp;
    }

    protected Node rotateLeft(Node node) {
        Node temp = node.right;
        temp.parent = node.parent;
        node.right = temp.left;
        if (node.right != null) {
            node.right.parent = node;
        }
        temp.left = node;
        node.parent = temp;
        if (temp.parent != null) {
            if (node == temp.parent.left) {
                temp.parent.left = temp;
            } else {
                temp.parent.right = temp;
            }
        } else {
            root = temp;
        }
        
        return temp;
    }
}

 

AVL Tree

 

前言

 

希望讀者

瞭解二元搜尋樹

瞭解左旋右旋基本操作

https://blog.csdn.net/hebtu666/article/details/84992363

直觀感受直接到文章底部,有正確的調整策略動畫,自行操作。

二元搜尋樹
 

二叉查詢樹(Binary Search Tree),(又:二元搜尋樹,二叉排序樹)它或者是一棵空樹,或者是具有下列性質的二元樹: 若它的左子樹不空,則左子樹上所有結點的值均小於它的根結點的值; 若它的右子樹不空,則右子樹上所有結點的值均大於它的根結點的值; 它的左、右子樹也分別為二叉排序樹。
具體介紹和實現:https://blog.csdn.net/hebtu666/article/details/81741034

我們知道,對於一般的二元搜尋樹(Binary Search Tree),其期望高度(即為一棵平衡樹時)為log2n,其各操作的時間複雜度(O(log2n))同時也由此而決定。但是,在某些極端的情況下(如在插入的序列是有序的時),二元搜尋樹將退化成近似鏈或鏈,

此時,其操作的時間複雜度將退化成線性的,即O(n)。我們可以通過隨機化建立二元搜尋樹來儘量的避免這種情況,但是在進行了多次的操作之後,由於在刪除時,我們總是選擇將待刪除節點的後繼代替它本身,這樣就會造成總是右邊的節點數目減少,以至於樹向左偏沉。這同時也會造成樹的平衡性受到破壞,提高它的操作的時間複雜度。
 

AVL Tree

電腦科學中,AVL樹是最先發明的自平衡二叉查詢樹。在AVL樹中任何節點的兩個子樹的高度最大差別為1,所以它也被稱為高度平衡樹。增加和刪除可能需要通過一次或多次樹旋轉來重新平衡這個樹。AVL樹得名於它的發明者G. M. Adelson-Velsky和E. M. Landis,他們在1962年的論文《An algorithm for the organization of information》中發表了它。

這種結構是對平衡性要求最嚴苛的self-Balancing Binary Search Tree。

旋轉操作繼承自self-Balancing Binary Search Tree

public class AVLTree extends AbstractSelfBalancingBinarySearchTree

旋轉

上面網址中已經介紹了二元搜尋樹的調整和自平衡二元搜尋樹的基本操作(左旋右旋),上篇文章我是這樣定義左旋的:

達到了   看似   更平衡的效果。

我們回憶一下:

看起來好像不是很平,對嗎?我們轉一下:

看起來平了很多。

但!是!

只是看起來而已。

我們知道。ABCD其實都是子樹,他們也有自己的深度,如果是這種情況:

我們簡化一下:

轉之後(A上來,3作為A的右孩子,A的右子樹作為新的3的左孩子):

沒錯,旋轉確實讓樹變平衡了,這是因為,不平衡是由A的左子樹造成的,A的左子樹深度更深。

我們這樣旋轉實際上是讓

A的左子樹相對於B提上去了兩層,深度相對於B,-2,

A的右子樹相對於B提上去了一層,深度相對於B,-1.

而如果是這樣的:

旋轉以後:

依舊是不平的。

那我們怎麼解決這個問題呢?

先3的左子樹旋轉:

細節問題:不再講解

這樣,我們的最深處又成了左子樹的左子樹。然後再按原來旋轉就好了。

 

旋轉總結

 

那我們來總結一下旋轉策略:

單向右旋平衡處理LL:

由於在*a的左子樹根結點的左子樹上插入結點,*a的平衡因子由1增至2,致使以*a為根的子樹失去平衡,則需進行一次右旋轉操作;

單向左旋平衡處理RR:

由於在*a的右子樹根結點的右子樹上插入結點,*a的平衡因子由-1變為-2,致使以*a為根的子樹失去平衡,則需進行一次左旋轉操作;

雙向旋轉(先左後右)平衡處理LR:

由於在*a的左子樹根結點的右子樹上插入結點,*a的平衡因子由1增至2,致使以*a為根的子樹失去平衡,則需進行兩次旋轉(先左旋後右旋)操作。

雙向旋轉(先右後左)平衡處理RL:

由於在*a的右子樹根結點的左子樹上插入結點,*a的平衡因子由-1變為-2,致使以*a為根的子樹失去平衡,則需進行兩次旋轉(先右旋後左旋)操作。

 

深度的記錄

 

我們解決了調整問題,但是我們怎麼發現樹不平衡呢?總不能沒插入刪除一次都遍歷一下求深度吧。

當然要記錄一下了。

我們需要知道左子樹深度和右子樹深度。這樣,我們可以新增兩個變數,記錄左右子樹的深度。

但其實不需要,只要記錄自己的深度即可。然後左右子樹深度就去左右孩子去尋找即可。

這樣就引出了一個問題:深度的修改、更新策略是什麼呢?

單個節點的深度更新

本棵樹的深度=(左子樹深度,右子樹深度)+1

所以寫出節點node的深度更新方法:

    private static final void updateHeight(AVLNode node) {
//不存在孩子,為-1,最後+1,深度為0
        int leftHeight = (node.left == null) ? -1 : ((AVLNode) node.left).height;
        int rightHeight = (node.right == null) ? -1 : ((AVLNode) node.right).height;
        node.height = 1 + Math.max(leftHeight, rightHeight);
    }

 

寫出旋轉程式碼

配合上面的方法和文章頭部給出文章Abstract Self-Balancing Binary Search Tree的旋轉,我們可以AVL樹的四種旋轉:

    private Node avlRotateLeft(Node node) {
        Node temp = super.rotateLeft(node);
        updateHeight((AVLNode)temp.left);
        updateHeight((AVLNode)temp);
        return temp;
    }

    private Node avlRotateRight(Node node) {
        Node temp = super.rotateRight(node);
        updateHeight((AVLNode)temp.right);
        updateHeight((AVLNode)temp);
        return temp;
    }

    protected Node doubleRotateRightLeft(Node node) {
        node.right = avlRotateRight(node.right);
        return avlRotateLeft(node);
    }

    protected Node doubleRotateLeftRight(Node node) {
        node.left = avlRotateLeft(node.left);
        return avlRotateRight(node);
    }

請自行模擬哪些節點的深度記錄需要修改。

 

總寫調整方法

 

我們寫出了旋轉的操作和相應的深度更新。

現在我們把這些方法分情況總寫。

    private void rebalance(AVLNode node) {
        while (node != null) {
            Node parent = node.parent;
            
            int leftHeight = (node.left == null) ? -1 : ((AVLNode) node.left).height;
            int rightHeight = (node.right == null) ? -1 : ((AVLNode) node.right).height;
            int nodeBalance = rightHeight - leftHeight;

            if (nodeBalance == 2) {
                if (((AVLNode)node.right.right).height+1 == rightHeight) {
                    node = (AVLNode)avlRotateLeft(node);
                    break;
                } else {
                    node = (AVLNode)doubleRotateRightLeft(node);
                    break;
                }
            } else if (nodeBalance == -2) {
                if (((AVLNode)node.left.left).height+1 == leftHeight) {
                    node = (AVLNode)avlRotateRight(node);
                    break;
                } else {
                    node = (AVLNode)doubleRotateLeftRight(node);
                    break;
                }
            } else {
                updateHeight(node);//平衡就一直往上更新高度
            }
            
            node = (AVLNode)parent;
        }
    }

插入完工

 

我們的插入就完工了。

    public Node insert(int element) {
        Node newNode = super.insert(element);//插入
        rebalance((AVLNode)newNode);//調整
        return newNode;
    }

 

刪除

也是一樣的思路,自底向上,先一路修改高度後,進行rebalance調整。

    public Node delete(int element) {
        Node deleteNode = super.search(element);
        if (deleteNode != null) {
            Node successorNode = super.delete(deleteNode);
            //結合上面網址二元搜尋樹實現的情況介紹
            if (successorNode != null) {
                // if replaced from getMinimum(deleteNode.right) 
                // then come back there and update heights
                AVLNode minimum = successorNode.right != null ? (AVLNode)getMinimum(successorNode.right) : (AVLNode)successorNode;
                recomputeHeight(minimum);
                rebalance((AVLNode)minimum);
            } else {
                recomputeHeight((AVLNode)deleteNode.parent);//先修改
                rebalance((AVLNode)deleteNode.parent);//再調整
            }
            return successorNode;
        }
        return null;
    }
    /**
     * Recomputes height information from the node and up for all of parents. It needs to be done after delete.
     */
    private void recomputeHeight(AVLNode node) {
       while (node != null) {
          node.height = maxHeight((AVLNode)node.left, (AVLNode)node.right) + 1;
          node = (AVLNode)node.parent;
       }
    }
    
    /**
     * Returns higher height of 2 nodes. 
     */
    private int maxHeight(AVLNode node1, AVLNode node2) {
        if (node1 != null && node2 != null) {
            return node1.height > node2.height ? node1.height : node2.height;
        } else if (node1 == null) {
            return node2 != null ? node2.height : -1;
        } else if (node2 == null) {
            return node1 != null ? node1.height : -1;
        }
        return -1;
    }

請手動模擬哪裡的高度需要改,哪裡不需要改。

 

直觀表現程式

 

如果看的比較暈,或者直接從頭跳下來的同學,這個程式是正確的模擬了,維護AVL樹的策略和一些我沒寫的基本操作。大家可以自己操作,直觀感受一下。

https://www.cs.usfca.edu/~galles/visualization/AVLtree.html?utm_source=qq&utm_medium=social&utm_oi=826801573962338304

 

跳錶介紹和實現

 

想慢慢的給大家自然的引入跳錶。

 

想想,我們

1)在有序數列裡搜尋一個數

2)或者把一個數插入到正確的位置

都怎麼做?

很簡單吧

對於第一個操作,我們可以一個一個比較,在陣列中我們可以二分,這樣比連結串列快

對於第二個操作,二分也沒什麼用,因為找到位置還要在陣列中一個一個挪位置,時間複雜度依舊是o(n)。

那我們怎麼發明一個查詢插入都比較快的結構呢?

 

 

 

可以打一些標記:

這樣我們把標記連起來,搜尋一個數時先從標記開始搜起下一個標記比本身大的話就往下走,因為再往前就肯定不符合要求了。

比如我們要搜尋18:

因為一次可以跨越好多數呀,自然快了一些。

既然可以打標記,我們可以改進一下,選出一些數來再打一層標記:

這樣我們搜尋20是這樣的:

最終我們可以打好多層標記,我們從最高層開始搜尋,一次可以跳過大量的數(依舊是右邊大了就往下走)。

比如搜尋26:

最好的情況,就是每一層的標記都減少一半,這樣到了頂層往下搜尋,其實和二分就沒什麼兩樣,我們最底層用連結串列串起來,插入一個元素也不需要移動元素,所謂跳錶就完成了一大半了。

 

現在的問題是,我們對於一個新數,到底應該給它打幾層標記呢?

(剛開始一個數都沒有,所以解決了這個問題,我們一直用這個策略更新即可)

答案是。。。。。投硬幣,全看臉。

我其實有點驚訝,我以為會有某些很強的和數學相關的演演算法,可以保證一個很好的搜尋效率,是我想多了。

我們對於一個新數位,有一半概率可以打一層標記,有一半概率不可以打。

對於打了一層標記的數,我們依舊是這個方法,它有一半概率再向上打一層標記,依次迴圈。

所以每一層能到達的概率都少一半。

各層的節點數量竟然就可以比較好的維護在很好的效率上(最完美的就是達到了二分的效果)

 

再分析一下,其實對於同一個數位:

等等。。

其實沒必要全都用指標,因為我們知道,通過指標找到一個數可比下標慢多了。

所以同一個數位的所有標記,沒必要再用指標,效率低還不好維護,用一個list儲存即可。

這樣,我們就設計出來一個數位的所有標記組成的結構:

	public static class SkipListNode {
		public Integer value;//本身的值
		public ArrayList<SkipListNode> nextNodes;
//指向下一個元素的結點組成的陣列,長度全看臉。

		public SkipListNode(Integer value) {
			this.value = value;
			nextNodes = new ArrayList<SkipListNode>();
		}
	}

將integer比較的操作封裝一下:

		private boolean lessThan(Integer a, Integer b) {
			return a.compareTo(b) < 0;
		}

		private boolean equalTo(Integer a, Integer b) {
			return a.compareTo(b) == 0;
		}

找到在本層應該往下拐的結點:

		// Returns the node at a given level with highest value less than e
		private SkipListNode findNext(Integer e, SkipListNode current, int level) {
			SkipListNode next = current.nextNodes.get(level);
			while (next != null) {
				Integer value = next.value;
				if (lessThan(e, value)) { // e < value
					break;
				}
				current = next;
				next = current.nextNodes.get(level);
			}
			return current;
		}

這樣我們就寫一個一層層往下找的方法,並且封裝成find(Integer e)的形式:

		// Returns the skiplist node with greatest value <= e
		private SkipListNode find(Integer e) {
			return find(e, head, maxLevel);
		}

		// Returns the skiplist node with greatest value <= e
		// Starts at node start and level
		private SkipListNode find(Integer e, SkipListNode current, int level) {
			do {
				current = findNext(e, current, level);
			} while (level-- > 0);
			return current;
		}

剛才的方法是找到最大的小於等於目標的值,如果找到的值等於目標,跳錶中就存在這個目標。否則不存在。

		public boolean contains(Integer value) {
			SkipListNode node = find(value);
			return node != null && node.value != null && equalTo(node.value, value);
		}

我們現在可以實現加入一個新點了,要注意把每層的標記打好:

		public void add(Integer newValue) {
			if (!contains(newValue)) {
				size++;
				int level = 0;
				while (Math.random() < PROBABILITY) {
					level++;//能有幾層全看臉
				}
				while (level > maxLevel) {//大於當前最大層數
					head.nextNodes.add(null);//直接連繫統最大
					maxLevel++;
				}
				SkipListNode newNode = new SkipListNode(newValue);
				SkipListNode current = head;//前一個結點,也就是說目標應插current之後
				do {//每一層往下走之前就可以設定這一層的標記了,就是連結串列插入一個新節點
					current = findNext(newValue, current, level);
					newNode.nextNodes.add(0, current.nextNodes.get(level));
					current.nextNodes.set(level, newNode);
				} while (level-- > 0);
			}
		}

刪除也是一樣的

		public void delete(Integer deleteValue) {
			if (contains(deleteValue)) {
				SkipListNode deleteNode = find(deleteValue);
				size--;
				int level = maxLevel;
				SkipListNode current = head;
				do {//就是一個連結串列刪除節點的操作
					current = findNext(deleteNode.value, current, level);
					if (deleteNode.nextNodes.size() > level) {
						current.nextNodes.set(level, deleteNode.nextNodes.get(level));
					}
				} while (level-- > 0);
			}
		}

作為一個容器,Iterator那是必須有的吧,裡面肯定有hasNext和next吧?

	public static class SkipListIterator implements Iterator<Integer> {
		SkipList list;
		SkipListNode current;

		public SkipListIterator(SkipList list) {
			this.list = list;
			this.current = list.getHead();
		}

		public boolean hasNext() {
			return current.nextNodes.get(0) != null;
		}

		public Integer next() {
			current = current.nextNodes.get(0);
			return current.value;
		}
	}

這個跳錶我們就實現完了。

現實工作中呢,我們一般不會讓它到無限多層,萬一有一個數它人氣爆炸亂數衝到了一萬層呢?

所以包括redis在內的一些跳錶實現,都是規定了一個最大層數的。

別的好像也沒什麼了。

最後貼出所有程式碼。

import java.util.ArrayList;
import java.util.Iterator;

public SkipListDemo {

	public static class SkipListNode {
		public Integer value;
		public ArrayList<SkipListNode> nextNodes;

		public SkipListNode(Integer value) {
			this.value = value;
			nextNodes = new ArrayList<SkipListNode>();
		}
	}

	public static class SkipListIterator implements Iterator<Integer> {
		SkipList list;
		SkipListNode current;

		public SkipListIterator(SkipList list) {
			this.list = list;
			this.current = list.getHead();
		}

		public boolean hasNext() {
			return current.nextNodes.get(0) != null;
		}

		public Integer next() {
			current = current.nextNodes.get(0);
			return current.value;
		}
	}

	public static class SkipList {
		private SkipListNode head;
		private int maxLevel;
		private int size;
		private static final double PROBABILITY = 0.5;

		public SkipList() {
			size = 0;
			maxLevel = 0;
			head = new SkipListNode(null);
			head.nextNodes.add(null);
		}

		public SkipListNode getHead() {
			return head;
		}

		public void add(Integer newValue) {
			if (!contains(newValue)) {
				size++;
				int level = 0;
				while (Math.random() < PROBABILITY) {
					level++;
				}
				while (level > maxLevel) {
					head.nextNodes.add(null);
					maxLevel++;
				}
				SkipListNode newNode = new SkipListNode(newValue);
				SkipListNode current = head;
				do {
					current = findNext(newValue, current, level);
					newNode.nextNodes.add(0, current.nextNodes.get(level));
					current.nextNodes.set(level, newNode);
				} while (level-- > 0);
			}
		}

		public void delete(Integer deleteValue) {
			if (contains(deleteValue)) {
				SkipListNode deleteNode = find(deleteValue);
				size--;
				int level = maxLevel;
				SkipListNode current = head;
				do {
					current = findNext(deleteNode.value, current, level);
					if (deleteNode.nextNodes.size() > level) {
						current.nextNodes.set(level, deleteNode.nextNodes.get(level));
					}
				} while (level-- > 0);
			}
		}

		// Returns the skiplist node with greatest value <= e
		private SkipListNode find(Integer e) {
			return find(e, head, maxLevel);
		}

		// Returns the skiplist node with greatest value <= e
		// Starts at node start and level
		private SkipListNode find(Integer e, SkipListNode current, int level) {
			do {
				current = findNext(e, current, level);
			} while (level-- > 0);
			return current;
		}

		// Returns the node at a given level with highest value less than e
		private SkipListNode findNext(Integer e, SkipListNode current, int level) {
			SkipListNode next = current.nextNodes.get(level);
			while (next != null) {
				Integer value = next.value;
				if (lessThan(e, value)) { // e < value
					break;
				}
				current = next;
				next = current.nextNodes.get(level);
			}
			return current;
		}

		public int size() {
			return size;
		}

		public boolean contains(Integer value) {
			SkipListNode node = find(value);
			return node != null && node.value != null && equalTo(node.value, value);
		}

		public Iterator<Integer> iterator() {
			return new SkipListIterator(this);
		}

		/******************************************************************************
		 * Utility Functions *
		 ******************************************************************************/

		private boolean lessThan(Integer a, Integer b) {
			return a.compareTo(b) < 0;
		}

		private boolean equalTo(Integer a, Integer b) {
			return a.compareTo(b) == 0;
		}

	}

	public static void main(String[] args) {

	}

}

c語言實現排序和查詢所有演演算法

 

 c語言版排序查詢完成,帶詳細解釋,一下看到爽,能直接執行看效果。

 

/* Note:Your choice is C IDE */
#include "stdio.h"
#include"stdlib.h"
#define MAX 10
void SequenceSearch(int *fp,int Length);
void Search(int *fp,int length);
void Sort(int *fp,int length);
/*
注意:
    1、陣列名x,*(x+i)就是x[i]哦

*/


/*
================================================
功能:選擇排序
輸入:陣列名稱(陣列首地址)、陣列中元素個數
================================================
*/
void select_sort(int *x, int n)
{
    int i, j, min, t;
    for (i=0; i<n-1; i++) /*要選擇的次數:下標:0~n-2,共n-1次*/
    {
        min = i; /*假設當前下標為i的數最小,比較後再調整*/
        for (j=i+1; j<n; j++)/*迴圈找出最小的數的下標是哪個*/
        {
            if (*(x+j) < *(x+min))
                min = j; /*如果後面的數比前面的小,則記下它的下標*/
        }
        if (min != i) /*如果min在迴圈中改變了,就需要交換資料*/
        {
            t = *(x+i);
            *(x+i) = *(x+min);
            *(x+min) = t;
        }
    }
}
/*
================================================
功能:直接插入排序
輸入:陣列名稱(也就是陣列首地址)、陣列中元素個數
================================================
*/

void insert_sort(int *x, int n)
{
    int i, j, t;
    for (i=1; i<n; i++) /*要選擇的次數:下標1~n-1,共n-1次*/
    {
        /*
         暫存下標為i的數。注意:下標從1開始,原因就是開始時
         第一個數即下標為0的數,前面沒有任何數,認為它是排
         好順序的。
        */
        t=*(x+i);
        for (j=i-1; j>=0 && t<*(x+j); j--) /*注意:j=i-1,j--,這裡就是下標為i的數,在它前面有序列中找插入位置。*/
        {
            *(x+j+1) = *(x+j); /*如果滿足條件就往後挪。最壞的情況就是t比下標為0的數都小,它要放在最前面,j==-1,退出迴圈*/
        }
        *(x+j+1) = t; /*找到下標為i的數的放置位置*/
    }
}
/*
================================================
功能:氣泡排序
輸入:陣列名稱(也就是陣列首地址)、陣列中元素個數
================================================
*/
void bubble_sort0(int *x, int n)
{
    int j, h, t;
    for (h=0; h<n-1; h++)/*迴圈n-1次*/
    {
        for (j=0; j<n-2-h; j++)/*每次做的操作類似*/
        {
            if (*(x+j) > *(x+j+1)) /*大的放在後面,小的放到前面*/
            {
                t = *(x+j);
                *(x+j) = *(x+j+1);
                *(x+j+1) = t; /*完成交換*/
            }
        }
    }
}
/*優化:記錄最後下沉位置,之後的肯定有序*/
void bubble_sort(int *x, int n)
{
    int j, k, h, t;
    for (h=n-1; h>0; h=k) /*迴圈到沒有比較範圍*/
    {
        for (j=0, k=0; j<h; j++) /*每次預置k=0,迴圈掃描後更新k*/
        {
            if (*(x+j) > *(x+j+1)) /*大的放在後面,小的放到前面*/
            {
                t = *(x+j);
                *(x+j) = *(x+j+1);
                *(x+j+1) = t; /*完成交換*/
                k = j; /*儲存最後下沉的位置。這樣k後面的都是排序排好了的。*/
            }
        }
    }
}
/*
================================================
功能:希爾排序
輸入:陣列名稱(也就是陣列首地址)、陣列中元素個數
================================================
*/

void shell_sort(int *x, int n)
{
    int h, j, k, t;
    for (h=n/2; h>0; h=h/2) /*控制增量*/
    {
        for (j=h; j<n; j++) /*這個實際上就是上面的直接插入排序*/
        {
            t = *(x+j);
            for (k=j-h; (k>=0 && t<*(x+k)); k-=h)
            {
                *(x+k+h) = *(x+k);
            }
            *(x+k+h) = t;
        }
    }
}
/*
================================================
功能:快速排序
輸入:陣列名稱(也就是陣列首地址)、陣列中起止元素的下標
注:自己畫畫
================================================
*/

void quick_sort(int *x, int low, int high)
{
    int i, j, t;
    if (low < high) /*要排序的元素起止下標,保證小的放在左邊,大的放在右邊。這裡以下標為low的元素(最左邊)為基準點*/
    {
        i = low;
        j = high;
        t = *(x+low); /*暫存基準點的數*/
        while (i<j) /*迴圈掃描*/
        {
            while (i<j && *(x+j)>t) /*在右邊的只要比基準點大仍放在右邊*/
            {
                j--; /*前移一個位置*/
            }
            if (i<j)
            {
                *(x+i) = *(x+j); /*上面的迴圈退出:即出現比基準點小的數,替換基準點的數*/
                i++; /*後移一個位置,並以此為基準點*/
            }
            while (i<j && *(x+i)<=t) /*在左邊的只要小於等於基準點仍放在左邊*/
            {
                i++; /*後移一個位置*/
            }
            if (i<j)
            {
                *(x+j) = *(x+i); /*上面的迴圈退出:即出現比基準點大的數,放到右邊*/
                j--; /*前移一個位置*/
            }
        }
        *(x+i) = t; /*一遍掃描完後,放到適當位置*/
        quick_sort(x,low,i-1);   /*對基準點左邊的數再執行快速排序*/
        quick_sort(x,i+1,high);   /*對基準點右邊的數再執行快速排序*/
    }
}
/*
================================================
功能:堆排序
輸入:陣列名稱(也就是陣列首地址)、陣列中元素個數
注:畫畫
================================================
*/
/*
功能:建堆
輸入:陣列名稱(也就是陣列首地址)、參與建堆元素的個數、從第幾個元素開始
*/
void sift(int *x, int n, int s)
{
    int t, k, j;
    t = *(x+s); /*暫存開始元素*/
    k = s;   /*開始元素下標*/
    j = 2*k + 1; /*左子樹元素下標*/
    while (j<n)
    {
        if (j<n-1 && *(x+j) < *(x+j+1))/*判斷是否存在右孩子,並且右孩子比左孩子大,成立,就把j換為右孩子*/
        {
            j++;
        }
        if (t<*(x+j)) /*調整*/
        {
            *(x+k) = *(x+j);
            k = j; /*調整後,開始元素也隨之調整*/
            j = 2*k + 1;
        }
        else /*沒有需要調整了,已經是個堆了,退出迴圈。*/
        {
            break;
        }
    }
    *(x+k) = t; /*開始元素放到它正確位置*/
}
/*
功能:堆排序
輸入:陣列名稱(也就是陣列首地址)、陣列中元素個數
注:
            *
         *     *
       *   -  *   *
      * * * 
建堆時,從從後往前第一個非葉子節點開始調整,也就是「-」符號的位置
*/
void heap_sort(int *x, int n)
{
    int i, k, t;
//int *p;
    for (i=n/2-1; i>=0; i--)
    {
        sift(x,n,i); /*初始建堆*/
    }
    for (k=n-1; k>=1; k--)
    {
        t = *(x+0); /*堆頂放到最後*/
        *(x+0) = *(x+k);
        *(x+k) = t;
        sift(x,k,0); /*剩下的數再建堆*/
    }
}




// 歸併排序中的合併演演算法
void Merge(int a[], int start, int mid, int end)
{
    int i,k,j, temp1[10], temp2[10];
    int n1, n2;
    n1 = mid - start + 1;
    n2 = end - mid;

    // 拷貝前半部分陣列
    for ( i = 0; i < n1; i++)
    {
        temp1[i] = a[start + i];
    }
    // 拷貝後半部分陣列
    for (i = 0; i < n2; i++)
    {
        temp2[i] = a[mid + i + 1];
    }
    // 把後面的元素設定的很大
    temp1[n1] = temp2[n2] = 1000;
    // 合併temp1和temp2
    for ( k = start, i = 0, j = 0; k <= end; k++)
    {
        //小的放到有順序的陣列裡
        if (temp1[i] <= temp2[j])
        {
            a[k] = temp1[i];
            i++;
        }
        else
        {
            a[k] = temp2[j];
            j++;
        }
    }
}

// 歸併排序
void MergeSort(int a[], int start, int end)
{
    if (start < end)
    {
        int i;
        i = (end + start) / 2;
        // 對前半部分進行排序
        MergeSort(a, start, i);
        // 對後半部分進行排序
        MergeSort(a, i + 1, end);
        // 合併前後兩部分
        Merge(a, start, i, end);
    }
}
/*順序查詢*/
void SequenceSearch(int *fp,int Length)
{
    int i;
    int data;
    printf("開始使用順序查詢.\n請輸入你想要查詢的資料.\n");
    scanf("%d",&data);
    for(i=0; i<Length; i++)
        if(fp[i]==data)
        {
            printf("經過%d次查詢,查詢到資料%d,表中位置為%d.\n",i+1,data,i);
            return ;
        }
    printf("經過%d次查詢,未能查詢到資料%d.\n",i,data);
}
/*二分查詢*/
void Search(int *fp,int Length)
{
    int data;
    int bottom,top,middle;
    int i=0;
    printf("開始使用二分查詢.\n請輸入你想要查詢的資料.\n");
    scanf("%d",&data);
    printf("由於二分查詢法要求資料是有序的,現在開始為陣列排序.\n");
    Sort(fp,Length);
    printf("陣列現在已經是從小到大排列,下面將開始查詢.\n");
    bottom=0;
    top=Length;
    while (bottom<=top)
    {
        middle=(bottom+top)/2;
        i++;
        if(fp[middle]<data)
        {
            bottom=middle+1;
        }
        else if(fp[middle]>data)
        {
            top=middle-1;
        }
        else
        {
            printf("經過%d次查詢,查詢到資料%d,在排序後的表中的位置為%d.\n",i,data,middle);
            return;
        }
    }
    printf("經過%d次查詢,未能查詢到資料%d.\n",i,data);
}

/*

下面測試了

*/
void Sort(int *fp,int Length)
{
    int temp;
    int i,j,k;
    printf("現在開始為陣列排序,排列結果將是從小到大.\n");
    for(i=0; i<Length; i++)
        for(j=0; j<Length-i-1; j++)
            if(fp[j]>fp[j+1])
            {
                temp=fp[j];
                fp[j]=fp[j+1];
                fp[j+1]=temp;
            }
    printf("排序完成!\n下面輸出排序後的陣列:\n");
    for(k=0; k<Length; k++)
    {
        printf("%5d",fp[k]);
    }
    printf("\n");

}
/*構造隨機輸出函數類*/
void input(int a[])
{
    int i;
    srand( (unsigned int)time(NULL) );
    for (i = 0; i < 10; i++)
    {
        a[i] = rand() % 100;
    }
    printf("\n");
}
/*構造鍵盤輸入函數類*/
/*void input(int *p)
{
     int i;
     printf("請輸入 %d 個資料 :\n",MAX);
      for (i=0; i<MAX; i++)
      {
       scanf("%d",p++);
      }
      printf("\n");
}*/
/*構造輸出函數類*/
void output(int *p)
{
    int i;
    for ( i=0; i<MAX; i++)
    {
        printf("%d ",*p++);
    }
}
void main()
{
    int start=0,end=3;
    int *p, i, a[MAX];
    int count=MAX;
    int arr[MAX];
    int choise=0;
    /*printf("請輸入你的資料的個數:\n");
    scanf("%d",&count);*/
    /* printf("請輸入%d個資料\n",count);
    for(i=0;i<count;i++)
    {
       scanf("%d",&arr[i]);
    }*/
    /*錄入測試資料*/
    input(a);
    printf("隨機初始陣列為:\n");
    output(a);
    printf("\n");
    do
    {
        printf("1.使用順序查詢.\n2.使用二分查詢法查詢.\n3.退出\n");
        scanf("%d",&choise);
        if(choise==1)
            SequenceSearch(a,count);
        else if(choise==2)
            Search(a,count);
        else if(choise==3)
            break;
    }
    while (choise==1||choise==2||choise==3);


    /*錄入測試資料*/
    input(a);
    printf("隨機初始陣列為:\n");
    output(a);
    printf("\n");
    /*測試選擇排序*/
    p = a;
    printf("選擇排序之後的資料:\n");
    select_sort(p,MAX);
    output(a);
    printf("\n");
    system("pause");
    /**/
    /*錄入測試資料*/
    input(a);
    printf("隨機初始陣列為:\n");
    output(a);
    printf("\n");
    /*測試直接插入排序*/
    printf("直接插入排序之後的資料:\n");
    p = a;
    insert_sort(p,MAX);
    output(a);
    printf("\n");
    system("pause");
    /*錄入測試資料*/
    input(a);
    printf("隨機初始陣列為:\n");
    output(a);
    printf("\n");
    /*測試氣泡排序*/
    printf("氣泡排序之後的資料:\n");
    p = a;
    insert_sort(p,MAX);
    output(a);
    printf("\n");
    system("pause");
    /*錄入測試資料*/
    input(a);
    printf("隨機初始陣列為:\n");
    output(a);
    printf("\n");
    /*測試快速排序*/
    printf("快速排序之後的資料:\n");
    p = a;
    quick_sort(p,0,MAX-1);
    output(a);
    printf("\n");
    system("pause");
    /*錄入測試資料*/
    input(a);
    printf("隨機初始陣列為:\n");
    output(a);
    printf("\n");
    /*測試堆排序*/
    printf("堆排序之後的資料:\n");
    p = a;
    heap_sort(p,MAX);
    output(a);
    printf("\n");
    system("pause");
    /*錄入測試資料*/
    input(a);
    printf("隨機初始陣列為:\n");
    output(a);
    printf("\n");
    /*測試歸併排序*/
    printf("歸併排序之後的資料:\n");
    p = a;
    MergeSort(a,start,end);
    output(a);
    printf("\n");
    system("pause");
}