目录
前言
该系列博客旨在记录我的刷题心得和一些解题技巧,题目全部来源于力扣,一些技巧和方法参考过力扣上的题解和labuladong大佬的文章。所有代码使用C语言,一些辅助函数(比如ADT的基本操作集)的具体实现在此省略,但会保留最核心的辅助函数的代码。虽然说这些内容主要是写给我自己看的,但也欢迎大家发表自己新颖的解法和不一样的观点。
一、栈
1.核心思想
我们都知道栈通常被称为是后进先出(last in first out)表,简称 LIFO 表。栈是一种比较简单的数据结构,但做题时很难想到用栈去解决。我认为栈提供一种快速取得之前访问的数据的方法,而且这些数据是有序的(最晚访问的数据在栈顶,最早访问的在栈底),这是栈的最核心的思想。
2.例题分析
例一 20. 有效的括号
对所给字符串从左开始遍历,遇到右括号时,在左边找到离该括号最近的未配对的左括号,再判断二者是否匹配。因为是从左开始遍历的,所以最近的左括号就是最晚访问的,因此可以用一个栈,把未配对的左括号入栈,每次遇到右括号弹出栈顶元素即可
char leftCh(char c){
if (c == '}') return '{';
if (c == ')') return '(';
return '[';
}
bool isValid(char * s){
char stack[10001];
int topOfStack=-1;
while(*s){
if(*s=='('||*s=='['||*s=='{'){
stack[++topOfStack]=*s;
}else{
if(topOfStack!=-1&&leftCh(*s)==stack[topOfStack])
topOfStack--;
else
return false;
}
s++;
}
return topOfStack==-1;
例二 921. 使括号有效的最少添加
大体思路跟上题一样,声明一个cnt变量记录需要添加的括号数,当左括号不足,即配对时栈内无元素时,cnt递增;当右括号不足,即循环退出时栈内有元素时,cnt递增
int minAddToMakeValid(char * s){
int top=-1;
int cnt=0;
while(*s){
if(*s=='('){
top++;
}else{
if(top!=-1)
top--;
else
cnt++;
}
s++;
}
return cnt+top+1;
}
例三 496. 下一个更大元素 I
本题是单调栈的一个应用。什么是单调栈详见链接。我们可以从左开始遍历,把nums2每个元素正着入栈,弹出比他小的元素直到遇到更大的元素,弹出元素的下一更大元素就是要入栈的元素,这样的栈中元素自顶向下是递增的。当然也可以从右开始遍历,把每个元素倒着入栈,弹出比他小的元素直到遇到更大的元素,他遇到的元素就是下一更大元素。最后用哈希表建立一个数组元素到下一更大元素的映射。
//从左遍历
int* nextGreaterElement(int* nums1, int nums1Size, int* nums2, int nums2Size, int* returnSize){
int map[10001]={0};
int stack[10001];
int top=-1;
for(int i=0;i<nums2Size;i++){
while(top!=-1&&stack[top]<nums2[i]){
map[stack[top--]]=nums2[i];
}
stack[++top]=nums2[i];
}
for(int i=0;i<=top;i++){
map[stack[i]]=-1;
}
*returnSize=nums1Size;
int *ans=malloc(sizeof(int)*nums1Size);
for(int i=0;i<nums1Size;i++){
ans[i]=map[nums1[i]];
}
return ans;
}
//从右遍历
int* nextGreaterElement(int* nums1, int nums1Size, int* nums2, int nums2Size, int* returnSize){
int map[10001]={0};
int stack[10001];
int top=-1;
for(int i=nums2Size-1;i>=0;i--){
while(top!=-1&&stack[top]<nums2[i]){
top--;
}
if(top==-1){
map[nums2[i]]=-1;
}else{
map[nums2[i]]=stack[top];
}
stack[++top]=nums2[i];
}
*returnSize=nums1Size;
int *ans=malloc(sizeof(int)*nums1Size);
for(int i=0;i<nums1Size;i++){
ans[i]=map[nums1[i]];
}
return ans;
}
例四 503.下一个更大元素 II
处理循环数组的常用技巧是把数组长度翻倍,为了实现此想法最简单的就是构造一个两倍长度的数组,但其实可以不用构造,用取模的方法即可实现。
int* nextGreaterElements(int* nums, int numsSize, int* returnSize){
int stack[10001];
int top=-1;
*returnSize=numsSize;
int *ans=malloc(sizeof(int)*numsSize);
for(int i=2*numsSize-1;i>=0;i--){ //从右遍历
while(top!=-1&&stack[top]<=nums[i%numsSize]){
top--;
}
if(top==-1){
ans[i%numsSize]=-1;
}else{
ans[i%numsSize]=stack[top];
}
stack[++top]=nums[i%numsSize];
}
return ans;
}
例五 316. 去除重复字母
先来考虑如何在不排序的情况下对字符串进行去重,我们可以构建一个res的字符串数组,同时用一个哈希表构建字符到该字符是否在res数组中的映射。当遍历原字符串时,把不在res数组中的字符copy到res中即可。
这样做对于本题而言去重的目的是达到了,但字典序最小,即单调的要求未实现,想到单调栈,但又有一个问题,如果一个字符在整个字符串中只出现了1次,然后它又被pop出去了,那么res中该字符将永远不会出现,这显然不合理。那么何时pop栈中的字符呢?一是要大于被push进来的字符,二是之后该字符还有机会被push进来。所以可以再用一个哈希表构建字符到未进栈的该字符个数的映射,当个数为0时就不能pop了。
char * removeDuplicateLetters(char * s){
int map[256]={0}; //未进栈的字符个数
int instack[256]={0}; //是否在栈中
char stack[10000];
int top=-1;
for(int i=0;i<strlen(s);i++){
map[s[i]]++;
}
while(*s){
map[*s]--;
if(instack[*s]){
s++;
continue;
}
while(top!=-1&&*s<stack[top]&&map[stack[top]]>0){
instack[stack[top]]=0;
top--;
}
stack[++top]=*s;
instack[stack[top]]=1;
s++;
}
char *res=malloc(sizeof(char)*(top+2));
for(int i=0;i<=top;i++){
res[i]=stack[i];
}
res[top+1]='\0';
return res;
}
二、队列
1.核心思想
队列是一种具有「先进入队列的元素一定先出队列」性质的表。它相当于滑动窗口,用于操作一个区间内的元素。
2.例题分析
例一 239. 滑动窗口最大值
很容易想到用队列来模拟这个窗口,当一个数进入窗口时,如果它比前面的数大,则把前面的数从队尾弹出(跟普通队列不太一样),再入队。之所以可以这样操作是因为被弹出的元素不可能成为最大元素,而且保证队头元素就是最大的。这就是单调队列。
int* maxSlidingWindow(int* nums, int numsSize, int k, int* returnSize){
int *ans=malloc(sizeof(int)*(numsSize));
int cnt=0;
int queue[numsSize];
int front=0,rear=0;
queue[0]=nums[0];
for(int i=1;i<k;i++){
while(rear!=-1&&queue[rear]<nums[i]){ //弹出小的元素
rear--;
}
queue[++rear]=nums[i];
}
int left=0,right=k;
while(right<numsSize){
ans[cnt++]=queue[front];
while(rear!=front-1&&queue[rear]<nums[right]){
rear--;
}
queue[++rear]=nums[right];
right++;
if(queue[front]==nums[left]){ //如果被移出窗口的元素恰为最大的,则需要更新
front++;
}
left++;
}
ans[cnt++]=queue[front];
*returnSize=cnt;
return ans;
}
三、堆
一、核心思想
“堆”这种数据结构主要用于快速取得一个数的集合中的最大数或最小数。具体底层实现不再赘述,这里讲讲“堆”的几个具体应用。
二、例题分析
例一 703. 数据流中的第 K 大元素
构建一个容量为K小顶堆,把所有元素插入这个堆中,当堆中元素容量达到K时,再插入元素时要先删去堆顶元素,再插入。最后堆顶的元素即为第K大的元素。
typedef struct {
Heap H;
int K;
} KthLargest;
KthLargest* kthLargestCreate(int k, int* nums, int numsSize) {
KthLargest *obj=malloc(sizeof(KthLargest));
obj->H=CreatHeap(k+1);
obj->K=k;
for(int i=0;i<numsSize;i++){
Insert(obj->H,nums[i]);
if(obj->H->Size>k)
Delete(obj->H);
}
return obj;
}
int kthLargestAdd(KthLargest* obj, int val) {
Insert(obj->H,val);
if(obj->H->Size>obj->K)
Delete(obj->H);
return obj->H->Data[1];
}
例二 295. 数据流的中位数
堆的另一应用是计算中位数,使用对顶堆,用小顶堆维护大值,大顶堆维护小值,每次插入后还要要对两个堆进行维护,具体代码看以上链接。
四、数据结构设计
一、核心思想
想做好该类题型我觉得必须对每种数据结构的特性非常熟悉。比如想让数据有序,或者以O(1)的时间插入、删除数据可以用链表,想快速访问某类数据可以用哈希表、数组。而且这类题码量一般很大(C语言不用库函数的话是真的难受),此时采用“自顶向下”的编程方式可以减少出错。
二、例题分析
例一 155. 最小栈
本题设计一个额外的minstack栈来记录每个元素下面最小的元素是多少,每个元素进栈时,若小于等于minstack栈顶元素,则直接进minstack栈,否则将栈顶元素再复制一份进栈(其实可以把minstack设计成自顶向下递增单调栈),每次pop同时维护两个栈。
//单调栈优化版
typedef struct {
int *stack;
int *minstack;
int top;
int mintop;
} MinStack;
MinStack* minStackCreate() {
MinStack *S=malloc(sizeof(MinStack));
S->stack=malloc(sizeof(int)*30000);
S->minstack=malloc(sizeof(int)*30000);
S->top=-1;
S->mintop=-1;
return S;
}
void minStackPush(MinStack* obj, int val) {
obj->stack[++obj->top]=val;
if(obj->mintop==-1||val<=obj->minstack[obj->mintop]){ //注意可能有重复的最小值,都要进栈
obj->minstack[++obj->mintop]=val;
}
}
void minStackPop(MinStack* obj) {
if(obj->stack[obj->top]==obj->minstack[obj->mintop]){
obj->mintop--;
}
obj->top--;
}
int minStackTop(MinStack* obj) {
return obj->stack[obj->top];
}
int minStackGetMin(MinStack* obj) {
return obj->minstack[obj->mintop];
}
例二 146. LRU缓存
本题涉及的数据结构有哈希表和双向链表,哈希表用于把key映射到双向链表的某个结点,双向链表保证缓存数据的有序性,而且方便删除结点(单向链表只能通过遍历找到要删结点的前一个结点来进行删除)。详细讲解可以参考labuladong公众号。
typedef struct node{
int k;
int val;
struct node *pre;
struct node *next;
}* Node; //双向链表结点
typedef struct hashnode{
Node node;
struct hashnode *next;
}* HNode; //哈希表结点,本身为链表,具有next分量,哈希表结点内要存储双向链表的结点,具有node分量
typedef struct DoubleNode{
Node head;
Node tail;
int size;
}*DoubleList; //双向链表
typedef struct hash{
int p;
HNode *hashmap;
}* HashTable; //拉链法实现哈希表
DoubleList DoubleListCreate(){
DoubleList obj=malloc(sizeof(struct DoubleNode));
obj->head=malloc(sizeof(struct node));
obj->tail=malloc(sizeof(struct node));
obj->head->next=obj->tail;
obj->tail->pre=obj->head;
obj->size=0;
return obj;
}
void addLast(DoubleList obj, Node x){
x->pre=obj->tail->pre;
x->next=obj->tail;
obj->tail->pre->next=x;
obj->tail->pre=x;
obj->size++;
}
void removeNode(DoubleList obj, Node x){
x->pre->next=x->next;
x->next->pre=x->pre;
obj->size--;
}
Node removeFirst(DoubleList obj){
if(obj->head->next==obj->tail)
return NULL;
Node tmp=obj->head->next;
obj->head->next=tmp->next;
tmp->next->pre=obj->head;
obj->size--;
return tmp;
}
int IsPrime(int X){
for(int i=2;i<=(int)sqrt(X);i++){
if(X%i==0)
return 0;
}
return 1;
}
int NextPrime(int X){
while(!IsPrime(X))
X++;
return X;
}
HashTable mapCreate(int size){
HashTable obj=malloc(sizeof(struct hash));
obj->hashmap=malloc(sizeof(HNode)*size);
obj->p=size;
for(int i=0;i<size;i++){
obj->hashmap[i]=malloc(sizeof(struct hashnode));
obj->hashmap[i]->next=NULL;
}
return obj;
}
int h(int k,int p){
return k%p;
}
HNode Find(HashTable objhash, int key){ //根据双向链表结点的key值查找
HNode tmp=objhash->hashmap[h(key,objhash->p)];
tmp=tmp->next;
while(tmp!=NULL&&tmp->node->k!=key)
tmp=tmp->next;
return tmp;
}
void Insert(HashTable objhash, Node key){ //注意此时参数key的类型为Node,因为我们要把双向链表的一个结点插入哈希表中
HNode findedHNode=Find(objhash,key->k);
if(findedHNode==NULL){
HNode newHNode=malloc(sizeof(struct hashnode));
newHNode->node=key;
HNode head=objhash->hashmap[h(key->k,objhash->p)];
newHNode->next=head->next;
head->next=newHNode;
}
}
void Delete(HashTable objhash, Node key){
HNode head=objhash->hashmap[h(key->k,objhash->p)];
while(head->next!=NULL&&head->next->node!=key)
head=head->next;
if(head->next!=NULL){
HNode tmp=head->next;
head->next=tmp->next;
free(tmp);
}
}
typedef struct {
HashTable map;
DoubleList cache;
int cap;
} LRUCache;
LRUCache* lRUCacheCreate(int capacity) {
LRUCache *obj=malloc(sizeof(LRUCache));
obj->map=mapCreate(NextPrime(capacity));
obj->cache=DoubleListCreate();
obj->cap=capacity;
return obj;
}
int lRUCacheGet(LRUCache* obj, int key) {
HNode findedHNode=Find(obj->map,key);
if(findedHNode==NULL)
return -1;
removeNode(obj->cache,findedHNode->node);
addLast(obj->cache,findedHNode->node);
return findedHNode->node->val;
}
void lRUCachePut(LRUCache* obj, int key, int value) {
HNode findedHNode=Find(obj->map,key);
if(findedHNode==NULL){
Node node=malloc(sizeof(struct node));
node->k=key;
node->val=value;
if(obj->cache->size==obj->cap){ //超出容量
Node firstNode=removeFirst(obj->cache); //从链表中删除
addLast(obj->cache,node);
Delete(obj->map,firstNode); //从哈希表中删除
Insert(obj->map,node);
}else{ //未超容量
addLast(obj->cache,node);
Insert(obj->map,node);
}
}else{ //找到
findedHNode->node->val=value;
removeNode(obj->cache,findedHNode->node);
addLast(obj->cache,findedHNode->node);
}
}
void lRUCacheFree(LRUCache* obj) {
free(obj);
}