精华内容
下载资源
问答
  • 一般将来时定义结构、例句、用法.doc
  • 本文代码实现基本按照《数据结构》课本目录顺序,外加大量的复杂算法实现,一篇文章足够。能换你一个收藏了吧?

    本文代码实现基本按照《数据结构》课本目录顺序,外加大量的复杂算法实现,一篇文章足够。能换你一个收藏了吧?

     当然如果落下什么了欢迎大家评论指出

    目录

    顺序存储线性表实现 

    单链表不带头标准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");
    }

     

     

    展开全文
  • 数据结构基础概念篇

    万次阅读 多人点赞 2017-11-14 13:44:24
    数据结构就是研究数据的逻辑结构和物理结构以及它们之间相互关系,并对这种结构定义相应的运算,而且确保经过这些运算后所得到的新结构仍然是原来的结构类型。数据:所有能被输入到计算机中,且能被计算机处理的...

    数据结构

    一些概念

    数据结构就是研究数据的逻辑结构物理结构以及它们之间相互关系,并对这种结构定义相应的运算,而且确保经过这些运算后所得到的新结构仍然是原来的结构类型。

    1. 数据:所有能被输入到计算机中,且能被计算机处理的符号的集合。是计算机操作的对象的总称。
    2. 数据元素:数据(集合)中的一个“个体”,数据及结构中讨论的基本单位
    3. 数据项:数据的不可分割的最小单位。一个数据元素可由若干个数据项组成。
    4. 数据类型:在一种程序设计语言中,变量所具有的数据种类。整型、浮点型、字符型等等

    5. 逻辑结构:数据之间的相互关系。

      • 集合 结构中的数据元素除了同属于一种类型外,别无其它关系。
      • 线性结构 数据元素之间一对一的关系
      • 树形结构 数据元素之间一对多的关系
      • 图状结构或网状结构 结构中的数据元素之间存在多对多的关系
    6. 物理结构/存储结构:数据在计算机中的表示。物理结构是描述数据具体在内存中的存储(如:顺序结构、链式结构、索引结构、哈希结构)等
    7. 在数据结构中,从逻辑上可以将其分为线性结构和非线性结构
    8. 数据结构的基本操作的设置的最重要的准则是,实现应用程序与存储结构的独立。实现应用程序是“逻辑结构”,存储的是“物理结构”。逻辑结构主要是对该结构操作的设定,物理结构是描述数据具体在内存中的存储(如:顺序结构、链式结构、索引结构、希哈结构)等。
    9. 顺序存储结构中,线性表的逻辑顺序和物理顺序总是一致的。但在链式存储结构中,线性表的逻辑顺序和物理顺序一般是不同的。

    10. 算法五个特性: 有穷性、确定性、可行性、输入、输出

    11. 算法设计要求:正确性、可读性、健壮性、高效率与低存储量需求。(好的算法)
    12. 算法的描述有伪程序、流程图、N-S结构图等。E-R图是实体联系模型,不是程序的描述方式。
    13. 设计算法在执行时间时需要考虑:算法选用的规模、问题的规模
    14. 时间复杂度:算法的执行时间与原操作执行次数之和成正比。时间复杂度有小到大:O(1)、O(logn)、O(n)、O(nlogn)、O(n2)、O(n3)。幂次时间复杂度有小到大O(2n)、O(n!)、O(nn)
    15. 空间复杂度:若输入数据所占空间只取决于问题本身,和算法无关,则只需要分析除输入和程序之外的辅助变量所占额外空间

    线性表

    线性表是一种典型的线性结构。头结点无前驱有一个后继,尾节点无后继有一个前驱。链表只能顺序查找,定位一个元素的时间为O(N),删除一个元素的时间为O(1)

    1. 线性表的顺序存储结构:把线性表的结点按逻辑顺序依次存放在一组地址连续的存储单元里。用这种方法存储的线性表简称顺序表。是一种随机存取的存储结构。顺序存储指内存地址是一块的,随机存取指访问时可以按下标随机访问,存储和存取是不一样的。如果是存储,则是指按顺序的,如果是存取,则是可以随机的,可以利用元素下标进行。数组比线性表速度更快的是:原地逆序、返回中间节点、选择随机节点。
      • 便于线性表的构造和任意元素的访问
      • 插入:插入新结点,之后结点后移。平均时间复杂度:O(n)
      • 删除:删除节点,之后结点前移。平均时间复杂度:O(n)
    2. 线性链表:用一组任意的存储单元来依次存放线性表的结点,这组存储单元即可以是连续的,也可以是不连续的,甚至是零散分布在内存中的任意位置上的。因此,链表中结点的逻辑次序和物理次序不一定相同。为了能正确表示结点间的逻辑关系,在存储每个结点值的同时,还必须存储指示其后继结点的地址。data域是数据域,用来存放结点的值。next是指针域(亦称链域),用来存放结点的直接后继的地址(或位置)。不需要事先估计存储空间大小。
      • 单链表中每个结点的存储地址是存放在其前趋结点next域中,而开始结点无前趋,故应设头指针head指向开始结点。同时,由于最后一个结点无后继,故结点的指针域为空,即NULL。头插法建表(逆序)、尾插法建表(顺序)。增加头结点的目的是算法实现上的方便,但增大了内存开销。
        • 查找:只能从链表的头指针出发,顺链域next逐个结点往下搜索,直到搜索到第i个结点为止。因此,链表不是随机存取结构
        • 插入:先找到表的第i-1的存储位置,然后插入。新结点先连后继,再连前驱。
        • 删除:首先找到ai-1的存储位置p。然后令p–>next指向ai的直接后继结点,即把ai从链上摘下。最后释放结点ai的空间.r=p->next;p->next=r->next;delete r。
        • 判断一个单向链表中是否存在环的最佳方法是快慢指针。
      • 静态链表:用一维数组来实现线性链表,这种用一维数组表示的线性链表,称为静态链表。静态:体现在表的容量是一定的。(数组的大小);链表:插入与删除同前面所述的动态链表方法相同。静态链表中指针表示的是下一元素在数组中的位置。
      • 静态链表是用数组实现的,是顺序的存储结构,在物理地址上是连续的,而且需要预先分配大小。动态链表是用申请内存函数(C是malloc,C++是new)动态申请内存的,所以在链表的长度上没有限制。动态链表因为是动态申请内存的,所以每个节点的物理地址不连续,要通过指针来顺序访问。静态链表在插入、删除时也是通过修改指针域来实现的,与动态链表没有什么分别
      • 循环链表:是一种头尾相接的链表。其特点是无须增加存储量,仅对表的链接方式稍作改变,即可使得表处理更加方便灵活。
        • 在单链表中,将终端结点的指针域NULL改为指向表头结点的或开始结点,就得到了单链形式的循环链表,并简单称为单循环链表。由于循环链表中没有NULL指针,故涉及遍历操作时,其终止条件就不再像非循环链表那样判断p或p—>next是否为空,而是判断它们是否等于某一指定指针,如头指针或尾指针等。
      • 双向链表:在单链表的每个结点里再增加一个指向其直接前趋的指针域prior。这样就形成的链表中有两个方向不同的链。双链表一般由头指针唯一确定的,将头结点和尾结点链接起来构成循环链表,并称之为双向链表。设指针p指向某一结点,则双向链表结构的对称性可用下式描述:p—>prior—>next=p=p—>next—>prior。从两个方向搜索双链表,比从一个方向搜索双链表的方差要小。
        • 插入:先搞定插入节点的前驱和后继,再搞定后结点的前驱,最后搞定前结点的后继。
        • 在有序双向链表中定位删除一个元素的平均时间复杂度为O(n)
        • 可以直接删除当前指针所指向的节点。而不需要像单向链表中,删除一个元素必须找到其前驱。因此在插入数据时,单向链表和双向链表操作复杂度相同,而删除数据时,双向链表的性能优于单向链表

    栈和队列

    栈(Stack)是限制在表的一端进行插入和删除运算的线性表,通常称插入、删除的这一端为栈顶(Top),另一端为栈底(Bottom)。先进后出。top= -1时为空栈,top=0只能说明栈中只有一个元素,并且元素进栈时top应该自增

    1. 顺序存储栈:顺序存储结构
    2. 链栈:链式存储结构。插入和删除操作仅限制在链头位置上进行。栈顶指针就是链表的头指针。通常不会出现栈满的情况。 不需要判断栈满但需要判断栈空。
    3. 两个栈共用静态存储空间,对头使用也存在空间溢出问题。栈1的底在v[1],栈2的底在V[m],则栈满的条件是top[1]+1=top[2]。
    4. 基本操作:删除栈顶元素、判断栈是否为空以及将栈置为空栈等
    5. 对于n各元素的入栈问题,可能的出栈顺序有C(2n,n)/(n+1)个。
    6. 堆栈溢出一般是循环的递归调用、大数据结构的局部变量导致的

    应用,代码

    1. 进制转换
    2. 括号匹配的检验
    3. 行编辑程序
    4. 迷宫求解:若当前位置“可通”,则纳入路径,继续前进;若当前位置“不可通”,则后退,换方向继续探索;若四周“均无通路”,则将当前位置从路径中删除出去。
    5. 表达式求解:前缀、中缀、后缀。
      • 操作数之间的相对次序不变;
      • 运算符的相对次序不同;
      • 中缀式丢失了括弧信息,致使运算的次序不确定
      • 前缀式的运算规则为:连续出现的两个操作数和在它们之前且紧靠它们的运算符构成一个最小表达式
      • 后缀式的运算规则为:运算符在式中出现的顺序恰为表达式的运算顺序;每个运算符和在它之前出现且紧靠它的两个操作数构成一个最小表达式。
    6. 实现递归:多个函数嵌套调用的规则是:后调用先返回。
    7. 浏览器历史纪录,Android中的最近任务,Activity的启动模式,CPU中栈的实现,Word自动保存,解析计算式,解析xml/json。解析XML时,需要校验节点是否闭合,节点闭合的话,有头尾符号相对应,遇到头符号将其放入栈中,遇到尾符号时,弹出栈的内容,看是否有与之对应的头符号,栈的特性刚好符合符号匹配的就近原则。

    不是所有的递归程序都需要栈来保护现场,比方说求阶乘的,是单向递归,直接用循环去替代从1乘到n就是结果了,另外一些需要栈保存的也可以用队列等来替代。不是所有的递归转化为非递归都要用到栈。转化为非递归主要有两种方法:对于尾递归或单向递归,可以用循环结构算法代替

    队列

    队列(Queue)也是一种运算受限的线性表。它只允许在表的一端进行插入,而在另一端进行删除。允许删除的一端称为队头(front),允许插入的一端称为队尾(rear)。先进先出。

    1. 顺序队列:顺序存储结构。当头尾指针相等时队列为空。在非空队列里,头指针始终指向队头前一个位置,而尾指针始终指向队尾元素的实际位置
    2. 循环队列。在循环队列中进行出队、入队操作时,头尾指针仍要加1,朝前移动。只不过当头尾指针指向向量上界(MaxSize-1)时,其加1操作的结果是指向向量的下界0。除非向量空间真的被队列元素全部占用,否则不会上溢。因此,除一些简单的应用外,真正实用的顺序队列是循环队列。故队空和队满时头尾指针均相等。因此,我们无法通过front=rear来判断队列“空”还是“满”
    3. 链队列:链式存储结构。限制仅在表头删除和表尾插入的单链表。显然仅有单链表的头指针不便于在表尾做插入操作,为此再增加一个尾指针,指向链表的最后一个结点。
    4. 设尾指针的循环链表表示队列,则入队和出队算法的时间复杂度均为O(1)。用循环链表表示队列,必定有链表的头结点,入队操作在链表尾插入,直接插入在尾指针指向的节点后面,时间复杂度是常数级的;出队操作在链表表头进行,也就是删除表头指向的节点,时间复杂度也是常数级的。

    5. 队空条件:rear==front,但是一般需要引入新的标记来说明栈满还是栈空,比如每个位置布尔值

    6. 队满条件:(rear+1) % QueueSize==front,其中QueueSize为循环队列的最大长度
    7. 计算队列长度:(rear-front+QueueSize)% QueueSize
    8. 入队:(rear+1)% QueueSize
    9. 出队:(front+1)% QueueSize
    10. 假设以数组A[N]为容量存放循环队列的元素,其头指针是front,当前队列有X个元素,则队列的尾指针值为(front+X mod N)

    串(String)是零个或多个字符组成的有限序列。长度为零的串称为空串(Empty String),它不包含任何字符。通常将仅由一个或多个空格组成的串称为空白串(Blank String) 注意:空串和空白串的不同,例如“ ”和“”分别表示长度为1的空白串和长度为0的空串。

    串的表示和实现:

    1. 定长顺序存储表示。静态存储分配的顺序表。
    2. 堆分配存储表示。存储空间是在程序执行过程中动态分配而得。所以也称为动态存储分配的顺序表
    3. 串的链式存储结构。

    串匹配:将主串称为目标串,子串称之为模式串。蛮力法匹配。KMP算法匹配。Boyer-Moore算法匹配。

    数组和广义表

    数组和广义表可看成是一种特殊的线性表,其特殊在于: 表中的元素本身也是一种线性表。内存连续。根据下标在O(1)时间读/写任何元素。

    二维数组,多维数组,广义表、树、图都属于非线性结构

    数组

    数组的顺序存储:行优先顺序;列优先顺序。数组中的任一元素可以在相同的时间内存取,即顺序存储的数组是一个随机存取结构。

    关联数组(Associative Array),又称映射(Map)、字典( Dictionary)是一个抽象的数据结构,它包含着类似于(键,值)的有序对。 不是线性表。

    矩阵的压缩:

    1. 对称矩阵、三角矩阵:直接存储矩阵的上三角或者下三角元素。注意区分i>=j和i

    广义表

    广义表(Lists,又称列表)是线性表的推广。广义表是n(n≥0)个元素a1,a2,a3,…,an的有限序列,其中ai或者是原子项,或者是一个广义表。若广义表LS(n>=1)非空,则a1是LS的表头,其余元素组成的表(a2,…an)称为LS的表尾。广义表的元素可以是广义表,也可以是原子,广义表的元素也可以为空。表尾是指除去表头后剩下的元素组成的表,表头可以为表或单元素值。所以表尾不可以是单个元素值。

    例子:

    1. A=()——A是一个空表,其长度为零。
    2. B=(e)——表B只有一个原子e,B的长度为1。
    3. C=(a,(b,c,d))——表C的长度为2,两个元素分别为原子a和子表(b,c,d)。
    4. D=(A,B,C)——表D的长度为3,三个元素都是广义 表。显然,将子表的值代入后,则有D=(( ),(e),(a,(b,c,d)))。
    5. E=(a,E)——这是一个递归的表,它的长度为2,E相当于一个无限的广义表E=(a,(a,(a,(a,…)))).

    三个结论:

    1. 广义表的元素可以是子表,而子表的元素还可以是子表。由此,广义表是一个多层次的结构,可以用图形象地表示
    2. 广义表可为其它表所共享。例如在上述例4中,广义表A,B,C为D的子表,则在D中可以不必列出子表的值,而是通过子表的名称来引用。
    3. 广义表的递归性

    考点:

    1. 广义表是0个或多个单因素或子表组成的有限序列,广义表可以是自身的子表,广义表的长度n>=0,所以可以为空表。广义表的同级元素(直属于同一个表中的各元素)具有线性关系
    2. 广义表的表头为空,并不代表该广义表为空表。广义表()和(())不同。前者是长度为0的空表,对其不能做求表头和表尾的运算;而后者是长度为l的非空表(只不过该表中惟一的一个元素是空表),对其可进行分解,得到的表头和表尾均是空表()
    3. 已知广义表LS=((a,b,c),(d,e,f)),运用head和tail函数取出LS中原子e的运算是head(tail(head(tail(LS)))。根据表头、表尾的定义可知:任何一个非空广义表的表头是表中第一个元素,它可以是原子,也可以是子表,而其表尾必定是子表。也就是说,广义表的head操作,取出的元素是什么,那么结果就是什么。但是tail操作取出的元素外必须加一个表——“()“。tail(LS)=((d,e,f));head(tail(LS))=(d,e,f);tail(head(tail(LS)))=(e,f);head(tail(head(tail(LS))))=e。
    4. 二维以上的数组其实是一种特殊的广义表
    5. 在(非空)广义表中:1、表头head可以是原子或者一个表 2、表尾tail一定是一个表 3.广义表难以用顺序存储结构 4.广义表可以是一个多层次的结构

    树和二叉树

    一种非线性结构。树是递归结构,在树的定义中又用到了树的概念。

    基本术语:

    1. 树结点:包含一个数据元素及若干指向子树的分支;
    2. 孩子结点:结点的子树的根称为该结点的孩子;
    3. 双亲结点:B结点是A结点的孩子,则A结点是B结点的双亲;
    4. 兄弟结点:同一双亲的孩子结点;
    5. 堂兄结点:同一层上结点;
    6. 结点层次:根结点的层定义为1;根的孩子为第二层结点,依此类推;
    7. 树的高(深)度:树中最大的结点层
    8. 结点的度:结点子树的个数
    9. 树的度: 树中最大的结点度。
    10. 叶子结点:也叫终端结点,是度为0的结点;
    11. 分枝结点:度不为0的结点(非终端结点);
    12. 森林:互不相交的树集合;
    13. 有序树:子树有序的树,如:家族树;
    14. 无序树:不考虑子树的顺序;

    二叉树

    二叉树可以为空。二叉树结点的子树要区分左子树和右子树,即使只有一棵子树也要进行区分,说明它是左子树,还是右子树。这是二叉树与树的最主要的差别。注意区分:二叉树、二叉查找树/二叉排序树/二叉搜索树二叉平衡(查找)树

    二叉平衡树肯定是一颗二叉排序树。堆不是一颗二叉平衡树。

    二叉树与树是不同的,二叉树不等价于分支树最多为二的有序树。当一个结点只包含一个子节点时,对于有序树并无左右孩子之分,而对于二叉树来说依然有左右孩子之分,所以二叉树与树是两种不同的结构。

    性质:

    1. 在二叉树的第 i 层上至多有2i-1个结点。
    2. 深度为 k 的二叉树上至多含 2k-1 个结点(k≥1)
    3. 对任何一棵二叉树,若它含有n0个叶子结点、n2个度为 2 的结点,则必存在关系式:n0= n2+1。
    4. 具有 n 个结点的完全二叉树的深度为⎣log2 n⎦+1 。
    5. n个结点的二叉树中,完全二叉树具有最小的路径长度。
    6. 如果对一棵有n个结点的完全二叉树的结点按层序编号,则对任一结点i(1<=i<=n),有:
      • 如果i=1,则结点i无双亲,是二叉树的根;如果i>1,则其双亲的编号是 i/2(整除)。
      • 如果2i>n,无左孩子;否则,其左孩子是结点2i。
      • 如果2i+1>n,则结点i无右孩子;否则,其右孩子是结点2i+1。

    二叉树的存储结构

    1. 顺序存储结构:仅仅适用于满或完全二叉树,结点之间的层次关系由性质5确定。
    2. 二叉链表法:每个节点存储左子树和右子树。三叉链表:左子树、右子树、父节点,总的指针是n+2
    3. 在有n个结点的二叉链表中,值为非空的链域的个数为n-1。在有N个结点的二叉链表中必定有2N个链域。除根结点外,其余N-1个结点都有一个父结点。所以,一共有N-1个非空链域,其余2N-(N-1)=N+1个为空链域。
    4. 二叉链存储法也叫孩子兄弟法,左指针指向左孩子,右指针指向右兄弟。而中序遍历的顺序是左孩子,根,右孩子。这种遍历顺序与存储结构不同,因此需要堆栈保存中间结果。而中序遍历检索二叉树时,由于其存储结构跟遍历顺序相符,因此不需要用堆栈。

    遍历二叉树和线索二叉树

    遍历二叉树:使得每一个结点均被访问一次,而且仅被访问一次。非递归的遍历实现要利用栈。

    • 先序遍历DLR:根节点->左子树->右子树
    • 中序遍历LDR:左子树->根节点->右子树。必须要有中序遍历才能得到一棵二叉树的正确顺序
    • 后续遍历LRD:左子树->右子树->根节点。需要栈的支持。
    • 层次遍历:用一维数组存储二叉树时,总是以层次遍历的顺序存储结点。层次遍历应该借助队列。

    线索二叉树:对二叉树所有结点做某种处理可在遍历过程中实现;检索(查找)二叉树某个结点,可通过遍历实现;如果能将二叉树线索化,就可以简化遍历算法,提高遍历速度,目的是加快查找结点的前驱或后继的速度。

    如何线索化?以中序遍历为例,若能将中序序列中每个结点前趋、后继信息保存起来,以后再遍历二叉树时就可以根据所保存的结点前趋、后继信息对二叉树进行遍历。对于二叉树的线索化,实质上就是遍历一次二叉树,只是在遍历的过程中,检查当前结点左,右指针域是否为空,若为空,将它们改为指向前驱结点或后继结点的线索。前驱就是在这一点之前走过的点,不是下一将要去往的点

    加上结点前趋后继信息(结索)的二叉树称为线索二叉树。n个结点的线索二叉树上每个结点有2个指针域(指向左孩子和右孩子),总共有2n个指针域;一个n个结点的树有n-1条边,那么空指针域= 2n - (n-1) = n + 1,即线索数为n+1。指针域tag为0,存放孩子指针,为1,存放前驱/后继节点指针。

    线索树下结点x的前驱与后继查找:设结点x相应的左(右)标志是线索标志,则lchild(rchild)就是前驱(后继),否则:

    • LDR–前驱:左子树中最靠右边的结点;后继:右子树中最靠左边的结点
    • LRD–前驱:右子树的根,若无右子树,为左子树跟。后继:x是根,后继是空;x是双亲的右孩子、x是双亲的左孩子,但双亲无右孩子,双亲是后继;x是双亲的左孩子,双亲有右孩子,双亲右子树中最左的叶子是后继
    • DLR–对称于LRD线索树—将LRD中所有左右互换,前驱与后继互换,得到DLR的方法。
    • 为简化线索链表的遍历算法,仿照线性链表,为线索链表加上一头结点,约定:
      • 头结点的lchild域:存放线索链表的根结点指针;
      • 头结点的rchild域: 中序序列最后一个结点的指针;
      • 中序序列第一结点lchild域指向头结点;
      • 中序序列最后一个结点的rchild域指向头结点;

    中序遍历的线索二叉树以及线索二叉树链表示意图
    xiansuobinarytree

    一棵左右子树均不空的二叉树在前序线索化后,其中空的链域的个数是1。前序和后续线索化后空链域个数都是1,中序是2。二叉树在线索化后,仍不能有效求解的问题是前序求前序先驱,后序求后序后继。

    中序遍历的顺序为:左、根、右,所以对于每一非空的线索,左子树结点的后继为根结点,右子树结点的前驱为根结点,再递归的执行上面的过程,可得非空线索均指向其祖先结点。在中序线索二叉树中,每一非空的线索均指向其祖先结点

    在二叉树上加上结点前趋、后继线索后,可利用线索对二叉树进行遍历,此时,不需栈,也不需递归。基本步骤:

    1. p=T->lchild; p指向线索链表的根结点;
    2. 若线索链表非空,循环:
      • 循环,顺着p左孩子指针找到最左下结点;访问之;
      • 若p所指结点的右孩子域为线索,p的右孩子结点即为后继结点循环: p=p->rchild; 并访问p所指结点;(在此循环中,顺着后继线索访问二叉树中的结点)
      • 一旦线索“中断”,p所指结点的右孩子域为右孩子指针,p=p->rchild,使 p指向右孩子结点;

    树和森林

    树的存储结构:

    1. 双亲表示法
    2. 孩子表示法
    3. 利用图表示树
    4. 孩子兄弟表示法(二叉树表示法):链表中每个结点的两指针域分别指向其第一个孩子结点和下一个兄弟结点

    将树转化成二叉树:右子树一定为空

    1. 加线:在兄弟之间加一连线
    2. 抹线:对每个结点,除了其左孩子外,去除其与其余孩子之间的关系
    3. 旋转:以树的根结点为轴心,将整树顺时针转45°

    森林转换成二叉树:

    1. 将各棵树分别转换成二叉树
    2. 将每棵树的根结点用线相连
    3. 以第一棵树根结点为二叉树的根

    树与转换后的二叉树的关系:转换后的二叉树的先序对应树的先序遍历;转换后的二叉树的中序对应树的后序遍历

    哈弗曼树/霍夫曼树

    一些概念

    1. 路径:从一个祖先结点到子孙结点之间的分支构成这两个结点间的路径;
    2. 路径长度:路径上的分支数目称为路径长度;
    3. 树的路径长度:从根到每个结点的路径长度之和。
    4. 结点的权:根据应用的需要可以给树的结点赋权值;
    5. 结点的带权路径长度:从根到该结点的路径长度与该结点权的乘积;
    6. 树的带权路径长度=树中所有叶子结点的带权路径之和;通常记作 WPL=∑wi×li
    7. 哈夫曼树:假设有n个权值(w1, w2, … , wn),构造有n个叶子结点的二叉树,每个叶子结点有一个 wi作为它的权值。则带权路径长度最小的二叉树称为哈夫曼树。最优二叉树。

    前缀码的定义:在一个字符集中,任何一个字符的编码都不是另一个字符编码的前缀。霍夫曼编码就是前缀码,可用于快速判断霍夫曼编码是否正确。霍夫曼树是满二叉树,若有n个节点,则共有(n+1)/2个码子

    给定n个权值作为n的叶子结点,构造一棵二叉树,若带权路径长度达到最小,称这样的二叉树为最优二叉树,也称为霍夫曼树(Huffman Tree)。霍夫曼树是带权路径长度最短的树,权值较大的结点离根较近。

    假设哈夫曼树是二叉的话,则度为0的结点个数为N,度为2的结点个数为N-1,则结点总数为2N-1。哈夫曼树的结点个数必为奇数。

    哈夫曼树不一定是完全二叉树,但一定是最优二叉树。

    若度为m的哈夫曼树中,其叶结点个数为n,则非叶结点的个数为[(n-1)/(m-1)]。边的数目等于度。

    图遍历与回溯

    图搜索->形成搜索树

    1. 穷举法。
    2. 贪心法。多步决策,每步选择使得构成一个问题的可能解,同时满足目标函数。
    3. 回溯法。根据题意,选取度量标准,然后将可能的选择方法按度量标准所要求顺序排好,每次处理一个量,得到该意义下的最优解的分解处理。

    无向图

    1. 回路或环:第一个顶点和最后一个顶点相同的路径。
    2. 简单回路或简单环:除第一个顶点和最后一个顶点之外,其余顶点不重复出现的回路
    3. 连通:顶点v至v’ 之间有路径存在
    4. 连通图:无向图图 G 的任意两点之间都是连通的,则称G是连通图。
    5. 连通分量:极大连通子图,子图中包含的顶点个数极大
    6. 所有顶点度的和必须为偶数

    有向图:

    1. 回路或环:第一个顶点和最后一个顶点相同的路径。
    2. 简单回路或简单环:除第一个顶点和最后一个顶点之外,其余顶点不重复出现的回路。
    3. 连通:顶点v至v’之间有路径存在
    4. 强连通图:有向图G的任意两点之间都是连通的,则称G是强连通图。各个顶点间均可达。
    5. 强连通分量:极大连通子图
    6. 有向图顶点的度是顶点的入度与出度之和。邻接矩阵中第V行中的1的个数是V的出度

    7. 生成树:极小连通子图。包含图的所有n个结点,但只含图的n-1条边。在生成树中添加一条边之后,必定会形成回路或环。

    8. 完全图:有 n(n-1)/2 条边的无向图。其中n是结点个数。必定是连通图。
    9. 有向完全图:有n(n-1)条边的有向图。其中n是结点个数。每两个顶点之间都有两条方向相反的边连接的图。
    10. 一个无向图 G=(V,E) 是连通的,那么边的数目大于等于顶点的数目减一:|E|>=|V|-1,而反之不成立。如果 G=(V,E) 是有向图,那么它是强连通图的必要条件是边的数目大于等于顶点的数目:|E|>=|V|,而反之不成立。没有回路的无向图是连通的当且仅当它是树,即等价于:|E|=|V|-1。

    图的存储形式

    1. 邻接矩阵和加权邻接矩阵
      • 无权有向图:出度: i行之和;入度: j列之和。
      • 无权无向图:i结点的度: i行或i列之和。
      • 加权邻接矩阵:相连为w,不相连为∞
    2. 邻接表
      • 用顶点数组表、边(弧)表表示该有向图或无向图
      • 顶点数组表:用数组存放所有的顶点。数组大小为图顶点数n
      • 边表(边结点表):每条边用一个结点进行表示。同一个结点的所有的边形成它的边结点单链表。
      • n个顶点的无向图的邻接表最多有n(n-1)个边表结点。有n个顶点的无向图最多有n*(n-1)/2条边,此时为完全无向图,而在邻接表中每条边存储两次,所以有n*(n-1)个结点

    图的遍历

    深度优先搜索利用栈,广度优先搜索利用队列

    求一条从顶点i到顶点s的简单路径–深搜。求两个顶点之间的一条长度最短的路径–广搜。当各边上的权值均相等时,BFS算法可用来解决单源最短路径问题。

    生成树和最小生成树

    每次遍历一个连通图将图的边分成遍历所经过的边和没有经过的边两部分,将遍历经过的边同图的顶点构成一个子图,该子图称为生成树。因此有DFS生成树和BFS生成树。

    生成树是连通图的极小子图,有n个顶点的连通图的生成树必定有n-1条边,在生成树中任意增加一条边,必定产生回路。若砍去它的一条边,就会把生成树变成非连通子图

    最小生成树:生成树中边的权值(代价)之和最小的树。最小生成树问题是构造连通网的最小代价生成树。

    Kruskal算法:令最小生成树集合T初始状态为空,在有n个顶点的图中选取代价最小的边并从图中删去。若该边加到T中有回路则丢弃,否则留在T中;依此类推,直至T中有n-1条边为止。

    Prim算法、Kruskal算法和Dijkstra算法均属于贪心算法。

    1. Dijkstra算法解决的是带权重的有向图上单源最短路径问题,该算法要求所有边的权重都为非负值。
    2. Dijkstra算法解决了从某个原点到其余各顶点的最短路径问题,由循环嵌套可知该算法的时间复杂度为O(N*N)。若要求任一顶点到其余所有顶点的最短路径,一个比较简单的方法是对每个顶点当做源点运行一次该算法,等于在原有算法的基础上,再来一次循环,此时整个算法的复杂度就变成了O(N*N*N)。
    3. Bellman-Ford算法解决的是一般情况下的单源最短路径问题,在这里,边的权重可以为负值。该算法返回一个布尔值,以表明是否存在一个从源节点可以到达的权重为负值的环路。如果存在这样一个环路,算法将告诉我们不存在解决方案。如果没有这种环路存在,算法将给出最短路径和它们的权重。

    双连通图和关节点

    若从一个连通图中删去任何一个顶点及其相关联的边,它仍为一个连通图的话,则该连通图被称为重(双)连通图

    若连通图中的某个顶点和其相关联的边被删去之后,该连通图被分割成两个或两个以上的连通分量,则称此顶点为关节点

    没有关节点的连通图为双连通图

    1. 若生成树的根结点,有两个或两个以上的分支,则此顶点(生成树的根)必为关节点;
    2. 对生成树上的任意一个非叶“顶点”,若其某棵子树中的所有“顶点”没有和其祖先相通的回边,则该“顶点”必为关节点。

    有向无环图及其应用

    拓扑排序。在用邻接表表示图时,对有n个顶点和e条弧的有向图而言时间复杂度为O(n+e)。一个有向图能被拓扑排序的充要条件就是它是一个有向无环图。拓扑序列唯一不能唯一确定有向图。

    AOV网(Activity On Vertex):用顶点表示活动,边表示活动的优先关系的有向图称为AOV网。AOV网中不允许有回路,这意味着某项活动以自己为先决条件。

    拓扑有序序列:把AOV网络中各顶点按照它们相互之间的优先关系排列一个线性序列的过程。若vi是vj前驱,则vi一定在vj之前;对于没有优先关系的点,顺序任意。

    拓扑排序:对AOV网络中顶点构造拓扑有序序列的过程。方法:

    1. 在有向图中选一个没有前驱的顶点且输出之
    2. 从图中删除该顶点和所有以它为尾的弧
    3. 重复上述两步,直至全部顶点均已输出;或者当图中不存在无前驱的顶点为止(此时说明图中有环)

    采用深度优先搜索拓扑排序算法可以判断出一个有向图中是否有环(回路).深度优先搜索只要在其中记录下搜索的节点数n,当n大于图中节点数时退出,并可以得出有回路。若有回路,则拓扑排序访问不到图中所有的节点,所以也可以得出回路。广度优先搜索过程中如果访问到一个已经访问过的节点,可能是多个节点指向这个节点,不一定是存在环。

    算法描述:

    1. 把邻接表中入度为0的顶点依此进栈
    2. 若栈不空,则
      • 栈顶元素vj退栈并输出;
      • 在邻接表中查找vj的直接后继vk,把vk的入度减1;若vk的入度为0则进栈
    3. 若栈空时输出的顶点个数不是n,则有向图有环;否则,拓扑排序完毕。

    AOE网:带权的有向无环图,其中顶点表示事件,弧表示活动,权表示活动持续时间。在工程上常用来表示工程进度计划。

    一些定义:

    1. 事件的最早发生时间(ve(j)):从源点到j结点的最长的路径。意味着事件最早能够发生的时间。
    2. 事件的最迟发生时间(vl(j)):不影响工程的如期完工,事件j必须发生的时间。
    3. 活动ai由弧

    查找

    顺序查找、折半查找、索引查找、分块查找是静态查找,动态查找有二叉排序树查找,最优二叉树查找,键树查找,哈希表查找

    静态查找表

    顺序表的顺序查找:应用范围:顺序表或线性链表表示的表,表内元素之间无序。查找过程:从表的一端开始逐个进行记录的关键字和给定值的比较。

    顺序有序表的二分查找。平均查找时间(n+1)/n log2(n+1)

    分块查找:将表分成几块,块内无序,块间有序,即前一块中的最大值小于后一块中的最小值。并且有一张索引表,每一项存放每一块的最大值和指向该块第一个元素的指针。索引表有序,块内无序。所以,块间查找用二分查找,块内用顺序查找,效率介于顺序和二分之间;先确定待查记录所在块,再在块内查找。因此跟表中元素个数和块中元素个数都有关。

    1. 用数组存放待查记录,
    2. 建立索引表,由每块中最大(小)的关键字及所属块位置的信息组成。
    3. 当索引表较大时,可以采用二分查找
    4. 在数据量极大时,索引可能很多,可考虑建立索引表的索引,即二级索引,原则上索引不超过三级

    分块查找平均查找长度:ASLbs = Lb + Lw。其中,Lb是查找索引表确定所在块的平均查找长度, Lw是在块中查找元素的平均查找长度。在n一定时,可以通过选择s使ASL尽可能小。当s=sqrt(n)时,ASL最小。

    1. 时间:顺序查找最差,二分最好,分块介于两者之间
    2. 空间:分块最大,需要增加索引数据的空间
    3. 顺序查找对表没有特殊要求
    4. 分块时数据块之间在物理上可不连续。所以可以达到插入、删除数据只涉及对应的块;另外,增加了索引的维护。
    5. 二分查找要求表有序,所以若表的元素的插入与删除很频繁,维持表有序的工作量极大。
    6. 在表不大时,一般直接使用顺序查找。

    动态查找

    二叉排序树的结点删除:

    1. x为叶子结点,则直接删除
    2. x只有左子树xL或只有右子树xR ,则令xL或xR直接成为双亲结点f的子树;
    3. x即有左子树xL也有右子树xR,在xL中选值最大的代替x,该数据按二叉排序树的性质应在最右边。

    平衡二叉树:每个结点的平衡因子都为 1、-1、0 的二叉排序树。或者说每个结点的左右子树的高度最多差1的二叉排序树。

    平衡二叉树的平衡:

    1. 左调整(新结点插入在左子树上的调整):
      • LL(插入在结点左子树的左子树上):旋转前后高度都为h+1
      • LR(新插入结点在左子树的右子树上):旋转前后高度仍为h+1
    2. 右调整(新结点插入在右子树上进行的调整):
      • RR(插入在的右子树的右子树上):处理方法和 LL对称
      • RL(插入在的右子树的左子树上):处理方法和 LR对称

    平衡树建立方法:

    1. 按二叉排序树插入结点
    2. 如引起结点平衡因子变为|2|,则确定旋转点,该点是离根最远(或最接近于叶子的点)
    3. 确定平衡类型后进行平衡处理,平衡后以平衡点为根的子树高不变
    4. 最小二叉平衡树的节点的公式如下 F(n)=F(n-1)+F(n-2)+1 这个类似于一个递归的数列,可以参考Fibonacci数列,1是根节点,F(n-1)是左子树的节点数量,F(n-2)是右子树的节点数量。

    常见的平衡二叉树:

    1. 红黑树是平衡二叉树,也就是左右子树是平衡的,高度大概相等。这种情况等价于一块完全二叉树的高度,查找的时间复杂度是树的高度,为logn,插入操作的平均时间复杂度为O(logn),最坏时间复杂度为O(logn)
      红黑树
      • 节点是红色或黑色。
      • 根是黑色。
      • 所有叶子都是黑色(叶子是NIL节点)。
      • 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
      • 从任一节点到其每个叶子的所有简单路径 都包含相同数目的黑色节点。
    2. avl树也是自平衡二叉树;红黑树和AVL树查找、插入、删除的时间复杂度相同;包含n个内部结点的红黑树的高度是o(logn); TreeMap 是一个红黑树的实现,能保证插入的值保证排序
    3. STL和linux多使用红黑树作为平衡树的实现:
      1. 如果插入一个node引起了树的不平衡,AVL和RB-Tree都是最多只需要2次旋转操作,即两者都是O(1);但是在删除node引起树的不平衡时,最坏情况下,AVL需要维护从被删node到root这条路径上所有node的平衡性,因此需要旋转的量级O(logN),而RB-Tree最多只需3次旋转,只需要O(1)的复杂度。
      2. 其次,AVL的结构相较RB-Tree来说更为平衡,在插入和删除node更容易引起Tree的unbalance,因此在大量数据需要插入或者删除时,AVL需要rebalance的频率会更高。因此,RB-Tree在需要大量插入和删除node的场景下,效率更高。自然,由于AVL高度平衡,因此AVL的search效率更高。
      3. map的实现只是折衷了两者在search、insert以及delete下的效率。总体来说,RB-tree的统计性能是高于AVL的。

    查找总结

    1. 既希望较快的查找又便于线性表动态变化的查找方法是哈希法查找。二叉排序树查找,最优二叉树查找,键树查找,哈希法查找是动态查找。分块、顺序、折半、索引顺序查找均为静态。分块法应该是将整个线性表分成若干块进行保存,若动态变化则可以添加在表的尾部(非顺序结构),时间复杂度是O(1),查找复杂度为O(n);若每个表内部为顺序结构,则可用二分法将查找时间复杂度降至O(logn),但同时动态变化复杂度则变成O(n);顺序法是挨个查找,这种方法最容易实现,不过查找时间复杂度都是O(n),动态变化时可将保存值放入线性表尾部,则时间复杂度为O(1);二分法是基于顺序表的一种查找方式,时间复杂度为O(logn);通过哈希函数将值转化成存放该值的目标地址,O(1)
    2. 二叉树的平均查找长度为O(log2n)——O(n).二叉排序树的查找效率与二叉树的高度有关,高度越低,查找效率越高。二叉树的查找成功的平均查找长度ASL不超过二叉树的高度。二叉树的高度与二叉树的形态有关,n个节点的完全二叉树高度最小,高度为[log2n]+1,n个节点的单只二叉树的高度最大,高度为n,此时查找成功的ASL为最大(n+1)/2,因此二叉树的高度范围为[log2n]+1——n.
    3. 链式存储不能随机访问,必须是顺序存储

    B_树的B+树

    B_树

    B-树就是B树。m阶B_树满足或空,或为满足下列性质的m叉树:

    B-树

    1. 树中每个结点最多有m棵子树
    2. 根结点在不是叶子时,至少有两棵子树
    3. 除根外,所有非终端结点至少有⎡m/2⎤棵子树
    4. 有s个子树的非叶结点具有 n = s-1个关键字,结点的信息组织为:(n,A0,K1,A1,K2,A2 … Kn,An)。这里:n为关键字的个数,ki(i=1,2,…,n)为关键字,且满足Ki小于Ki+1,,Ai(i=0,1,..n)为指向子树的指针。
    5. 所有的叶子结点都出现在同一层上,不带信息(可认为外部结点或失败结点)。
    6. 关键字集合分布在整颗树中
    7. 任何一个关键字出现且只出现在一个结点中
    8. 搜索有可能在非叶子结点结束
    9. 其搜索性能等价于在关键字全集内做一次二分查找
    10. 只适用于随机检索,不适用于顺序检索。
    11. 有结点的平衡因子都为零
    12. M阶B-树中含有N个关键字,最大深度为log⎡m/2⎤(n+1)/2+2

    B_树中结点的插入

    1. m代表B_树的阶,插入总发生在最低层
    2. 插入后关键字个数小于等于 m-1,完成。
    3. 插入后关键字个数等于m,结点分裂,以中点数据为界一分为二,中点数据放到双亲结点中。这样就有可能使得双亲结点的数据个数为m,引起双亲结点的分裂,最坏情况下一直波及到根,引起根的分裂——B_树长高。

    3阶B_树的插入。每个结点最多3棵子树,2个数据;最少2棵子树,1个数据。所以3阶B_树也称为2-3树。

    B_树中结点的删除

    1. 删除发生在最底层
      • 被删关键字所在结点中的关键字数目大于等于 m/2 ,直接删除。
      • 删除后结点中数据为⎡m/2⎤-2,而相邻的左(右)兄弟中数据大于⎡m/2⎤-1,此时左(右兄弟)中最大(小)的数据上移到双亲中,双亲中接(靠)在它后(前)面的数据移到被删数据的结点中
      • 其左右兄弟结点中数据都是⎡m/2⎤-1,此时和左(右)兄弟合并,合并时连同双亲中相关的关键字。此时,双亲中少了一项,因此又可能引起双亲的合并,最坏一直到根,使B-树降低一层。
    2. 删除不在最底层
      • 在大于被删数据中选最小的代替被删数据,问题转换成在最底层的删除

    B+树

    在实际的文件系统中,用的是B+树或其变形。有关性质与操作类似与B_树。

    B+树

    差异:

    1. 有n棵子树的结点中有n个关键字,每个关键字不保存数据,只用来索引,所有数据都保存在叶子节点。
    2. 所有叶子结点中包含全部关键字信息,及对应记录位置信息及指向含有这些关键字记录的指针,且叶子结点本身依关键字的大小自小而大的顺序链接。(而B树的叶子节点并没有包括全部需要查找的信息)
    3. 所有非叶子为索引,结点中仅含有其子树根结点中最大(或最小)关键字。 (而B树的非终节点也包含需要查找的有效信息)
    4. 非叶最底层顺序联结,这样可以进行顺序查找

    B+特性

    1. 所有关键字都出现在叶子结点的链表中(稠密索引),且链表中的关键字恰好是有序的;
    2. 不可能在非叶子结点命中
    3. 非叶子结点相当于是叶子结点的索引(稀疏索引),叶子结点相当于是存储(关键字)数据的数据层
    4. 更适合文件索引系统
    5. B+树插入操作的平均时间复杂度为O(logn),最坏时间复杂度为O(logn)

    查找过程

    • 在 B+ 树上,既可以进行缩小范围的查找,也可以进行顺序查找;
    • 在进行缩小范围的查找时,不管成功与否,都必须查到叶子结点才能结束;
    • 若在结点内查找时,给定值≤Ki, 则应继续在 Ai 所指子树中进行查找

    插入和删除的操作:类似于B_树进行,即必要时,也需要进行结点的“分裂”或“合并”。

    为什么说B+tree比B树更适合实际应用中操作系统的文件索引和数据库索引?

    1. B+tree的磁盘读写代价更低
      • B+tree的内部结点并没有指向关键字具体信息的指针。因此其内部结点相对B 树更小。如果把所有同一内部结点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多。一次性读入内存中的需要查找的关键字也就越多。相对来说IO读写次数也就降低了。
      • 举个例子,假设磁盘中的一个盘块容纳16bytes,而一个关键字2bytes,一个关键字具体信息指针2bytes。一棵9阶B-tree(一个结点最多8个关键字)的内部结点需要2个盘快。而B+树内部结点只需要1个盘快。当需要把内部结点读入内存中的时候,B树就比B+树多一次盘块查找时间(在磁盘中就是盘片旋转的时间)。
    2. B+tree的查询效率更加稳定
      • 由于非终结点并不是最终指向文件内容的结点,而只是叶子结点中关键字的索引。所以任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当。

    B树和B+树都是平衡的多叉树。B树和B+树都可用于文件的索引结构。B树和B+树都能有效的支持随机检索。B+树既能索引查找也能顺序查找.

    哈希表

    1. 在记录的存储地址和它的关键字之间建立一个确定的对应关系;这样不经过比较,一次存取就能得到元素。
    2. 哈希函数——在记录的关键字与记录的存储位置之间建立的一种对应关系。是从关键字空间到存储位置空间的一种映象。
    3. 哈希表——应用哈希函数,由记录的关键字确定记录在表中的位置信息,并将记录根据此信息放入表中,这样构成的表叫哈希表。
    4. Hash查找适合于关键字可能出现的值的集合远远大于实际关键字集合的情形。
    5. 更适合查找,不适合频繁更新
    6. Hash表等查找复杂依赖于Hash值算法的有效性,在最好的情况下,hash表查找复杂度为O(1)。只有无冲突的hash_table复杂度才是O(1)。一般是O(c),c为哈希关键字冲突时查找的平均长度。插入,删除,查找都是O(1)。平均查找长度不随表中结点数目的增加而增加,而是随负载因子的增大而增大
    7. 由于冲突的产生,使得哈希表的查找过程仍然是一个给定值与关键字比较的过程。

    根据抽屉原理,冲突是不可能完全避免的,所以,选择好的散列函数和冲突处理方法:

    1. 构造一个性能好,冲突少的Hash函数
    2. 如何解决冲突

    常用的哈希函数

    1. 直接定址法。仅适合于:地址集合的大小 == 关键字集合的大小
    2. 数字分析法。对关键字进行分析,取关键字的若干位或其组合作哈希地址。仅适合于:能预先估计出全体关键字的每一位上各种数字出现的频度。
    3. 平方取中法。以关键字的平方值的中间几位作为存储地址。
    4. 折叠法。将关键字分割成位数相同的几部分,然后取这几部分的叠加和(舍去进位)做哈希地址。移位叠加/间界叠加。适合于: 关键字的数字位数特别多,且每一位上数字分布大致均匀情况。
    5. 除留余数法。取关键字被某个不大于哈希表表长m的数p除后所得余数作哈希地址,即H(key)=key%p,p<=m。
    6. 随机数法。取关键字的伪随机函数值作哈希地址,即H(key)=random(key),适于关键字长度不等的情况。

    冲突解决

    1. 开放定址法。当冲突发生时,形成一个探查序列;沿此序列逐个地址探查,直到找到一个空位置(开放的地址),将发生冲突的记录放到该地址中。即Hi=(H(key)+di) % m,i=1,2,……k(k<=m-1),H(key)哈希函数,m哈希表长,di增量序列。缺点:删除:只能作标记,不能真正删除;溢出;载因子过大、解决冲突的算法选择不好会发生聚集问题。要求装填因子α较小,故当结点规模较大时会浪费很多空间。
      • 线性探测再散列:di=1,2,3,…,m-1
      • 二次探测再散列:di=12,-12,22,-22,…,±k2(k<=m/2)
      • 伪随机探测再散列: di为伪随机数序列
    2. 链地址法:将所有关键字为同义词的记录存储在一个单链表中,并用一维数组存放头指针。拉链法中可取α≥1,且结点较大时,拉链法中增加的指针域可忽略不计,因此节省空间。一旦发生冲突,在当前位置给单链表增加结点就行。
    3. 其他方法:再哈希法、建立公共溢出区
    4. 在用拉链法构造的散列表中,删除结点的操作易于实现。拉链法的缺点是:指针需要额外的空间,故当结点规模较小时,开放定址法较为节省空间。由于拉链法中各链表上的结点空间是动态申请的,故它更适合于造表前无法确定表长的情况。拉链法解决冲突时,需要使用指针,指示下一个元素的存储位置
    5. 开哈希表–链式地址法;闭哈希表–开放地址法.开哈希和闭哈希主要的区别在于,随着哈希表的密集度提高,使用闭哈希时,不仅会与相同哈希值的元素发生冲突,还容易与不同哈希值的元素发生冲突;而开哈希则不受哈希表疏密与否的影响,始终只会与相同哈希值的元素冲突而已。所以在密集度变大的哈希表中查找时,显然开哈希的平均搜索长度不会增长。
    6. 设有n个关键字具有相同的Hash函数值,则用线性探测法把这n个关键字映射到Hash表中需要做n*(n-1)/2次线性探测。如果使用二次探测再散列法将这n个关键字存入哈希表,至少要进行n*(n+1)/2次探测

    Hash查找效率:装填因子=表中记录数/表容量

    有B+Tree/Hash_Map/STL Map三种数据结构。对于内存中数据,查找性能较好的数据结构是Hash_Map,对于磁盘中数据,查找性能较好的数据结构是B+Tree。Hash操作能根据散列值直接定位数据的存储地址,设计良好的hash表能在常数级时间下找到需要的数据,但是更适合于内存中的查找。B+树是一种是一种树状的数据结构,适合做索引,对磁盘数据来说,索引查找是比较高效的。STL_Map的内部实现是一颗红黑树,但是只是一颗在内存中建立二叉树树,不能用于磁盘操作,而其内存查找性能也比不上Hash查找。

    内部排序

    1. 内部排序:全部数据可同时放入内存进行的排序。
    2. 外部排序:文件中数据太多,无法全部调入内存进行的排序。

    插入类:

    1. 直接插入排序。最坏情况是数据递减序,数据比较和移动量最大,达到O(n2),最好是数据是递增序,比较和移动最少为O(n)。趟数是固定的n-1,即使有序,也要依次从第二个元素开始。排序趟数不等于时间复杂度。
    2. 折半插入排序 。由于插入第i个元素到r[1]到r[i-1]之间时,前i个数据是有序的,所以可以用折半查找确定插入位置,然后插入。
    3. 希尔排序。缩小增量排序。5-3-1。在实际应用中,步长的选取可简化为开始为表长n的一半(n/2),以后每次减半,最后为1。插入的改进,最后一趟已基本有序,比较次数和移动次数相比直接插入最后一趟更少

    交换类:

    1. 冒泡排序。O(n2)通常认为冒泡是比较差的,可以加些改进,比如在一趟中无数据的交换,则结束等措施。
      • 在数据已基本有序时,冒泡是一个较好的方法
      • 在数据量较少时(15个左右)可以用冒泡
    2. 快速排序。
      • 时间复杂度。最好情况:每次支点总在中间,O(nlog2n),平均O(nlog2n)。最坏,数据已是递增或递减,O(n2)。pivotkey的选择越靠近中央,即左右两个子序列长度越接近,排序速度越快。越无序越快。
      • 空间复杂度。需栈空间以实现递归,最坏情况:S(n)=O(n);一般情况:S(n)=O(log2n)
      • 在序列已是有序的情况下,时间复杂度最高。原因:支点选择不当。改进:随机选取支点或最左、最右、中间三个元素中的值处于中间的作为支点,通常可以避免最坏情况。所以,快速排序在表已基本有序的情况下不合适。
      • 在序列长度已较短时,采用直接插入排序、起泡排序等排序方法。序列的个数通常取10左右。

    选择类排序:

    1. 简单选择排序。O(n2)。总比较次数n(n-1)/2。
    2. 堆排序。建堆 O(n),筛选排序O(nlogn)。找出若干个数中最大/最小的前K个数,用堆排序是最好。小根堆中最大的数一定是放在叶子节点上,堆本身是个完全二叉树,完全二叉树的叶子节点的位置大于[n/2]。时间复杂度不会因为待排序序列的有序程度而改变,但是待排序序列的有序程度会影响比较次数。
    3. 归并排序。时间:与表长成正比,若一个表表长是m,另一个是n,则时间是O(m+n)。单独一个数组归并,时间:O(nlogn),空间:O(n),比较次数介于(nlogn)/2和(nlogn)-n+1,赋值操作的次数是(2nlogn)。归并排序算法比较占用内存,但却是效率高且稳定的排序算法。在外排序中使用。归并的趟数是logn。
    4. 基数排序。在一般情况下,每个结点有 d 位关键字,必须执行 t = d次分配和收集操作。分配的代价:O(n);收集的代价:O(rd) (rd是基数);总的代价为:O( d ×(n + rd))。适用于以数字和字符串为关键字的情况。
    5. 枚举排序,通常也被叫做秩排序,比较计数排序。对每一个要排序的元素,统计小于它的所有元素的个数,从而得到该元素在整个序列中的位置,时间复杂度为O(n2)

    比较法分类的下界:O(nlogn)

    排序算法的一些特点:

    1. 堆排序、冒泡排序、快速排序在每趟排序过程中,都会有一个元素被放置在其最终的位置上。
    2. 有字符序列 {Q,H,C,Y,P,A,M,S,R,D,F,X} ,新序列{F,H,C,D,P,A,M,Q,R,S,Y,X},是快速排序算法一趟扫描的结果。(拿Q作为分割点,快速排序一轮。二路归并,第一趟排序,得到 n / 2 个长度为 2 的各自有序的子序列,第二趟排序,得到 n / 4 个长度为 4 的各自有序的子序列H Q C Y A P M S D R F X。如果是快速排序的话,第一个元素t将会被放到一个最准确的位置,t前的数均小于t,后面的数均大于t。希尔排序每个小分组内将会是有序的。堆排序,把它构成一颗二叉树的时候,该堆要么就是大根堆,要么就是小根堆,第一趟Y排在最后;冒泡,那么肯定会有数据下沉的动作,第一趟有A在第一位。)
    3. 在文件”局部有序”或文件长度较小的情况下,最佳内部排序的方法是直接插入排序。(归并排序要求待排序列已经部分有序,而部分有序的含义是待排序列由若干有序的子序列组成,即每个子序列必须有序,并且其时间复杂度为O(nlog2n);直接插入排序在待排序列基本有序时,每趟的比较次数大为降低,即n-1趟比较的时间复杂度由O(n^2)降至O(n)。在待排序的元素序列基本有序或者每个元素距其最终位置不远也可用插入排序,效率最高的排序方法是插入排序
    4. 排序趟数与序列的原始状态有关的排序方法是优化冒泡和快速排序法。(插入排序和选择排序不管序列的原始状态是什么都要执行n-1趟,优化冒泡和快排不一定。仔细理解排序的次数比较次数的区别)
    5. 不稳定的排序方法:快排,堆排,希尔,选择
    6. 要与关键字的初始排列次序无关,那么就是最好、最坏、一般的情况下排序时间复杂度不变, 总共有堆排序,归并排序,选择排序,基数排序
    7. 快速排序、Shell 排序、归并排序、直接插入排序的关键码比较次数与记录的初始排列有关。折半插入排序、选择排序无关。(直接插入排序在完全有序的情况下每个元素只需要与他左边的元素比较一次就可以确定他最终的位置;折半插入排序,比较次数是固定的,与初始排序无关;快速排序,初始排序不影响每次划分时的比较次数,都要比较n次,但是初始排序会影响划分次数,所以会影响总的比较次数,但快排平均比较次数最小;归并排序在归并的时候,如果右路最小值比左路最大值还大,那么只需要比较n次,如果右路每个元素分别比左路对应位置的元素大,那么需要比较2*n-1次,所以与初始排序有关)
    8. 精俭排序,即一对数字不进行两次和两次以上的比较,插入和归并是“精俭排序”。插入排序,前面是有序的,后面的每一个元素与前面有序的元素比较,比较过的就是有序的了,不会再比较一次。归并每次合并后,内部都是有序的,内部的元素之间不用再比较。选择排序,每次在后面的元素中找到最小的,找最小元素的过程是在没有排好序的那部分进行,所有肯定会比较多次。堆排序也需比较多次。

    外部排序

    1. 生成合并段(run):读入文件的部分记录到内存->在内存中进行内部排序->将排好序的这些记录写入外存,形成合并段->再读入该文件的下面的记录,往复进行,直至文件中的记录全部形成合并段为止。
    2. 外部合并:将上一阶段生成的合并段调入内存,进行合并,直至最后形成一个有序的文件。
    3. 外部排序指的是大文件的排序,即待排序的记录存储在外存储器上,待排序的文件无法一次装入内存,需要在内存和外部存储器之间进行多次数据交换,以达到排序整个文件的目的。外部排序最常用的算法是多路归并排序,即将原文件分解成多个能够一次性装入内存的部分,分别把每一部分调入内存完成排序。然后,对已经排序的子文件进行多路归并排序
    4. 不管初始序列是否有序, 冒泡、选择排序时间复杂度是O(n^2),归并、堆排序时间复杂度是O(nlogn)
    5. 外部排序的总时间 = 内部排序(产出初始归并段)所需时间 + 外存信息读取时间 + 内部归并所需的时间
    6. 外排中使用置换选择排序的目的,是为了增加初始归并段的长度。减少外存读写次数需要减小归并趟数

    7. 根据内存容量设若干个输入缓冲区和一个输出缓冲区。若采用二路归并,用两个输入缓冲。

    8. 归并的方法类似于归并排序的归并算法。增加的是对缓冲的监视,对于输入,一旦缓冲空,要到相应文件读后续数据,对于输出缓冲,一旦缓冲满,要将缓冲内容写到文件中去。
    9. 外排序和内排序不只是考虑内外排序算法的性能,还要考虑IO数据交换效率的问题,内存存取速度远远高于外存。影响外排序的时间因素主要是内存与外设交换信息的总次数

    有效的算法设计

    1. 贪心法。Dijkstra的最短路径(时间复杂度O(n2));Prim求最小生成树邻接表存储时是O(n+e),图O(n2);关键路径及关键活动的求法。
    2. 回溯法
    3. 分支限界法
    4. 分治法。分割、求解、合并。二分查找、归并排序、快速排序。
    5. 动态规划。Floyd-Warshall算法求解图中所有点对之间最短路径时间复杂度为O(n3)

    动态规划解题的方法是一种高效率的方法,其时间复杂度通常为O(n2),O(n3)等,可以解决相当大的信息量。(数塔在n<=100层时,可以在很短的时间内得到问题解)

    • 适用的原则:原则为优化原则,即整体优化可以分解为若干个局部优化。
    • 动态规划比穷举法具有较少的计算次数
    • 递归算法需要很大的栈空间,而动态规划不需要栈空间

    贪心和动态规划的差别:

    1. 所谓贪心选择性质是指所求问题的整体最优解可以通过一系列局部最优的选择,即贪心选择来达到。这是贪心算法可行的第一个基本要素,也是贪心算法与动态规划算法的主要区别。
    2. 在动态规划算法中,每步所作的选择往往依赖于相关子问题的解。因而只有在解出相关子问题后,才能作出选择。而在贪心算法中,仅在当前状态下作出最好选择,即局部最优选择。然后再去解作出这个选择后产生的相应的子问题。
    3. 贪心算法所作的贪心选择可以依赖于以往所作过的选择,但决不依赖于将来所作的选择,也不依赖于子问题的解。正是由于这种差别,动态规划算法通常以自底向上的方式解各子问题,而贪心算法则通常以自顶向下的方式进行,以迭代的方式作出相继的贪心选择,每作一次贪心选择就将所求问题简化为一个规模更小的子问题。

    P问题

    1. P问题,如果它可以通过运行多项式次(即运行时间至多是输入量大小的多项式函数的一种算法获得解决),可以找到一个能在多项式的时间里解决它的算法。—-确定性问题
    2. NP问题,虽然可以用计算机求解,但是对于任意常数k,它们不能在O(nk)时间内得到解答,可以在多项式的时间里验证一个解的问题。所有的P类问题都是NP问题。
    3. NP完全问题,知道有效的非确定性算法,但是不知道是否存在有效的确定性算法,同时,不能证明这些问题中的任何一个不存在有效的确定性算法。这类问题称为NP完全问题。
    展开全文
  • 这种将数据结构或对象以某种格式转化为字节流的过程,称之为序列化(Serialization),目的是把当前的状态保存下来,在需要复原数据结构或对象(序列化不包含与对象相关联的函数,所以后面只提数据结构)。...

    Protobuf是Google开源的一个项目,博主将会在几篇博文中对其进行讲解。

    本文实例源码github地址https://github.com/yngzMiao/yngzmiao-blogs/tree/master/2019Q4/20191225


    序列化和反序列化

    有些时候,我们希望将对象保存到文件中,或者传输给其他的应用程序。比如:web网页后端和前端的数据交互,应用程序产生的中间数据等等。

    这种将数据结构或对象以某种格式转化为字节流的过程,称之为序列化(Serialization),目的是把当前的状态保存下来,在需要时复原数据结构或对象(序列化时不包含与对象相关联的函数,所以后面只提数据结构)。反序列化(Deserialization),是序列化的逆过程,读取字节流,根据约定的格式协议,将数据结构复原

    在序列化和反序列化的过程中,需要注意的点:

    • 代码运行过程中,数据结构和对象位于内存,其中的各项数据成员可能彼此紧邻,也可能分布在并不连续的各个内存区域,比如指针指向的内存块等。而文件中字节是顺序存储的,要想将数据结构保存成文件,就需要把所有的数据成员平铺开,然后串接在一起;
    • 直接串接可能是不行的,因为字节流中没有天然的分界,所以在序列化时需要按照某种约定的格式(协议),以便在反序列化时知道从哪里到哪里是哪个数据成员,因此格式可能需要约定:指代数据成员的标识、起始位置、终止位置、长度、分隔符等。

    由上可见,格式协议是最重要的,它直接决定了序列化和反序列化的效率、字节流的大小和可读性等。

    最常见的序列化和反序列化的格式就是json格式、XML格式,他们有自己的一套完整的格式协议。而本文讲解的格式是Protobuf


    Protobuf

    先贴上Protobuf的官方资料:

    Protobuf简介

    ProtobufGoogle开发的一种用于序列化结构化数据(比如Java中的Object,C中的Structure)的语言中立、平台中立、可扩展的数据描述语言,可用于数据存储、通信协议等方面。Protocol Buffers可以理解为是更快、更简单、更小的JSON或者XML,区别在于Protocol Buffers是二进制格式,而JSON和XML是文本格式。

    目前protobuf支持的语言包括:C++、C#、Java、JS、OC、PHP、Ruby这七种。

    相比较于XML、json,Protobuf的优点:

    • 简洁,体积小,消息大小只需要json的10分之一,XML的20分之一;
    • 速度快,解析速度比XML、json快20~100倍;
    • 自动生成数据访问类,方便应用程序的使用。Protobuf编译器会将.proto文件编译生成对应的数据访问类;
    • 向后兼容性好,不必破坏旧数据格式的程序就可以对数据结构进行升级。不必担心因为消息结构的改变而造成的大规模的代码重构或者迁移的问题。

    相对而言,Protobuf也有缺点:

    • protobuf采用了二进制格式进行编码,可读性差;
    • protobuf并非自描述的,必须要有格式定义文件(.proto 文件)。

    既然Protobuf可以自动生成数据访问类,也就是说,只要规定了.proto文件,可以直接生成C++的.cc文件和.h文件,可以直接生成python的.py文件,可以直接生成Java的.java文件……

    那么它是如何生成的呢?这就需要下载安装Protobuf了。

    Protobuf的下载安装

    Protobuf的release版本,下载可以移步:Protobuf release版本

    如果是Linux操作系统下,可以直接下载:protoc-3.8.0-linux-x86_64.zip

    这个版本包含了protoc二进制文件以及与protobuf一起分发的一组标准.proto文件。

    进入bin文件夹,查看protoc的版本信息:

    ./protoc --version
    

    如果打印出了protoc的版本信息,就表示没有任何问题。

    当然,你也可以选择下载Protobuf的源代码,然后通过解压、编译、安装来使用它。这种方式的下载路径为:protobuf-3.8.0.tar.gz

    python2安装步骤如下所示:

    tar -xzf protobuf-3.8.0.tar.gz 
    cd protobuf-3.8.0 
    ./configure --prefix=$INSTALL_DIR 
    make 
    make check 
    make install
    

    python3安装步骤如下所示:

    tar -xzf protobuf-3.8.0.tar.gz 
    cd protobuf-3.8.0 
    ./autogen.sh
    ./configure
    make
    make check
    sudo make install
    sudo ldconfig # refresh shared library cache.
    

    很有可能,执行过程中会出现以下错误提示:

    ./autogen.sh: 4: ./autogen.sh: autoreconf: not found
    

    解决办法:执行以下命令即可。

    sudo apt-get install autoconf
    sudo apt-get install automake
    sudo apt-get install libtool
    

    其实推荐第一种安装方式,在protobuf的使用过程中,一般只需要可执行文件即可。但是,如果你使用C++版本,但自己没有Google对应的protobuf头文件和静态库,还是需要第二种安装方式

    通常情况Protobuf都安装在/usr/local目录下,该目录下包含了Protobuf的头文件,静态库和动态库文件,如果是需要使用C++版本,头文件和静态库需要拷贝出来。

    Protobuf的使用

    在使用Protobuf之前,需要提前创建一个.proto文件。在.proto文件中,需要定义要生成的数据访问类的成员信息等内容。然后,就可以指定该.proto文件来生成了。

    如何直接生成呢?这就需要用到安装的可执行文件了。

    ./protoc 指定.proto文件 --cpp_out=./
    ./protoc 指定.proto文件 --java_out=./
    ./protoc 指定.proto文件 --python_out=./
    

    .proto语法结构

    每个定义结构化数据结构体的.proto文件,也需要按照一定的结构和语法进行编写,但这个语法是非常简单的。

    提前声明一点,proto2和proto3的语法规则是有一定的差异和不兼容性的,需要注意

    版本声明

    在编写.proto文件的最开始部分,需要指定.proto文件版本:

    syntax = "proto2";                  //声明proto2版本(选其一)
    syntax = "proto3";                  //声明proto3版本(选其一)
    

    定义message结构

    使用message定义一个消息类型,与C++、Java等高级语言对应起来就可以理解为Class

    每个message通常由字段修饰符、字段类型、字段名、标识号组成。

    以Person为例,在proto2中:

    message Person {
      required int32 id =1;
      required string name = 2;
      optional int32 age = 3;
      repeated string email = 4;
    }
    

    字段修饰符:只有三种字段修饰符(required、optional、repeated),且每个字段必须有字段修饰符。

    • required:表示该字段的是必须设置的(该限制体现在:若在对应语言中该字段处于未被赋值/初始化的状态,则会报错);
    • optional: 表示该字段的是可选设置的,可通过[default=xxx]指定一个默认值,若没有显示指定默认值并且该字段没有被设置,则会使用该类型的默认值;
    • repeated: 表示该字段可以有多个值,一般会被编译为对应语言的集合类或数组。由于一些历史原因,基本数值类型的repeated的字段并没有被尽可能地高效编码。在新的代码中,用户应该使用特殊选项[packed=true]来保证更高效的编码。

    字段类型:可以指定proto定义的数据类型,当然也可以指定自己定义的数据类型。proto定义的数据类型如下:

    .proto类型C++ TypeJava TypePython TypeNote
    doubledoubledoublefloat
    floatfloatfloatfloat
    int32使用可变长度编码,负数编码效率低下。如果可能具有负值,请改用sint32int32intint
    int64使用可变长度编码,负数编码效率低下。如果可能具有负值,请改用sint64int64longint/long
    uint32使用可变长度编码uint32intint/long
    uint64使用可变长度编码uint64longint/long
    sint32使用可变长度编码int32intint
    sint64使用可变长度编码int64longint/long
    fixed32始终为四个字节uint32intint/long
    fixed64始终为八个字节uint64longint/long
    sfixed32始终为四个字节int32intint
    sfixed64始终为八个字节int64longint/long
    boolboolbooleanbool
    string字符串必须始终包含UTF-8编码或7位ASCII文本stringStringunicode(Python 2)/str(Python 3)
    bytes可以包含任意字节序列stringByteStringbytes

    标识号:在消息定义中,每个字段都有唯一的一个标识符。这些标识符是用来在消息的二进制格式中识别各个字段的,一旦开始使用就不能够再改变

    注:[1, 15]之内的标识号在编码的时候会占用一个字节。[16, 2047]之内的标识号则占用2个字节。所以应该为那些频繁出现的消息元素保留 [1, 15]之内的标识号。切记:要为将来有可能添加的、频繁出现的标识号预留一些标识号。

    但是,在proto3中,对这些规则做了一些的修改:

    • 取消了required字段修饰符,optional字段修饰符可以省略
    • 移除了default选项
    • repeated字段默认采用packed编码,即不需要明确使用[packed=true]来为字段指定比较紧凑的packed编码方式。

    为什么移除default选项?

    proto3中,字段的默认值只能根据字段类型由系统决定。也就是说,默认值全部是约定好的,而不再提供指定默认值的语法。

    proto2中,若某字段被设置为默认值的时候,该字段不会被序列化。这样可以节省空间,提高效率。但这样就无法区分某字段是根本没赋值,还是赋值了默认值。也就是说,如果更新default默认值,会出现意想不到的问题。

    为什么取消required字段修饰符?

    因为required是永久性的:在将一个字段标识为required的时候,应该特别小心。如果在某些情况下不想写入或者发送一个required的字段,将原始该字段修饰符更改为optional可能会遇到问题——旧版本的使用者会认为不含该字段的消息是不完整的,从而可能会无目的的拒绝解析。

    Google的一些工程师得出了一个结论:使用required弊多于利;他们更愿意使用optional和repeated而不是required。当然,这个观点并不具有普遍性。

    也就是说,在proto3中,定义同样的message需要这样:

    message Person {
        string name = 1;
        string phone = 2;
        string email = 3;
        repeated string address = 4;
    }
    

    当然,除了proto定义的数据类型之外,还可以指定自己定义的数据类型,甚至是枚举类型。

    自己定义新的数据类型,只需要在.proto文件中定义新的message类型。枚举类型利用enum开头,需要注意枚举类型的第一个字段的标识号必须为0

    例如,在proto2中:

    syntax = "proto2";
    
    enum PhoneType {
        MOBILE = 0;
        HOME = 1;
        WORK = 2;
    }
    
    message PhoneNumber {
        required string number = 1;
        optional PhoneType type = 2;
    }
    
    message Address {
        optional string country = 1;
        optional string detail = 2;
    }
    
    message Person {
        required int32 id =1;
        required string name = 2;
        optional int32 age = 3;
        repeated string email = 4;
        repeated PhoneNumber phone = 5;
        optional Address address = 6;
    }
    

    在proto3中:

    syntax = "proto3";
    
    enum PhoneType {
        MOBILE = 0;
        HOME = 1;
        WORK = 2;
    }
    
    message PhoneNumber {
        string number = 1;
        PhoneType type = 2;
    }
    
    message Address {
        string country = 1;
        string detail = 2;
    }
    
    message Person {
        int32 id =1;
        string name = 2;
        int32 age = 3;
        repeated string email = 4;
        repeated PhoneNumber phone = 5;
        Address address = 6;
    }
    

    其他定义

    proto可以通过导入import其他.proto文件中的定义来使用它们。即:

    import proto路径
    

    proto可以新增一个可选的package声明符,用来防止不同的消息类型有命名冲突。即:

    package com.yngzmiao;
    

    当然,proto除了这些定义规则之外,还有其他的规则。如message嵌套定义RPC服务接口等等,一般情况下也不会使用到。需要了解的可以参考官方文档。


    相关阅读

    展开全文
  • c/c++结构体的定义及使用:struct typedef struct详细总结(含代码实例)结构体struct详解struct结构体变量一般形式成员的获取赋值指针和数组数组成员赋值结构体嵌套结构体与函数结构体typedef struct详解一般...

    结构体struct详解

    数组(Array),它是一组具有相同类型的数据的集合。但在实际的编程过程中,我们往往还需要一组类型不同的数据,显然不能用一个数组来存放:

    在C语言中,可以使用结构体(Struct) 来存放*一组不同类型的数据*。结构体的定义形式为:

    struct 结构体名{
    	结构体所包含的变量或数组
    };
    

    结构体是一种集合,它里面包含了多个变量或数组,它们的类型可以相同,也可以不同,每个这样的变量或数组都称为结构体的成员(Member),如下:

    struct student{
    	char *name;//姓名
    	int num;//学号
    	int age;//年龄
    	char group;//所在学习小组
    	float score;//成绩
    };
    

    student为结构体名,它包含了 5 个成员,分别是 name、num、age、group、score。结构体成员的定义方式与变量和数组的定义方式相同,只是不能初始化。
    结构体也是一种数据类型,它由程序员自己定义,可以包含多个其他类型的数据。

    struct结构体变量

    既然结构体是一种数据类型,那么就可以用它来定义变量。例如:

    struct student student1,student2;
    

    定义了两个变量 student1student2,它们都是 student 类型,都由 5 个成员组成。注意关键字struct不能少。

    也可以在定义结构体的同时定义结构体变量(如下例所示):

    struct student{
    	char *name;//姓名
    	int num;//学号
    	int age;//年龄
    	char group;//所在学习小组
    	float score;//成绩
    };stduent1,student2;
    

    将变量放在结构体定义的最后即可。
    如果只需要 student1、student2 两个变量,后面不需要再使用结构体名定义其他变量,那么在定义时也可以不给出结构体名,如下所示:

    struct{//无需写出student
    	char *name;//姓名
    	int num;//学号
    	int age;//年龄
    	char group;//所在学习小组
    	float score;//成绩
    };stduent1,student2;
    

    这样写的优点就是简单,缺点就是由于没有结构体名,后面无法用该结构定义新的变量。

    理论上讲结构体的各个成员在内存中是连续存储的,和数组非常类似,例如上面的结构体变量 student1、student2 的内存分布如下图所示,共占用 4+4+4+1+4 = 17 个字节。
    Alt
    但是在编译器的具体实现中,各个成员之间可能会存在缝隙,对于 student1、student2,成员变量 group 和 score 之间就存在 3 个字节的空白填充(见下图)。这样算来,student1、student2 其实占用了 17 + 3 = 20 个字节。Alt

    一般形式

    第一种: 只有结构体定义

    struct stuff{  
            char job[20];  
            int age;  
            float height;  
    };  
    struct stuff Huqinwei = {"manager",30,185};  //声明
    
    Huqinwei.job[0] = 'M';  
    Huqinwei.job[1] = 'a';  
    Huqinwei.age = 27;  
    Huqinwei.height = 185;  
    

    第二种: 附加变量初始化的结构体定义

    //直接带变量名Huqinwei  
    struct stuff{  
            char job[20];  
            int age;  
            float height;  
    }Huqinwei= {"manager",30,185};
    

    其实这就相当于:

    //直接带变量名Huqinwei  
    struct stuff{  
            char job[20];  
            int age;  
            float height;  
    };  
    struct stuff Huqinwei;  
    

    第三种: 如果该结构体你只用一个变量Huqinwei,而不再需要用

    struct stuff yourname;  
    

    成员的获取和赋值

    获取结构体成员的一般格式为:

    结构体变量名.成员名;
    

    通过这种方式可以获取成员的值也可以给成员赋值:

    #include<stdio.h>
    int main(){
    struct student{
    	char *name;//姓名
    	int num;//学号
    	int age;//年龄
    	char group;//所在学习小组
    	float score;//成绩
    };stduent1;
    //给结构体成员赋值
    	student1.name = "Tom";
    	student1.num = 12;
    	student1.age = 18;
    	student1.group = 'A';
    	student1.score = 136.5;
    //读取结构体成员的值
    	printf("%s的学号是%d,年龄是%d,在%c组,今年的成绩是%.1f!\n", student1.name, student1.num, student1.age, student1.group, student1.score);
    	return 0;
    }
    运行结果为:
    	Tom的学号是12,年龄是18,在A组,今年的成绩是136.5

    除了可以对成员进行逐一赋值,也可以在定义时整体赋值,例如:

    struct student{
    	char *name; //姓名
    	int num; //学号
    	int age; //年龄
    	char group; //所在小组
    	float score; //成绩
    } student1, student2 = { "Tom", 12, 18, 'A', 136.5 };
    

    不过整体赋值仅限于定义结构体变量的时候,在使用过程中只能对成员逐一赋值。
    需要注意的是,结构体是一种自定义的数据类型,是创建变量的模板,不占用内存空间;结构体变量才包含了实实在在的数据,需要内存空间来存储

    指针和数组

    结构体成员变量的访问除了可以借助符号".",还可以用"->"访问。

    struct stuff *ref = &Huqinwei;  
    ref->age = 100;  
    printf("age is:%d\n",Huqinwei.age);  
    

    指针也是一样的

    struct stuff *ptr;  
    ptr->age = 200;  
    printf("age is:%d\n",Huqinwei.age);  
    

    结构体也不能免俗,必须有数组

    struct test{  
            int a[3];  
            int b;  
    };  
    //对于数组和变量同时存在的情况,有如下定义方法:  
            struct test student[3] = {{{66,77,55},0},  
                                     {{44,65,33},0},  
                                     {{46,99,77},0}};  
    //特别的,可以简化成:  
            struct test student[3] = {{66,77,55,0},  
                                     {44,65,33,0},  
                                     {46,99,77,0}};  
    

    数组成员赋值

    typedef struct 
    {
        char key[15];  //结点的关键字
        char name[20];
        int age;
    }student ;    //定义结点类型,可定义为简单类型,也可定义为结构
    
    strcpy(student.name,"sss");
    

    结构体嵌套

    //对于“一锤子买卖”,其中A、B可删,不过最好带着  
    struct A{   
            struct B{  
                 int c;  
            }  b;  
    } a;  
    //使用如下方式访问:  
    a.b.c = 10;   
    

    特别的,可以一边定义结构体B,一边就使用上:

    struct A{  
            struct B{  
                    int c;  
            }b;  
            struct B sb;  
    }a;  
    

    使用方法与测试:

            a.b.c = 11;  
            printf("%d\n",a.b.c);  
            a.b.c = 22;  
            printf("%d\n",a.sb.c);  
    		//结果无误。   
    

    结构体与函数

    关于传参,首先:

    void func(int);  
    func(a.b.c); 
    

    把结构体中的int成员变量当做和普通int变量一样的东西来使用,是不用脑子就想到的一种方法。
    另外两种就是传递副本和指针了 :

    //struct A定义同上  
    //设立了两个函数,分别传递struct A结构体和其指针。  
    void func1(struct A a){  
            printf("%d\n",a.b.c);  
    }  
    void func2(struct A* a){  
            printf("%d\n",a->b.c);  
    }  
    main(){  
            a.b.c = 112;  
            struct A * pa;  
            pa = &a;  
            func1(a);  
            func2(&a);  
            func2(pa);  
    }  
    

    结构体typedef struct详解

    typedef为C语言的关键字,作用是为一种数据类型定义一个新名字。这里的数据类型包括内部数据类型(int,char等)自定义的数据类型(struct等)。

    在编程中使用typedef目的一般有两个,一个是给变量一个易记且意义明确的新名字,另一个是简化一些比较复杂的类型声明

    1、typedef的最简单使用

    typedef long byte_4;
    

    作用:给已知数据类型long起个新名字,叫byte_4。
    2、typedef与结构结合使用

    typedef struct tagMyStruct
    { 
     int iNum;
     long lLength;
    } MyStruct;
    

    这语句实际上完成两个操作:
    1) 定义一个新的结构类型

    struct tagMyStruct
    { 
     int iNum; 
     long lLength; 
    };
    

    分析:tagMyStruct称为“tag”,即“标签”,实际上是一个临时名字,struct 关键字和tagMyStruct一起,构成了这个结构类型,不论是否有typedef,这个结构都存在。
    2) typedef为这个新的结构起了一个名字,叫MyStruct。

    typedef struct tagMyStruct MyStruct;
    

    一般形式

    形式一

    我们使用:

    typedef struct student{
    	int age;
    	char s;
    } Stu
    

    在定义结构体变量的时候,可以使用

    Stu student1;
    

    也可以省略掉student:

    typedef struct{
    	int age;
    	char s;
    } Stu
    

    在定义结构体变量的时候,依然可以使用

    Stu student1;
    

    形式二

     typedef struct  
       {
        int num;
        int age;
       }stu1,stu2,stu3;
    

    相当于

     typedef struct  
       {
        int num;
        int age;
       }stu1;
     typedef stu1 stu2;
     typedef stu1 stu3;
    

    typedef struct的用法实践

    第一种情况中,Victor 将来可以作为一个变量类型来使用 ,就像 int一样去定义变量,vicptr则是指向Victor类的指针类型,在第二种情况中,我们并不能直接使用 Vic 来定义变量,但是在用stu定义的变量,最终的类型却是Vic。第三种情况中,victor3 是个指针类型的变量,指针必须要给定指向的变量,如victor1,否则程序就会报错,通过修改指针victor3,我们成功修改了victor1的值。

    /* 1 */
    typedef struct {
        int age;
        int weight;
    }Victor, *vicptr;
    /* 2 */
    typedef struct Vic {
        int age;
        int weight;
    }stu;
    
    void main(){
        Victor victor1;
        victor1.age = 22;
        victor1.weight = 69;
        stu victor2;
        victor2.age = 24;
        victor2.weight = 70;
    
        vicptr victor3;
        victor3 = &victor1;
        victor3->age = 25;
        victor3->weight = 71;
    }
    

    运行结果如下:
    Alt

    用途1

    定义一种类型的别名,而不只是简单的宏替换。可以用作同时声明指针型的多个对象。
    比如:

    char* pa, pb; // 这多数不符合我们的意图,它只声明了一个指向字符变量的指针和一个字符变量;
    

    以下则可行:

    typedef char* PCHAR; // 一般用大写
    PCHAR pa, pb; // 可行,同时声明了两个指向字符变量的指针
    

    虽然:

    char *pa, *pb;
    

    也可行,但相对来说没有用typedef的形式直观,尤其在需要大量指针的地方,typedef的方式更省事。

    用途2

    以前的代码中,声明struct新对象时,必须要带上struct,即形式为:*struct 结构名 对象名 ,如:

    struct tagPOINT1
    	{
    		int x;
    		int y;
    	};
    struct tagPOINT1 p1;
    

    而在C++中,则可以直接写:结构名 对象名,即:

    tagPOINT1 p1;
    

    经常多写一个struct太麻烦了,于是就发明了:

    typedef struct tagPOINT
    {
    int x;
    int y;
    }POINT;
    
    POINT p1; // 这样就比原来的方式少写了一个struct,比较省事,尤其在大量使用的时候
    

    定义结构体数组类型

    规范

    typedef int arrs[5];
    typedef arrs * p_arr5;
    typedef p_arr5 arrp10[10];
        arr5 togs;       // togs是具有5个元素的int数组
        p_arr5 p2;      // p2是一个指针,指向具有元素的数组
        arrp10  ap;    // ap是具有十个元素的指针数组,每个指针指向具有5个元素的int数组
    

    例子

    typedef struct VertexNode
    {
    	char data;
    	int weitht;
    	struct EdgeNode * firstEdge;
    }VertexNode,AdjList[MAX_VERTEX];
    

    这里AdjList就是结构体数组类型

    AdjList adjlist;
    

    等价于

    struct VertexNode adjlist[MAX_VERTEX];
    

    同样情况

    typedef int arr[5];
    arr a;//就定义了一个有5个int型变量的数组a。
    

    定义结构体指针

    规范

    typedef  struct ANSWER_HEADER
    {
    	u8 u8Type;
    	u8 u8Code;
    	u32 u32TimeStamp;
    	struct ANSWER_HEADER *pNext;
    }ANSWER_HEADER_T, *PANSWER_HEADER_T;
    

    ANSWER_HEADER为结构名,这个名字主要是为了在结构体中包含自己为成员变量的时候有用
    ANSWER_HEADER_T为struct ANSWER_HEADER的别名
    PANSWER_HEADER_T为struct ANSWER_HEADER*的别名
    上面的定义方式等价于:

    struct ANSWER_HEADER
    {
    	u8 u8Type;
    	u8 u8Code;
    	u32 u32TimeStamp;
    	struct ANSWER_HEADER *pNext;
    };
    typedef  struct ANSWER_HEADER ANSWER_HEADER_T;
    typedef  struct ANSWER_HEADER *PANSWER_HEADER_T;
    

    定义实现

    定义一个名为TreeNode的结构体,和指向该结构体类型的指针PtrToTreeNode (不使用typedef)

     struct TreeNode
    {
            int Element;
            struct TreeNode* LeftChild;
            struct TreeNode* RightChild;
    };
    struct TreeNode *PtrToTreeNode; //定义指针
    

    使用typedef关键字用一个单词Node代替struct TreeNode,并定于指向该结构体类型的指针PtrToTreeNode:

    struct TreeNode
    {
            int Element;
            struct TreeNode* LeftChild;
            struct TreeNode* RightChild;
    };
    typedef struct TreeNode Node;   //用Node代替struct TreeNode
    Node *PtrToTreeNode;            //定义指针
    

    将结构体的定义和typedef连在一起写,再次缩短代码:

    typedef struct TreeNode
    {
            int Element;
            struct TreeNode* LeftChild;
            struct TreeNode* RightChild;
    }Node;                          //定义结构体并用Node代替struct TreeNode
    Node *PtrToTreeNode;            //定义指针
    

    还可以继续缩短代码,直接定义了指向结构体类型的指针,但是这种写法没有为结构体起一个别名。

     typedef struct TreeNode
    {
            int Element;
            struct TreeNode* LeftChild;
            struct TreeNode* RightChild;
    } *PtrToTreeNode;               //直接定义指针
    

    在c和c++中struct和typedef struct的区别

    区别一

    struct _x1 { ...}x1; 
    //定义了类_x1和_x1的对象实例x1
    typedef struct _x2{ ...} x2; 
    //定义了类_x2和_x2的类别名x2
    

    区别二

    	typedef struct {
    			int data;
    			int text;
    	} S1;	//这种方法可以在c或者c++中定义一个S1结构
    	
    	struct S2 {
    			int data;
    			int text;
    	};		// 这种定义方式只能在C++中使用,而如果用在C中,那么编译器会报错
    	
    	struct {
    			int data;
    			int text;
    	} S3;	//这种方法并没有定义一个结构,而是定义了一个s3的结构变量,编译器会为S3内存。
    	
    	void main()
    	{
    			S1 mine1;// OK ,S1 是一个类型
    			S2 mine2;// OK,S2 是一个类型
    			S3 mine3;// ERROR,S3 不是一个类型
    			S1.data = 5;// ERROR,S1 是一个类型
    			S2.data = 5;// ERROR,S2 是一个类型
    			S3.data = 5;// OK,S3是一个变量
    	}
    	//另外,对与在结构中定义结构本身的变量也有几种写法
    	struct S6 {
    			S6* ptr;
    	};
    	// 这种写法只能在C++中使用
    typedef struct {
    			S7* ptr;
    	} S7;
    	// 这是一种在C和C++中都是错误的定义
    

    简单理解:

    struct Student
    	{
    		int a;
    	}stu1;//stu1是一个变量
    typedef struct Student2
    	{
    		int a;
    	}stu2;//stu2是一个结构体类型
    //使用时可以直接访问stu1.a
    //但是stu2则必须先 stu2 s2;
    //然后 s2.a=10;
    

    区别三

    typedef struct tagMyStruct
        { 
         int iNum;
         long lLength;
        } MyStruct;
    

    在C中,这个申明后申请结构变量的方法有两种:
        (1)struct tagMyStruct 变量名
        (2)MyStruct 变量名
     在c++中可以有
        (1)struct tagMyStruct 变量名
        (2)MyStruct 变量名
        (3)tagMyStruct 变量名

    综合实例

    Test1

    #include <iostream> 
    
    using namespace std; 
    
    typedef struct _point{ 
       		int x; 
       		int y; 
    }point; //定义类,给类一个别名 
    struct _hello{ 
       		int x,y; 
    } hello; //同时定义类和对象 
    int main() 
    { 
       point pt1; 
       pt1.x = 2; 
       pt1.y = 5; 
       cout<< "ptpt1.x=" << pt1.x << "pt.y=" <<pt1.y <<endl; 
       //hello pt2; 
       //pt2.x = 8; 
       //pt2.y =10; 
       //cout<<"pt2pt2.x="<< pt2.x <<"pt2.y="<<pt2.y <<endl; 
       //上面的hello pt2;这一行编译将不能通过. 为什么? 
       //因为hello是被定义了的对象实例了. 
       //正确做法如下: 用hello.x和hello.y 
       hello.x = 8; 
       hello.y = 10; 
       cout<< "hellohello.x=" << hello.x << "hello.y=" <<hello.y <<endl; 
       return 0; 
    }
    

    Test2

    //以下student是标识符(标识符是用户编程时使用的名字,对于变量、常量、函数、语句块也有名
    //字;),stu则为变量类型(类比int和char等),pstu相当于(int*)。
    typedef struct student{
       string name;
       int age;
    }stu,*pstu;
    //C++中,ss为结构体类型
    struct ss{
       string name;
       int age;
    };
    int main(){
    //变量类型==struct+标识符
       //使用变量类型+结构变量名
       stu xiaoming;
       //或者使用stuct+标识符+结构变量名
       struct student zhangsan;
       //c++ struct+结构变量名
       ss lisi;
       xiaoming.name="xiaoming";
       xiaoming.age=18;
       zhangsan.name="zhangsan";
       zhangsan.age=17;
       lisi.name="lisi";
    }
    

    Test3 typedef与数组类型、数组指针

    typedef int num[5]; //定义数组类型
    typedef char *num1[5]; //定义指针数组类型
    typedef int(*num2)[5]; //定义指向多维数组的指针类型

    int (*num3)[5]; //定义指向多维数组的指针变量
    测试程序:

    #include <stdio.h>
    #include <stdlib.h>
    
    typedef int num[5]; //定义数组类型
    typedef char *num1[5]; //定义指针数组类型
    typedef int(*num2)[5];  //定义指向多维数组的指针类型
    int (*num3)[5];  //定义指向多维数组的指针变量
    int main() {
       int i, j;
       num a; //等价于 int a[5];
       num *b = NULL;
       b = &a;
       for (i = 0;i < 5;i++)
       	(*b)[i] = i;
       for (i = 0;i < 5;i++)
       	printf("%d\n", (*b)[i]);    //这一段完成了对num 以及num *的测试
       int c[][5] = { {1,2,3,4,5},{6,7,8,9,10} };
       num2 d;
       d = c;
       for (i = 0;i < 2;i++)
       	for (j = 0;j < 5;j++)
       		printf("%d\n", c[i][j]);
       num3 = c;
       for (i = 0;i < 2;i++)
       	for (j = 0;j < 5;j++)
       		printf("%d\n", num3[i][j]); //这一段完成了num2   num3的测试
       num1 e;
       for (i = 0;i < 5;i++) {
       	e[i] = (int *)malloc(sizeof(char)*10);
       	strcpy(e[i], "hello");
       }
       for (i = 0;i < 5;i++)
       	printf("%s\n", e[i]);  //这一段完成了 num1的测试
       system("pause");
    }
    说明:
       a是一个整型数组名,数组含有5个元素,b是num * 类型,只能指向num类型(也就是函数5个整型元素的数组)
       c是一个指针数组,每个元素是一个指针
       d是一个指向二维数组的指针变量,二维数组的列数是5
    

    Test4 结构体指针(二层指针)

    #include <iostream>
    using namespace std;
    typedef struct {
          int x;
          int y;
    }point,*_point; //定义类,给类一个别名
    //验证 typedef point * _point;
    int main()
      {
         _point *hp;
         point pt1;
         pt1.x = 2;
         pt1.y = 5;
         _point p;
         p = &pt1;
         hp = &p;
         cout<<  pt1.x<<" "<<pt1.y <<endl;
         cout<< (**hp).x <<" "<< (**hp).y <<endl;
         return 0;
       }
          
          //运行结果:2	5
          //		2	5
    

    参考链接

    https://blog.csdn.net/m0_37973607/article/details/78900184.
    https://blog.csdn.net/zhanghow/article/details/53463825.
    https://blog.csdn.net/u013273161/article/details/83793122.
    https://blog.csdn.net/weixin_41127779/article/details/82023671.
    https://blog.csdn.net/mpp_king/article/details/70229150.
    https://blog.csdn.net/haiou0/article/details/6877718.
    https://blog.csdn.net/qq_37962204/article/details/78240757.
    https://blog.csdn.net/wzz110011/article/details/78883838.
    https://blog.csdn.net/zyh821351004/article/details/47961967.
    https://blog.csdn.net/u013632190/article/details/47720703.

    展开全文
  • 如何让机器自动针对问题...本文介绍了一种利用人工生命遗传算法自动发现深度学习中神经网络最优结构的方法,实验结果表明,这种方法通过少量计算可以自动发现人类手工设计出较好的神经网络结构相近性能的神经网络。
  • GOP结构

    万次阅读 2020-07-15 08:49:41
    在使用HM编码器我们经常要使用预定义的配置文件,在配置文件里有关于GOP的相关定义,这里定义了GOP的结构和相关参数,GOP结构在视频序列中重复使用。定义中包含GOPSize行,按解码顺序每行一帧,所以Frame1是解码第...
  • 结构化数据 **  结构化数据可以使用关系型数据库来表示...结构化的数据的存储排列是很有规律的,这对查询修改等操作很有帮助。但是,显然,它的扩展性不好(比如,我希望增加一个字段)。 ** 非结构化数据 ...
  • 本文将来讲解一下一种常见的线性数据结构—链表,因为链表和数组一样都是一种线性的数据结构,但是它俩的实现原理是完全不同的,所以在讲解链表之前,我们来回顾一下 数组 结构
  • Protobuf3 系列二 定义复杂的proto文件

    万次阅读 2017-04-25 19:56:18
    定义复杂的对象这是Protobuf3的系列二: 如何在protobuf中定义更复杂的对象proto文件除了定义string, int等基础对象外(protobuf的基础数据结构和Java变量的对应关系见文章末尾),还可以在proto中定义更复杂的对象,...
  • 表达式控制结构输入参数输出参数与Javascript一样,函数可以将参数作为输入; 与JavascriptC不同,它们也可以返回任意数量的参数作为输出。输入参数输入参数的声明方式与变量相同。 作为例外,未使用的参数可以...
  • java数据结构与算法之栈(Stack)设计与实现

    万次阅读 多人点赞 2016-11-28 12:27:43
    【版权申明】转载请注明出处(请尊重原创,博主保留追究权) ... 出自【zejian的博客】 关联文章:java数据结构与算法之顺序表与链表设计与实现分析 ...java数据结构与算法之改良顺序表与双链表类似ArrayListLinkedL
  • 结构型模式——适配器模式

    千次阅读 2018-03-12 16:32:48
    这种类型的设计模式属于结构型模式,它结合了两个独立接口的功能。 这种模式涉及到一个单一的类,该类负责加入独立的或不兼容的接口功能。举个真实的例子,读卡器是作为内存卡笔记本之间的适配器。你将内存卡插入...
  • 【版权申明】未经博主同意,不允许转载!(请尊重原创,博主保留追究权) ... 出自【zejian的博客】 关联文章:java数据结构与算法之顺序表与链表设计与实现...java数据结构与算法之改良顺序表与双链表类似ArrayListL
  • 数据结构

    千次阅读 多人点赞 2017-03-27 22:26:49
    数据结构就是研究数据的逻辑结构和物理结构以及它们之间相互关系,并对这种结构定义相应的运算,而且确保经过这些运算后所得到的新结构仍然是原来的结构类型。 数据:所有能被输入到计算机中,且能被计算机处理的...
  • 面向对象和结构化方法的比较

    千次阅读 2007-09-01 16:44:00
    最近又有客户问起结构化方法(即结构化分析SA Structured Analysis 和结构化设计SD Structured Design)面向对象分析设计(OOAD Object-Oriented Analysis & Design)方法的区别,这是一个很多人谈了很多遍的问题,...
  • 【并发编程】CPU cache结构和缓存一致性(MESI协议)

    万次阅读 多人点赞 2016-01-03 06:29:21
    一、cache cpu cache已经发展到了三级缓存结构,基本上现在买的个人电脑都是L3结构。 1. cache的意义 ...所以cache的出现,是为了缓解CPU内存之间速度的不匹配问题(结构:cpu -> cache -> memory...
  • 什么是敏捷性能合弄结构(APH)?

    千次阅读 2019-02-15 10:28:44
    敏捷性能合弄结构(APH)是由美国AgileCxO研究院出品的全球范围首个专门用来评价企业敏捷性能的模型。该方法分析、提取了当前流行的各种敏捷方法、精益方法的先进理念与实践。它可以帮助企业评价当前的敏捷能力等级...
  • 数据结构资料

    千次阅读 2012-10-23 16:17:09
    第一课:数据结构的基本概念术语 第二课:抽象数据类型的表示与实现 第三课:算法及算法设计要求 第四课:算法效率的度量存储空间需求 第五课:线性表的类型定义 第六课:线性表的顺序表示实现 第七课:...
  • 学数据结构,是不是一定要先学离散数学

    万次阅读 多人点赞 2016-01-02 08:31:01
     学数据结构之前是不是一定要学离散数学【答复】  你问了一个非常好的问题,不少初学计算机的同学该会产生这个疑问(大多数有疑问不提而已,这不是好习惯),更深层地,这里可能包含着我们的学习路线的问题,尤其...
  • 软件体系结构风格整理

    万次阅读 多人点赞 2019-01-06 15:17:36
    什么是软件体系结构...这时候为了明确这个概念,通过数学集合的方式来定义风格,如此一来,我们更能认清风格的内部结构及本质。 同样,针对某种软件应用场景,应该选择与其相适应的构件,以及合理安排构件间的关...
  • 指针与结构

    千次阅读 2013-10-08 12:21:38
    指针与结构  ——理解C++数据结构的基础 指针不仅可以指向变量、数组、函数,还可以和结构(structure)联系起来,这使得...深入地理解结构会对你理解C++的面向对象有很大帮助,并且会让在你学习数据结构时有一份
  • C/SB/S结构概念

    万次阅读 2016-05-30 09:56:38
    C/SB/S结构概念 1.什么是C/S结构 C/S (Client/Server)结构,即客户机服务器结构。它是软件系统体系结构,通过它可以充分利用两端硬件环境的优势,将任务合理分配到Client端Server端来实现,降低了系统的...
  • 最全的数据结构归纳总结

    千次阅读 多人点赞 2018-12-17 22:05:58
    数据结构就是研究数据的逻辑结构和物理结构以及它们之间相互关系,并对这种结构定义相应的运算,而且确保经过这些运算后所得到的新结构仍然是原来的结构类型。 数据:所有能被输入到计算机中,且能被计算机...
  • 硬盘由一个或几个表面镀有磁性物质的金属或玻璃等物质盘片以及盘片两面所安装的磁头相应的控制电路组成(图1),其中盘片磁头密封在无尘的金属壳中。硬 盘工作,盘片以设计转速高速旋转,设置在盘片表面的磁头则...
  • 数据结构思维 第一章 接口

    万次阅读 2017-09-02 15:57:19
    数据结构:从 Java 集合框架(JCF)中的结构开始,你将学习如何使用列表映射等数据结构,你将看到它们的工作原理。 算法分析:我提供了技术,来分析代码以及预测运行速度需要多少空间(内存)。 信息检索:为了...
  • 八叉树十六叉树结构

    千次阅读 2013-11-24 13:17:37
    (1)三维四维数据结构的提出。前面介绍的数据结构都是二维的,然而在有些信息系统中,需要有真三维的空间数据结构。例如矿山开采中的地下资源埋藏采矿巷道的空间分布,如果用二维的坐标体系就根本无法很好表达。...
  • Protobuf 3 定义复杂的proto文件

    千次阅读 2019-04-23 09:49:42
    除了定义string, int等基础对象外(protobuf的基础数据结构和Java变量的对应关系见文章末尾),还可以在proto中定义更复杂的对象,详细如下: 定义List列表: 值可以是普通变量,也可以复杂对象。 message Complex...
  • 分析PNG图像结构

    万次阅读 多人点赞 2012-12-13 13:45:04
    PNG文件存储结构的格式可以在http://www.w3.org/TR/REC-png.htm上找到定义。 BMP文件总体上由两部分组成,分别是PNG文件标志数据块(chunks),如表5-8所示。其中数据块分为两类:关键数据块(critical chunk)...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 123,060
精华内容 49,224
关键字:

一般将来时定义和结构