刷题心得02 队列、栈、堆、数据结构设计

目录

前言

一、栈

1.核心思想

2.例题分析

例一        20. 有效的括号

例二        921. 使括号有效的最少添加 

 例三        496. 下一个更大元素 I

 例四        503.下一个更大元素 II

例五        316. 去除重复字母

二、队列

1.核心思想

2.例题分析

 例一        239. 滑动窗口最大值

三、堆

一、核心思想

二、例题分析

例一        703.  数据流中的第 K 大元素

例二        295.  数据流的中位数

四、数据结构设计

一、核心思想

二、例题分析

例一        155. 最小栈

 例二        146. LRU缓存


前言

        该系列博客旨在记录我的刷题心得和一些解题技巧,题目全部来源于力扣,一些技巧和方法参考过力扣上的题解和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);
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

vio_gram

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值