大家好,又见面了,我是你们的朋友全栈君。
本文代码实现基本按照《数据结构》课本目录顺序,外加大量的复杂算法实现,一篇文章足够。能换你一个收藏了吧?
当然如果落下什么了欢迎大家评论指出
目录
问题三:怎么用对称轴向两边扩的方法找到偶回文?(容易操作的)
那么请问,加进去的符号,有什么要求么?是不是必须在原字符中没出现过?请思考
后缀树:后缀树,就是把一串字符的所有后缀保存并且压缩的字典树。
相对于字典树来说,后缀树并不是针对大量字符串的,而是针对一个或几个字符串来解决问题。比如字符串的回文子串,两个字符串的最长公共子串等等。
后缀数组:就是把某个字符串的所有后缀按照字典序排序后的数组。(数组中保存起始位置就好了,结束位置一定是最后)
输入某二叉树的后序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字
输入某二叉树的后序遍历和先序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字
https://blog.csdn.net/hebtu666/article/details/84322113
并查集入门三连:HDU1213 POJ1611 POJ2236
Abstract Self-Balancing Binary Search Tree
顺序存储线性表实现
在计算机中用一组地址连续的存储单元依次存储线性表的各个数据元素,称作线性表的顺序存储结构。
顺序存储结构的主要优点是节省存储空间,因为分配给数据的存储单元全用存放结点的数据(不考虑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*R + 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.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 |
|
中序遍历
首先中序遍历左(右)子树,再访问根,最后中序遍历右(左)子树,C语言代码如下
1 2 3 4 5 6 7 8 |
|
后序遍历
首先后序遍历左(右)子树,再后序遍历右(左)子树,最后访问根,C语言代码如下
1 2 3 4 5 6 7 |
|
层次遍历
即按照层次访问,通常用队列来做。访问根,访问子女,再访问子女的子女(越往后的层次越低)(两个子女的级别相同)
输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{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个项目,想要获得其中最大的收益,并且一次只能做一个项目。这是一个贪心策略的问题,应该在能做的项目中选择收益最大的。
按照花费的多少放到一个小根堆里面,然后要是小根堆里面的头节点的花费少于给定资金,就将头节点一个个取出来,放到按照收益的大根堆里面。然后做大根堆顶的项目即可。
并查集实现
并查集是什么东西?
它是用来管理元素分组情况的一种数据结构。
他可以高效进行两个操作:
- 查询a,b是否在同一组
- 合并a和b所在的组
萌新可能不知所云,这个结构到底有什么用?
经分析,并查集效率之高超乎想象,对n个元素的并查集进行一次操作的复杂度低于O(logn)
我们先说并查集是如何实现的:
也是使用树形结构,但不是二叉树。
每个元素就是一个结点,每组都是一个树。
无需关注它的形状,或哪个节点具体在哪个位置。
初始化:
我们现在有n个结点,也就是n个元素。
合并:
然后我们就可以合并了,合并方法就是把一个根放到另一颗树的下面,也就是整棵树作为人家的一个子树。
查询:
查询两个结点是否是同一组,需要知道这两个结点是不是在一棵树上,让他们分别沿着树向根找,如果两个元素最后走到一个根,他们就在一组。
当然,树形结构都存在退化的缺点,对于每种结构,我们都有自己的优化方法,下面我们说明如何避免退化。
- 记录每一棵树的高度,合并操作时,高度小的变为高度大的子树即可。
- 路径压缩:对于一个节点,只要走到了根节点,就不必再在很深的地方,直接改为连着根即可。进一步优化:其实每一个经过的节点都可以直接连根。
这样查询的时候就能很快地知道根是谁了。
下面上代码实现:
和很多树结构一样,我们没必要真的模拟出来,数组中即可。
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域而不是其他“无用”的域,它可以很方便地实现动态顺序统计中的select和rank操作。
对于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树的策略和一些我没写的基本操作。大家可以自己操作,直观感受一下。
跳表介绍和实现
想慢慢的给大家自然的引入跳表。
想想,我们
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");
}
发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/135067.html原文链接:https://javaforall.cn
【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛
【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...