目录
顺序存储线性表实现
单链表不带头标准c语言实现
单链表不带头压缩c语言实现
约瑟夫环-(数组、循环链表、数学)
线性表表示集合
线性表实现一元多项式操作
链表环问题
移除链表元素
回文链表
链表表示整数,相加
LRU
LFU
合并链表
反转链表
反转链表2
对链表排序
旋转链表
数组实现栈
链表实现栈
数组实现队列
链表实现队列
双栈的实现
栈/队列 互相模拟实现
栈的排序
栈——括号匹配
栈——表达式求值
借汉诺塔理解栈与递归
单调栈
双端单调队列
单调队列优化的背包问题
01背包问题
完全背包问题
多重背包问题
串的定长表示
串的堆分配实现
KMP
一、引子
二、分析总结
三、基本操作
四、原理
五、复杂度分析
Manacher
小问题一:请问,子串和子序列一样么?请思考一下再往下看
小问题二:长度为n的字符串有多少个子串?多少个子序列?
一、分析枚举的效率
二、初步优化
问题三:怎么用对称轴向两边扩的方法找到偶回文?(容易操作的)
那么请问,加进去的符号,有什么要求么?是不是必须在原字符中没出现过?请思考
小结:
三、Manacher原理
假设遍历到位置i,如何操作呢
四、代码及复杂度分析
前缀树
后缀树/后缀数组
后缀树:后缀树,就是把一串字符的所有后缀保存并且压缩的字典树。
相对于字典树来说,后缀树并不是针对大量字符串的,而是针对一个或几个字符串来解决问题。比如字符串的回文子串,两个字符串的最长公共子串等等。
后缀数组:就是把某个字符串的所有后缀按照字典序排序后的数组。(数组中保存起始位置就好了,结束位置一定是最后)
AC自动机
数组缺失
二叉树遍历
前序
中序
后序
进一步思考
二叉树序列化/反序列化
先序中序后序两两结合重建二叉树
先序遍历
中序遍历
后序遍历
层次遍历
输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。
输入某二叉树的后序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字
输入某二叉树的后序遍历和先序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字
先序中序数组推后序数组
二叉树遍历
遍历命名
方法1:我们可以重建整棵树:
https://blog.csdn.net/hebtu666/article/details/84322113
方法2:我们可以不用重建,直接得出:
根据数组建立平衡二叉搜索树
java整体打印二叉树
判断平衡二叉树
判断完全二叉树
判断二叉搜索树
二叉搜索树实现
堆的简单实现
堆应用例题三连
一个数据流中,随时可以取得中位数。
金条
项目最大收益(贪心问题)
并查集实现
并查集入门三连:HDU1213 POJ1611 POJ2236
HDU1213
POJ1611
POJ2236
线段树简单实现
功能:一样的,依旧是查询和改值。
查询[s,t]之间最小的数。修改某个值。
那我们继续说,如何查询。
如何更新?
树状数组实现
最大搜索子树
morris遍历
最小生成树
拓扑排序
最短路
简单迷宫问题
深搜DFS\广搜BFS
皇后问题
一般思路:
优化1:
优化2:
二叉搜索树实现
Abstract Self-Balancing Binary Search Tree
二叉搜索树
概念引入
AVL树
红黑树
size balance tree
伸展树
Treap
最简单的旋转
带子树旋转
代码实现
AVL Tree
前言
二叉搜索树
AVL Tree
旋转
旋转总结
单向右旋平衡处理LL:
单向左旋平衡处理RR:
双向旋转(先左后右)平衡处理LR:
双向旋转(先右后左)平衡处理RL:
深度的记录
单个节点的深度更新
写出旋转代码
总写调整方法
插入完工
删除
直观表现程序
跳表介绍和实现
c语言实现排序和查找所有算法
在计算机中用一组地址连续的存储单元依次存储线性表的各个数据元素,称作线性表的顺序存储结构。
顺序存储结构的主要优点是节省存储空间,因为分配给数据的存储单元全用存放结点的数据(不考虑c/c++语言中数组需指定大小的情况),结点之间的逻辑关系没有占用额外的存储空间。采用这种方法时,可实现对结点的随机存取,即每一个结点对应一个序号,由该序号可以直接计算出来结点的存储地址。但顺序存储方法的主要缺点是不便于修改,对结点的插入、删除运算时,可能要移动一系列的结点。
优点:随机存取表中元素。缺点:插入和删除操作需要移动元素。
线性表中数据元素之间的关系是一对一的关系,即除了第一个和最后一个数据元素之外,其它数据元素都是首尾相接的(注意,这句话只适用大部分线性表,而不是全部。比如,循环链表逻辑层次上也是一种线性表(存储层次上属于链式存储),但是把最后一个数据元素的尾指针指向了首位结点)。
给出两种基本实现:
/*
静态顺序存储线性表的基本实现
*/
#include
#include
#include
#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
#include
#include
#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
printf("%d ",L.elem[i]);
printf("\n");//为了美观
}
void DeleteMin(SqList *L)
{
//表空在Listdelete函数里判断
int i;
int j=0;//最小值下标
ElemType *e;
for(i=0;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);
}
链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。 相比于线性表顺序结构,操作复杂。由于不必须按顺序存储,链表在插入的时候可以达到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
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开始写数据结构,因为自己写的,追求代码量少,和学院ppt不太一样。有错请指出
#include
#include
#include
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
i=(s+n-1)%n;
while(k)
{
for(j=1;j
printf("%d\n",a[i]);//出局
for(j=i+1;j
k--;
}
//模拟结束,最后输出的就是留下的人
}
可以用带头单循环链表来求解:
也是一样的,只是实现不同,给出核心代码:
while(k)
{
for(j=1;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
{
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
{
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
{
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
#include
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全称是Least Recently Used,即最近最久未使用的意思。
LRU算法的设计原则是:如果一个数据在最近一段时间没有被访问到,那么在将来它被访问的可能性也很小。也就是说,当限定的空间已存满数据时,应当把最久没有被访问到的数据淘汰。(这一段是找的,让大家理解一下什么是LRU)。
说一下我们什么时候见到过LRU:其实老师们肯定都给大家举过这么个例子:你在图书馆,你把书架子里的书拿到桌子上。。但是桌子是有限的,你有时候不得不把一些书放回去。这就相当于内存和硬盘。这个例子都说过吧?
LRU就是记录你最长时间没看过的书,就把它放回去。在cache那里见过吧
然后最近在研究redis,又看到了这个LRU,所以就想写一下吧。
题目:设计一个结构,这个结构可以查询K-V,但是容量有限,当存不下的时候就要把用的年代最久远的那个东西扔掉。
其实思路很简单,我们维护一个双向链表即可,get也就是使用了,我们就把把它提到最安全的位置。新来的KV就依次放即可。
我们就先写这个双向链表结构
先写节点结构:
public static class Node
public V value;
public Node
public Node
public Node(V value) {
this.value = value;
}
}
然后写双向链表结构: 我们没必要把链表操作都写了,分析一下,我们只有三个操作:
1、加节点
2、使用了某个节点就把它调到尾,代表优先级最高
3、把优先级最低的移除,也就是去头部
(不会的,翻我之前的链表操作都有写)
public static class NodeDoubleLinkedList
private Node
private Node
public NodeDoubleLinkedList() {
this.head = null;
this.tail = null;
}
public void addNode(Node
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
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
if (this.head == null) {
return null;
}
Node
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
//为了kv or vk都能查
private HashMap
private HashMap
//用来做优先级
private NodeDoubleLinkedList
private int capacity;//容量
public MyCache(int capacity) {
if (capacity < 1) {//你容量连1都不给,捣乱呢
throw new RuntimeException("should be more than 0.");
}
this.keyNodeMap = new HashMap
this.nodeKeyMap = new HashMap
this.nodeList = new NodeDoubleLinkedList
this.capacity = capacity;
}
public V get(K key) {
if (this.keyNodeMap.containsKey(key)) {
Node
this.nodeList.moveNodeToTail(res);//使用过了就放到尾部
return res.value;
}
return null;
}
public void set(K key, V value) {
if (this.keyNodeMap.containsKey(key)) {
Node
node.value = value;//放新v
this.nodeList.moveNodeToTail(node);//我们认为放入旧key也是使用过
} else {
Node
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
K removeKey = this.nodeKeyMap.get(removeNode);
//删除掉两个map中的记录
this.nodeKeyMap.remove(removeNode);
this.keyNodeMap.remove(removeKey);
}
}
请你为 最不经常使用(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
unordered_map
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
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
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;
}
}
反转从位置 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
#include
#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
#include
#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
#include
#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
#include
#include
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
Stack
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
Queue
//压入
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
Stack
while(!stack.isEmpty()){
int cru=stack.pop();
while(!help.isEmpty()&&help.peek()
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
for(int i=0;i
if(s[i]=='(')st.push(')');
else if(s[i]=='[')st.push(']');
else if(s[i]=='{')st.push('}');
else if(st.empty())return false;
else if(st.top()!=s[i])return false;
else st.pop();
}
return st.empty();
}
};
今天把表达式求值给搞定吧。
问题:给你个表达式,有加减乘除和小括号,让算出结果。
我们假定计算式是正确的,并且不会出现除数为0等错误。
py大法好啊,在保证可读性的前提下能压到一共就三十多行代码。
其实能压到不到三十行,但是代码就不好看了。。。。
计算函数:
def getvalue(a, b, op):
if op == "+":return a+b
elif op == "-":return a-b
elif op == "*":return a*b
else:return a/b
出栈一个运算符,两个数值,计算,将结果入data用于之后计算
def process(data, opt):
operator = opt.pop()
num2 = data.pop()
num1 = data.pop()
data.append(getvalue(num1, num2, operator))
比较符号优先级:
乘除运算优先级比加减高。
op1优先级比op2高返回True,否则返回False
def compare(op1, op2):
return op1 in ["*","/"] and op2 in ["+","-"]
主函数:
基本思路:
处理每个数字为一个整数,处理每一项为一个单独的数字,把括号内处理为一个单独的数字。
把式子处理为只有整数、加减的式子再最后计算。
def calculate(s):
data = []#数据栈
opt = []#操作符栈
i = 0 #表达式遍历的索引
while i < len(s):
if s[i].isdigit(): # 数字,入栈data
start = i
while i+1 < len(s) and s[i + 1].isdigit():
i += 1
data.append(int(s[start: i + 1])) # i为最后一个数字字符的位置
elif s[i] == ")": # 右括号,opt出栈,data出栈并计算,结果入data,直到左括号
while opt[-1] != "(":
process(data,opt)#优先级高的一定先弹出
opt.pop() # 出栈的一定是左括号
elif not opt or opt[-1] == "(":opt.append(s[i])#栈空,或栈顶为左括号,入opt
elif s[i]=="(" or compare(s[i],opt[-1]):opt.append(s[i])#左括号或比栈顶优先级高,入
else: #优先级不比栈顶高,opt出栈同时data出栈并计算,计算结果入data
while opt and not compare(s[i], opt[-1]):
if opt[-1] == "(":break #遇到左括号,停止计算
process(data, opt)
opt.append(s[i])
i += 1 #索引后移
while opt:
process(data, opt)
print(data.pop())
我们先说,在一个函数中,调用另一个函数。
首先,要意识到,函数中的代码和平常所写代码一样,也都是要执行完的,只有执行完代码,或者遇到return,才会停止。
那么,我们在函数中调用函数,执行完了,就会重新回到本函数中,继续向下执行,直到结束。
在执行其它函数时,本函数相当于中断了,不执行了。那我们重新回来的时候,要从刚才暂停的地方开始,继续执行,这期间,所有现场信息都要原封不动,就相当于时间暂停了一样,什么都不能改变,这样才能做到程序的准确。
所以,通常,在执行另一个函数之前,电脑会将现场信息压入一个系统栈,为被调用的函数分配存储区,然后开始执行被调函数。执行完毕后,保存计算结果,释放被调函数的空间,按照被调函数里保存的返回地址,返回到原函数。
那什么是递归函数呢?
就是多个函数嵌套调用。不同的是,这些函数是同一个函数,只是参数可能不同,甚至参数也一样,只是存储空间不同。
每一层递归所需信息构成一个栈,每一块内存储着所有实在参数,所有局部变量,上一层的返回地址,等等一切现场信息。每执行完就弹出。
递归函数有着广泛应用,主要适合可以把自身分化成一样的子问题的问题。比如汉诺塔。
汉诺塔:汉诺塔(又称河内塔)问题是源于印度一个古老传说的益智玩具。大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘。
思路:函数(n,a,b,c)含义是把n个盘子从a柱子搬到c柱子的方法
一个盘子,直接搬过去。
多个盘子,我们把n-1个盘子都移动到另一个柱子上,把最大的搬过去然后把剩下的搬过去。
def hanoi(n, a, b, c):
if n == 1:
print(a, '-->', c)
else:
hanoi(n - 1, a, c, b)
print(a, '-->', c)
hanoi(n - 1, b, a, c)
# 调用
hanoi(3, 'A', 'B', 'C')
结果:
A --> C
A --> B
C --> B
A --> C
B --> A
B --> C
A --> C
我们的栈:
第一次:
我们把hanoi(3, 'A', 'B', 'C')存了起来,调用了hanoi(3-1, 'A', 'C', 'B'),现在栈里压入了3, 'A', 'B', 'C',还有函数执行到的位置等现场信息。然后执行hanoi(3-1, 'A', 'C', 'B'),发现要调用hanoi(3-1-1, 'A', 'B', 'C'),我们又把3-1, 'A', 'C', 'B'等信息压入了栈,现在栈是这样的:
栈头
2, 'A', 'C', 'B'
3, 'A', 'B', 'C'
栈尾
然后执行hanoi(3-1-1, 'A', 'B', 'C'),发现n=1了,打印了第一条A --> C,然后释放掉了hanoi(3-1-1, 'A', 'B', 'C')的空间,并通过记录的返址回到了hanoi(3-1, 'A', 'C', 'B'),然后执行打印语句A --> B,然后发现要调用hanoi(3-1-1, 'C', 'A', 'B'),此时栈又成了:
2, 'A', 'C', 'B'
3, 'A', 'B', 'C'
调用hanoi(1, 'A', 'C', 'B')发现可以直接打印,C --> B。
然后我们又回到了2, 'A', 'C', 'B'这里。发现整个函数执行完了,那就弹出吧。这时栈是这样的:
3, 'A', 'B', 'C'
只有这一个。
我们继续执行这个函数的代码,发现
def hanoi(n, a, b, c):
if n == 1:
print(a, '-->', c)
else:
hanoi(n - 1, a, c, b)//执行到了这里
print(a, '-->', c)
hanoi(n - 1, b, a, c)
那我们就继续执行,发现要打印A --> C
然后继续,发现要调用 hanoi(n - 1, b, a, c),那我们继续把现场信息压栈,继续执行就好了。
递归就是把大问题分解成小问题进而求解。
具体执行就是通过系统的栈来实现返回原函数的功能。
转存失败重新上传取消
多色汉诺塔问题:
奇数号圆盘着蓝色,偶数号圆盘着红色,如图所示。现要求将塔座A 上的这一叠圆盘移到塔座B 上,并仍按同样顺序叠置。在移动圆盘时应遵守以下移动规则:
规则(1):每次只能移动1 个圆盘;
规则(2):任何时刻都不允许将较大的圆盘压在较小的圆盘之上;
规则(3):任何时刻都不允许将同色圆盘叠在一起;
其实双色的汉诺塔就是和无色的汉诺塔算法类似,通过推理可知,无色汉诺塔的移动规则在双色汉诺塔这里的移动规则并没有违反。
这里说明第一种就可以了:Hanoi(n-1,A,C,B);
在移动过程中,塔座上的最低圆盘的编号与n-1具有相同奇偶性,塔座b上的最低圆盘的编号与n-1具有不相同的奇偶性,从而塔座上的最低圆盘的编号与n具有相同的奇偶性,塔座上c最低圆盘的编号与n具有不同的奇偶性;
所以把打印操作换成两个打印即可
总:因为递归可能会有重复子问题的出现。
就算写的很好,无重复子问题,也会因为来回调用、返回函数,而速度较慢。所以,有能力的可以改为迭代或动态规划等方法。
通过使用栈这个简单的结构,我们可以巧妙地降低一些问题的时间复杂度。
单调栈性质:
1、若是单调递增栈,则从栈顶到栈底的元素是严格递增的。若是单调递减栈,则从栈顶到栈底的元素是严格递减的。
2、越靠近栈顶的元素越后进栈。(显而易见)
本文介绍单调栈用法
通过一道题来说明。
POJ2559
1. 题目大意:链接
给出一个柱形统计图(histogram), 它的每个项目的宽度是1, 高度和具体问题有关。 现在编程求出在这个柱形图中的最大面积的长方形。
7 2 1 4 5 1 3 3
7表示柱形图有7个数据,分别是 2 1 4 5 1 3 3, 对应的柱形图如下,最后求出来的面积最大的图如右图所示。
做法1:枚举每个起点和终点,矩形面积就是长*最小高度。O(N^3)
做法2:区间最小值优化。O(N^2)
做法3:以每一个下标为中心向两边扩,遇到更短的就停,这样我们可以确定以每一个下标高度为最高的矩形。O(N^2)
单调栈:维护一个单调递增栈,所有元素各进栈和出栈一次即可。每个元素出栈的时候更新最大的矩形面积。
过程:
1)判断当前元素小于栈顶
2)条件满足,就可以更新栈顶元素的最大长度了,并且把栈顶弹出
3)继续执行(1),直到条件不满足。
重要结论:
1)栈顶下面一个元素一定是,栈顶左边第一个比栈顶小的元素
2)当前元素一定是,右边第一个比栈顶小的元素。
为什么呢?
比如这是个栈
,
1)如果右边存在距离更近的比1号小的数,1号早已经弹出了。
2)如果左边有距离更近的比1号小的数,
如果它比2号小,它会把2号弹出,自己成为2号
如果它比2号大,它不会弹出2号,但是它会压栈,变成2号,原来的2号成为3号。
所以不管怎么说,这个逻辑是正确的。
最后放代码并讲解
下面看一道难一些的题
LeetCode 85 Maximal Rectangle
1 0 1 0 0
1 0 1 1 1
1 1 1 1 1
1 0 0 1 0
Return 6.二三行后面那六个1
给定一个由二进制组成的矩阵map,找到仅仅包含1的最大矩形,并返回其面积。
这道题是一行一行的做。对每一行都求出每个元素对应的高度,这个高度就是对应的连续1的长度,然后对每一行都更新一次最大矩形面积。
连续1长度也很好更新,本个元素是0,长度就是0,本个元素是1,那就加上之前的。
具体思路代码中讲解。
import java.util.Stack;
public class MaximalRectangle {
public static int maxRecSize(int[][] map) {
if (map == null || map.length == 0 || map[0].length == 0) {
return 0;
}
int maxArea = 0;
int[] height = new int[map[0].length];
for (int i = 0; i < map.length; i++) {
for (int j = 0; j < map[0].length; j++) {
height[j] = map[i][j] == 0 ? 0 : height[j] + 1;//0长度为0,1长度为前面+1
}
maxArea = Math.max(maxRecFromBottom(height), maxArea);//调用第一题的思想
}
return maxArea;
}
//第一题思路
public static int maxRecFromBottom(int[] height) {
if (height == null || height.length == 0) {
return 0;
}
int maxArea = 0;
Stack
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
{
//不满足单调,尾就弹出
while(s
//直到满足,放入
deq[t++]=i;
//计算窗口最大值
if(i-k+1>=0)b[i-k+1]=a[deq[s];
//判断头过期弹出
if(deq[s]==i-k+1)s++;
}
}
基本入门就到这里。
对于背包问题,经典的背包九讲已经讲的很明白了,本来就不打算写这方面问题了。
但是吧。
我发现,那个最出名的九讲竟然没写队列优化的背包。。。。
那我必须写一下咯嘿嘿,这么好的思想。
我们回顾一下背包问题吧。
题目
有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
{
for(int a=0;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--;
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
#include
//串的定长顺序存储表示
#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
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
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
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
p[i]=S->str[i];
for(i=pos-1;i
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
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->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
{
printf("%c",S.str[i]);
}
printf("\n");
}
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));
}
}