首页 > 编程语言 >数据结构与算法刷题(参考代码随想录结构,C、C++实现)

数据结构与算法刷题(参考代码随想录结构,C、C++实现)

时间:2024-11-16 23:08:25浏览次数:1  
标签:遍历 nums int 元素 随想录 C++ next 指针 刷题

目录

数组

数组理论基础

  • 数组下标从0开始;
  • 数组内存空间地址连续;

二分查找

  • 题目链接:704. 二分查找 - 力扣(LeetCode)
  • 思路:二分思想,关键确定好左右定义的区间意义:左闭右开or左闭右闭?然后确定循环终止条件、边界更新实现;左右双指针
  • 代码实现:
int search(int* nums, int numsSize, int target) {
    // 1、定义二分区间,左闭右开
    int left = 0, right = numsSize;

    // 2、二分搜索,左闭右开 -- 相等没有意义
    while(left < right) {
        int middle = (left + right) / 2;

        int midValue = *(nums + middle);
        // 2.1 在左边
        if(midValue > target) {
            right = middle;
        }
        // 2.2 在右边
        else if(midValue < target) {
            left = middle + 1;
        }
        // 2.3 找到了
        else {
            return middle;
        }
    }

    // 3、没找到
    return -1;

}

移除元素

  • 题目链接:27. 移除元素 - 力扣(LeetCode)
  • 思路: 快慢双指针:快指针在前面走,如果遇到与目标值相同——快指针跳过,慢指针不动;慢指针按次序指向数组中当前需要存放不为val的值的位置。快慢双指针
  • 代码实现:
// 快慢双指针
// 快指针在前面走,如果遇到与目标值相同——快指针跳过,慢指针不动;
// 慢指针按次序指向数组中当前需要存放不为val的值的位置
int removeElement(int* nums, int numsSize, int val) {
    // 1.定义快慢双指针
    int fastP  = 0, slowP = 0;
    // 2.循环所有元素
    while(fastP < numsSize) {
        // 2.1 如果fastP不指向val,赋值同时两个指针自增
        if(*(nums + fastP) != val) {
            *(nums + slowP++) = *(nums + fastP);
        }
        // 2.2 如果是val,只对fastP自增
        fastP++;
    }

    // 3.此时慢指针的位置就能得到与val不同的元素的数量
    return slowP;
}

有序数组的平方

  • 题目链接:977. 有序数组的平方 - 力扣(LeetCode)
  • 思路:
    • 左右双指针,非递减序列中平方的最大值一定在两边;
    • 注意其中 returnSize要更新为结果元素的数量,这也是返回数组(int *)时告知数组长度的方式
    • 动态为数组分配内存:int *ans = (int *)malloc(numsSize * sizeof(int));;
  • 代码实现:
/**
 * Note: The returned array must be malloced, assume caller calls free().
 */
 // 左右双指针:平方最大值一定是从两边到中间
int* sortedSquares(int* nums, int numsSize, int* returnSize) {
    *returnSize = numsSize;
    // 1.定义左右双指针、结果数组
    int left = 0, right = numsSize - 1;
    int *ans = (int *)malloc(numsSize * sizeof(int));
    int index = numsSize - 1;
    // 2.开始循环:循环终止条件--相等为止
    while(left <= right) {
        // 循环中比较左右双指针哪个指向元素的平方最大
        int leftValue = nums[left] * nums[left], rightValue = nums[right] * nums[right];
        // printf("leftValue:%d, rightValue:%d\n", leftValue, rightValue);
        if(leftValue < rightValue) {
            ans[index] = rightValue;
            --right;
        } else {
            ans[index] = leftValue;
            ++left;
        }
        // printf("ans[index] = %d\n", ans[index]);
        index--;
    }

    // 3.返回
    // printf("return ans:%d", ans[numsSize - 1]);
    return ans;
}

长度最小的子数组

// 思路:滑动窗口
// 窗口:左闭右闭,如果小与target,一直移动右指针;否则记录长度,然后移动左指针
int minSubArrayLen(int target, int* nums, int numsSize) {
    // 1.滑动窗口左右指针,左闭右闭
    int left = 0, right = 0;
    int sum = 0;
    int count = numsSize + 1;
    // 2.遍历
    for(;right < numsSize; ++right){
        // 2.1 滑动窗口右移了,计入
        // printf("right = %d\n", right);
        sum += nums[right];
        // printf("sum = %d\n", sum);
        while(sum >= target) {
            // 2.2 只要子序列满足要求,收缩滑动窗口左边界
            count = count < (right - left + 1) ? count : (right - left + 1);
            // printf("==>count = %d\n", count);
            sum -= nums[left++];
            // printf("left = %d\n", left);
        }
    }

    // 3.返回
    return count == numsSize + 1 ? 0 : count;
}

螺旋矩阵Ⅱ

  • 题目链接:59. 螺旋矩阵 II - 力扣(LeetCode)
  • 思路:按照题目要求遍历,注意螺旋遍历过程中保持变量不变——每一行、列填充时统一为左闭右开;
    • 注意returnColumnSizesans的创建:*returnColumnSizes = (int *)malloc(sizeof(int) * n);int **ans = (int**)malloc(sizeof(int*) * n);
    • returnColumnSizes本质上也是一个二维数组,第一个维度是有多少列,第二个维度是每一列包含多少个元素(也就是多少行);
  • 代码实现:
/**
 * Return an array of arrays of size *returnSize.
 * The sizes of the arrays are returned as *returnColumnSizes array.
 * Note: Both returned array and *columnSizes array must be malloced, assume caller calls free().
 */
 // 思路:模拟,注意控制循环时保持变量不变,每一行、列的填充统一为左闭右开
int** generateMatrix(int n, int* returnSize, int** returnColumnSizes) {
    // 1.初始化返回数组的大小
    *returnSize = n;
    *returnColumnSizes = (int *)malloc(sizeof(int) * n);
    // 2.初始化结果:二维数组
    int **ans = (int**)malloc(sizeof(int*) * n);
    // 初始化每一行
    for(int i = 0; i < n; ++i) {
        ans[i] = (int*)malloc(sizeof(int) * n);
        (*returnColumnSizes)[i] = n;
    }

    // 3.初始化循环变量
    // 计数变量(用于赋值)
    int count = 1;
    // 按照n计算螺旋的圈数
    int loop = n / 2;
    // 记录中间位置,如果n为奇数,这里需要单独赋值
    int mid = n / 2;
    // 螺旋的起点
    int startX = 0;
    int startY = 0;
    // 偏移量,用于计算螺旋中每一行、列的终点
    int offset = 1;

    // 4.开始螺旋循环--按照循环圈数
    while(loop != 0) {
        // 4.1 设置起点
        int i = startX;
        int j = startY;

        // 4.2 顶部一行,注意左闭右开
        for(; j < startY + n - offset; j++) {
            ans[i][j] = count++;
        }
        // 4.3 右侧一列
        for(; i < startX + n - offset; i++) {
            ans[i][j] = count++;
        }
        // 4.4 底部一行
        for(; j > startY; j--) {
            ans[i][j] = count++;
        }
        // 4.5 左侧一列
        for(; i > startX; i--) {
            ans[i][j] = count++;
        }

        // 4.6 一轮螺旋结束,更新变量
        // 减少螺旋圈数
        --loop;
        // 更新下一轮螺旋起点
        ++startX;
        ++startY;
        // 更新偏移量offset,一轮螺旋少两个
        offset += 2;
    }

    // 5.判断n是否为奇数,从而进行中间点补充
    if(n % 2 == 1) {
        ans[mid][mid] = count;
    }

    // 6.返回结果
    return ans;
}

总结篇

本章出现的知识点小结:

  • 二分算法;
  • 快慢双指针;
  • 左右双指针;
  • 滑动窗口
  • 按照题目模拟,注意保持变量意义不变。

链表

1.链表理论基础

  • 链表的类型:

    • 单链表;
    • 双链表;

  • 性能分析:

2.移除链表元素

  • 题目链接:203. 移除链表元素 - 力扣(LeetCode)

  • 思路:设置虚拟头节点,删除的时候注意先记录要删除的节点、然后在链表中删除、在内存中删除;

    • 注意:通过指针方式访问其成员变量用 ->
  • 代码实现:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
//  虚拟头节点
struct ListNode* removeElements(struct ListNode* head, int val) {
    // 1.设置虚拟头节点
    typedef struct ListNode ListNode;
    ListNode *dummyHead = (ListNode *)malloc(sizeof(ListNode));
    dummyHead->next = head;
    ListNode *cur = dummyHead;
    // 2.遍历
    while(cur->next != NULL) {
        // 2.1 如果下一个节点需要被删除
        if(cur->next->val == val) {
            // 执行删除--记录下要删除的节点、在链表中删除、在内存中释放
            ListNode *temp = cur->next;
            cur->next = cur->next->next;
            free(temp);
        }
        else {
            // 2.2 不需要删除--更新cur
            cur = cur->next;
        }
    }

    // 3.返回真实头节点
    // 注意:释放虚拟头节点
    head = dummyHead->next;
    free(dummyHead);
    return head;
}

3.设计链表

  • 题目链接:707. 设计链表 - 力扣(LeetCode)

  • 思路:

    • 用c语言创建链表的时候要使用虚拟头节点,不然就没办法在头节点位置做更改;
    • 注意在 index 位置执行插入删除的时候,使用单链表的话找到 index 位置就晚了,因此要找的是 index - 1,可以使用一个小技巧,让 i 从 1 开始遍历,这样判断的时候判断 i == index 就可以了。
    • AddAtTai 可以优化,不管有没有头节点,只要当前节点的下一节点为 NULL 停止进行添加就可以了。
    • myLinkedListAddAtIndex 中在索引0插入可以优化为直接调用 AddAtHead
  • 代码实现:

typedef struct MyLinkedList{
    int val;
    struct MyLinkedList *next;
} MyLinkedList;


MyLinkedList* myLinkedListCreate() {
    // 1.创建虚拟头节点
    MyLinkedList *dummyHead = (MyLinkedList *)malloc(sizeof(MyLinkedList));
    // 2.设置值和下一节点
    dummyHead->val = 0;
    dummyHead->next = NULL;

    return dummyHead;
}

int myLinkedListGet(MyLinkedList* obj, int index) {
    // 调试输出
    // printf("debug:\n");
    // MyLinkedList *debugNode = obj->next;
    // int debugIndex = 0;
    // while(debugNode != NULL) {
    //     printf("debuIndex = %d, val = %d\n", debugIndex++, debugNode->val);
    //     debugNode = debugNode->next;
    // }

    // 先取出真实头节点
    MyLinkedList *cur = obj->next;
    // 遍历各个节点
    for(int i = 0; cur != NULL; ++i) {
        // 找到目标节点
        if(i == index) {
            // 返回节点val
            return cur->val;
        }
        else {
            // 没找到
            cur = cur->next;
        }
    }
    // 遍历完不够这么多索引——下标无效
    return -1;
}

void myLinkedListAddAtHead(MyLinkedList* obj, int val) {
    // 先创建出该节点
    MyLinkedList *newHead = (MyLinkedList *)malloc(sizeof(MyLinkedList));
    newHead->val = val;
    // 将 newHead 设置为新的头节点
    newHead->next = obj->next;
    obj->next = newHead;
}

void myLinkedListAddAtTail(MyLinkedList* obj, int val) {
    // 先创建该节点
    MyLinkedList *node = (MyLinkedList *)malloc(sizeof(MyLinkedList));
    node->val = val;
    node->next = NULL;

    // 特殊情况:没有头节点
    if(obj->next == NULL) {
        // 直接添加为头节点
        obj->next = node;
    }
    else {
        // 正常情况:存在头节点——遍历找到末尾
        MyLinkedList *cur = obj->next;
        // 遍历到链表的末尾节点
        while(cur->next != NULL) {
            cur = cur->next;
        }
        // 添加
        cur->next = node;
    }
}

void myLinkedListAddAtIndex(MyLinkedList* obj, int index, int val) {
    // 特殊情况:在头节点(索引0)插入
    if(index == 0) {
        // 创建节点
        MyLinkedList *node = (MyLinkedList *)malloc(sizeof(MyLinkedList));
        node->val = val;
        // 添加到头节点位置
        node->next = obj->next;
        obj->next = node;
        return;
    }

    // 不在头节点插入
    MyLinkedList *cur = obj->next;
    // 遍历找到目标位置,注意要插入的是index位置,所以要找到 index - 1就停
    // 让遍历参数 i 从 1 开始
    for(int i = 1; cur != NULL; ++i) {
        if(i == index) {
            // 创建节点,插入
            MyLinkedList *node = (MyLinkedList *)malloc(sizeof(MyLinkedList));
            node->val = val;

            node->next = cur->next;
            cur->next = node;
        }
        else {
            // 移动到下一节点
            cur = cur->next;
        }
    }
    // 没找到,不插入,不做任何处理
}

void myLinkedListDeleteAtIndex(MyLinkedList* obj, int index) {
    // 特殊情况:头节点的删除(前提:头节点存在)
    if(obj->next != NULL && index == 0) {
        MyLinkedList *tmp = obj->next;
        // 逻辑删除
        obj->next = tmp->next;
        // 物理删除
        free(tmp);

        return;
    }

    // 非头节点删除情况——注意
    // 注意:要删除的是索引为index的节点,因此当找到 index - 1 时就要着手删除
    // 小技巧:让i从1开始遍历
    MyLinkedList *cur = obj->next;
    // 遍历
    // int targetIndex = index - 1;
    for(int i = 1; cur != NULL; ++i) {
        if(i == index) {
            // 找到了——执行删除
            MyLinkedList *tmp = cur->next;
            // 逻辑删除 -- 需要保证 tmp 不为 NULL,如果tmp为NULL——索引本身是无效的
            if(tmp != NULL) {
                cur->next = tmp->next;
            }
            // 物理删除 -- 即使 tmp 为NULL,free也没影响
            free(tmp);
        }
        else {
            // 移动到下一节点
            cur = cur->next;
        }
    }
}

void myLinkedListFree(MyLinkedList* obj) {
    // 遍历链表释放每个节点
    while(obj != NULL){
        MyLinkedList *tmp = obj;
        obj = obj->next;
        free(tmp);
    }
}

/**
 * Your MyLinkedList struct will be instantiated and called as such:
 * MyLinkedList* obj = myLinkedListCreate();
 * int param_1 = myLinkedListGet(obj, index);
 
 * myLinkedListAddAtHead(obj, val);
 
 * myLinkedListAddAtTail(obj, val);
 
 * myLinkedListAddAtIndex(obj, index, val);
 
 * myLinkedListDeleteAtIndex(obj, index);
 
 * myLinkedListFree(obj);
*/

4.反转链表

  • 题目链接:206. 反转链表 - 力扣(LeetCode)

  • 思路:记录当前节点的前一节点和下一节点,然后反转:

    • 更改下一节点:cur -> next = prev;
    • 移动 prev 和 cur;
  • 代码实现:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
struct ListNode* reverseList(struct ListNode* head) {
    // 思路:从头节点开始,每次记录前一个节点、当前节点和下一个节点
    typedef struct ListNode ListNode;
    // 1.当前节点和前一个节点变量,前一节点初始化为NULL
    ListNode *prev = NULL;
    ListNode *cur = head;
    
    // 2.遍历
    while(cur != NULL) {
        // 2.1 记录下一节点
        ListNode *tmp = cur->next;
        // 2.2 让当前节点的下一节点成为前一节点
        cur->next = prev;
        // 2.3 更新前一节点为当前节点
        prev = cur;
        // 2.4 更新当前一节点到正序的下一节点
        cur = tmp;
    }

    // 3.返回--最后cur走到了NULL,因此返回prev
    return prev;
}

5.两两交换链表中的节点

  • 题目链接:24. 两两交换链表中的节点 - 力扣(LeetCode)

  • 思路:按题目要求对每一对节点进行处理即可;

    • 交换时分三步:cur和next为要交换的节点,则先让prev指向next,再让cur指向next的next,再连接起来next到cur,最后记得更新prev和cur。
  • 代码实现:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
struct ListNode* swapPairs(struct ListNode* head) {
    // 思路:遍历,每次递增两个节点,边界条件为判断到当前节点以及其下一个节点为NULL停止
    // 1.虚拟头节点
    typedef struct ListNode ListNode;
    ListNode *dummyHead = (ListNode*)malloc(sizeof(ListNode));
    dummyHead->next = head;
    ListNode *prev = dummyHead;
    ListNode *cur = head;

    // 2.遍历 -- 确保进入新的循环时至少还有两个节点
    while(cur != NULL && cur->next != NULL) {
        // 对cur和其next执行交换
        // 2.1 将next接到prev后面
        prev->next = cur->next;
        // 2.2 将next的next接到cur后面
        cur->next = cur->next->next;
        // 2.3 将cur接到next后面
        prev->next->next = cur;
        // 更新prev、cur
        prev = cur;
        cur = cur->next;
    }

    // 3.返回
    return dummyHead->next;
}

6.删除链表的倒数第N个节点

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
struct ListNode* removeNthFromEnd(struct ListNode* head, int n) {
    // 思路:快慢双指针,链表最少有一个节点,让快慢指针间隔n
    // 因为可能会删除头结点,所以需要虚拟头结点

    typedef struct ListNode ListNode;
    // 1.定义虚拟头结点
    ListNode *dummyHead = (ListNode*)malloc(sizeof(ListNode));
    dummyHead->next = head;

    // 2.定义快慢双指针
    // 2.1 快指针
    ListNode *fast = head;
    // 2.2 快指针多走 n
    for(int i = 0; i < n; ++i) {
        fast = fast->next;
    }
    // 2.3 定义慢指针此时指向head
    ListNode *slow = head;

    // 3.让fast移动到为NULL,slow同步移动,则此时slow就在倒数第n个节点——要删除就要记录slow的前一个节点
    ListNode *prev = dummyHead;
    while(fast != NULL) {
        fast = fast->next;
        slow = slow->next;
        prev = prev->next;
    }

    // 4.执行删除
    // 4.1 逻辑删除
    prev->next = slow->next;
    // 4.2 物理删除
    free(slow);

    // 5 返回--释放虚拟头结点
    ListNode *newHead = dummyHead->next;
    free(dummyHead);
    return newHead;

    
    
}

7.链表相交

  • 题目链接:面试题 02.07. 链表相交 - 力扣(LeetCode)

  • 思路:如果二者相交,应该是尾端对齐,按照对齐的位置一个一个对比节点地址是否相同(注意不是节点值);

    • 先计算出来两个链表的长度m、n,计算得到长度差lenDiff;
    • 然后将尾端对齐——即让长的先走lenDiff步;
    • 比较节点地址是否相同。
  • 代码实现:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
    // 思路:如果二者相交,应该是尾端对齐,按照对齐的位置一个一个对比
    // 先计算出来m、n,计算得到长度差lenDiff;
    // 然后将尾端对齐——即让长的先走lenDiff;
    // 按对齐节点一一比较

    typedef struct ListNode ListNode;
    // 1.计算长度
    int lenA = 0, lenB = 0;
    ListNode *curA = headA;
    while(curA != NULL) {
        ++lenA;
        curA = curA->next;
    }
    ListNode *curB = headB;
    while(curB != NULL) {
        ++lenB;
        curB = curB->next;
    }

    // 2.计算长度差
    int lenDiff = lenA > lenB ? (lenA - lenB) : (lenB - lenA);

    // 3.长的先走 lenDiff 步
    curA = headA;
    curB = headB; 
    if(lenA > lenB) {
        for(int i = 0; i < lenDiff; ++i) {
            curA = curA->next;
        }
    }
    else {
        for(int i = 0; i < lenDiff; ++i) {
            curB = curB->next;
        }
    }

    // 4.比较 curA 和 curB 是否指向相同的节点
    while(curA != NULL) {
        if(curA == curB) {
            return curA;
        }
        curA = curA->next;
        curB = curB->next;
    }

    // 5.返回NULL
    return NULL;

}

8.环形链表Ⅱ

  • 题目链接:142. 环形链表 II - 力扣(LeetCode)

  • 思路:

    • 快慢双指针,如果存在环,快指针每次走两步,慢指针每次走一步,必能相遇;
    • 按照数学推导,当快慢指针相遇时,两个指针分别同步从开头和相遇点移动,再次相遇就是入环节点。
  • 代码实现:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
struct ListNode *detectCycle(struct ListNode *head) {
    // 思路:快慢双指针,如果存在环,快指针每次走两步,慢指针每次走一步,必能相遇
    // 按照数学推导,当快慢指针相遇时,两个指针分别同步从开头和相遇点移动,再次相遇就是入环节点

    typedef struct ListNode ListNode;
    // 1.快慢双指针
    ListNode *fast = head;
    ListNode *slow = head;

    // 2.移动,找快慢指针相遇
    // 快指针每次移动两步,要保证节点不为NULL——主要是排除没有环
    while(fast != NULL && fast->next != NULL) {
        slow = slow->next;
        fast = fast->next->next;

        // 3.如果相遇——必定有环,寻找环入口
        if(slow == fast) {
            ListNode *node1 = head;
            ListNode *node2 = fast;

            // 4.同步移动到再次相遇
            while(node1 != node2) {
               node1 = node1->next;
               node2 = node2->next; 
            }
            return node1;
        }
    }

    return NULL;
}

9.总结篇

  • 链表分类:单链表、双链表、循环链表;
  • 虚拟头结点;
  • 链表中的双指针:快慢双指针。

哈希表

1.哈希表理论基础

  • 哈希表:根据关键码的值直接进行访问;

  • 哈希碰撞的解决:拉链法、线性探测法;

  • 哈希法的三种数据结构:

    • 数组
    • set(集合)
    • map(映射)

2.有效的字母异位词

  • 题目链接:242. 有效的字母异位词 - 力扣(LeetCode)

  • 思路:哈希,因为只包含小写字母,用数组就可以,遍历s统计字符数量、t减去,最后看所有位置是不是都是0;

    • 注意数组测试后元素应该可以默认为0;
    • 也可以调用函数strlen获得字符串的长度,然后进行for循环;
  • 代码实现:

bool isAnagram(char* s, char* t) {
    // 思路:哈希,因为只包含小写字母,用数组就可以

    // 1.定义哈希
    int hash[26] = {0};

    // 2.遍历字符串s,统计字符出现次数
    while(*s != '\0') {
        // 2.1 统计字符
        // 2.2 移动s
        // printf("%c\n", *s);
        hash[*(s++) - 'a']++;
    }

    // 3.遍历t,减去字符
    while(*t != '\0') {
        --hash[*(t++) - 'a'];
    }

    // 4.判断是否所有位都为0
    for(int i = 0; i < 26; ++i) {
        if(hash[i] != 0) {
            return false;
        }
    }

    return true;
}

3.两个数组的交集

  • 题目链接:349. 两个数组的交集 - 力扣(LeetCode)

  • 思路:哈希,题目规定了两个数组的长度范围,因此可以使用哈希数组;

    • 注意,返回的结果数组要传出去,因此不能是局部变量,要动态分配内存:int* ret = (int*)malloc(lessSize * sizeof(int));
  • 代码实现:

/**
 * Note: The returned array must be malloced, assume caller calls free().
 */
int* intersection(int* nums1, int nums1Size, int* nums2, int nums2Size, int* returnSize) {
    // 思路:哈希,题目规定了两个数组的长度范围,因此可以使用哈希数组

    // 1.创建哈希数组
    int hash[1001];
    // 结果数组——需要返回,动态创建出来
    int lessSize = nums1Size < nums2Size ? nums1Size : nums2Size;
    int* ret = (int*)malloc(lessSize * sizeof(int));

    // 2.遍历nums1,将出现的数字存入哈希
    for(int i = 0; i < nums1Size; ++i) {
        hash[*(nums1 + i)]++;
    }

    // 3.遍历nums2,如果数字在hash中出现过,存放到结果中
    // 结果数组索引
    int index = 0;
    for(int i = 0; i < nums2Size; ++i) {
        if(hash[*(nums2 + i)] != 0) {
            // 当前元素在hash中存在,放入结果
            ret[index++] = nums2[i];
            // 记得将hash当前位置清掉,避免重复添加
            hash[*(nums2 + i)] = 0;
        }
    }

    // 4.返回结果,记得为结果数组添加长度
    *returnSize = index;
    return ret;
}

4.快乐数

  • 题目链接:202. 快乐数 - 力扣(LeetCode)

  • 思路:关键点——可能无限循环就到不了1——通过判断算出来的结果是否出现过——哈希

    • 用哪种哈希?c语言没有set,数组要求可能出现的索引有限?
    • n <= 2^31-1,也就是10位数,每一位最大是9,那么每次计算最大可能是9910 = 810,哈希数组可行
  • 代码实现:

int getSum(int n);

bool isHappy(int n) {
    // 思路:关键点——可能无限循环就到不了1——通过判断算出来的结果是否出现过——哈希
    // 用哪种哈希?c语言没有set,数组要求可能出现的索引有限?
    // n <= 2^31-1,也就是10位数,每一位最大是9,那么每次计算最大可能是9*9*10 = 810,哈希数组可行
    #define HASH_LENGTH = 811;

    // 1.定义哈希数组
    int hash[811] ={};

    // 2.遍历计算,判断是否出现了无限循环
    while(n != 1) {
        // 2.1 计算
        int sum = getSum(n);
        // 2.2 判断
        if(hash[sum] != 0) {
            // 无限循环,返回false
            return false;
        }
        else {
            // 加入hash、替换n
            hash[sum]++;
            n = sum;
        }
    }

    // 3.成功出来了,返回true
    return true;
}

// 根据n求取每一位上的平方和
int getSum(int n) {
    int sum = 0;
    while(n != 0) {
        int temp = n % 10;
        sum += temp * temp;
        n = n / 10;
    }
    return sum;
}

5.两数之和

  • 题目链接:1. 两数之和 - 力扣(LeetCode)

  • 思路:哈希:每遍历一个数,将他和target的差值存放到哈希中,后面遍历到的数如果存在于哈希中,就找到了;

    • 需要根据值返回索引,因此用hashmap;
  • 代码实现:

class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        // 哈希:每遍历一个数,将他和target的差值存放到哈希中,后面遍历到的数如果存在于哈希中,就找到了,需要返回索引,因此用hashmap
        // 1.定义哈希
        std::unordered_map<int, int> map;
        // 2.遍历元素并执行寻找
        for(int i = 0; i < nums.size(); ++i) {
            // 2.1 判断和nums[i]组成target的值在不在map
            auto iter = map.find(target - nums[i]);
            if(iter != map.end()) {
                // 2.2 找到了,返回索引结果
                return {iter->second, i};
            }
            // 2.3 没找到,存入map——值是key,索引是value
            map.insert(pair<int, int>(nums[i], i));
        }

        return {};
    }
};

6.四数相加Ⅱ

  • 题目链接:454. 四数相加 II - 力扣(LeetCode)

  • 思路:

    • 只要满足题目要求的数量——不用考虑向量之间元素的重复;
    • 因此只要先统计两组元素可能的和,然后在另两组元素和中找相反数——哈希;
    • 每次找到,需要知道该和的个数——用map;
  • 代码实现:

class Solution {
public:
    int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
        // 思路:只要满足题目要求的数量——不用考虑向量之间元素的重复
        // 因此只要先统计两组元素可能的和,然后在另两组元素和中找相反数——哈希
        // 每次找到,需要知道该和的个数——用map

        int count = 0;
        // 1.定义哈希
        std::unordered_map<int, int> map;

        // 2.嵌套遍历前两组向量,统计和
        for(int a : nums1) {
            for(int b : nums2) {
                // 插入哈希
                map[a + b]++;
            }
        }

        // 3.嵌套遍历后两组向量,计算和,在哈希中找相反数
        for(int c : nums3) {
            for(int d : nums4) {
                if(map.find(0 - (c + d)) != map.end()) {
                    count += map[0 - (c + d)];
                }
            }
        }

        return count;

    }
};

7.赎金信

  • 题目链接:383. 赎金信 - 力扣(LeetCode)

  • 思路:

    • 哈希,先遍历magazine,字符为key,出现次数为value;
    • 然后遍历ransomNote,每个字符取哈希中找,找到对应值<=0返回false;
    • 优化:字母全是小写字母,因此可以用哈希数组代替哈希map节省空间、时间消耗。
  • 代码实现:

class Solution {
public:
    bool canConstruct(string ransomNote, string magazine) {
        // 1.哈希
        std::unordered_map<char, int> map;

        // 2.遍历magazine
        for(char ch : magazine) {
            map[ch]++;
        }

        // 3.遍历randomNote
        std::unordered_map<char, int>::iterator it;
        for(char ch : ransomNote) {
            it = map.find(ch);
            // 3.1 找不到或者已经被前面匹配完了
            if(it == map.end() || it -> second <= 0) {
                return false;
            }
            // 3.2 map中还有可以匹配的字符——减一
            it -> second--;
        }

        return true;
    }
};

8.三数之和

  • 题目链接:15. 三数之和 - 力扣(LeetCode)

  • 思路:排序+双指针

    • 结果需要的是元素的组合而不是元素的索引,因此可以进行排序,从而方便在后续排除某些答案选项;
    • 排序后先遍历第一个元素,对第一个元素可以注意剪枝和去重;
    • 对另外两个元素,分别设为剩余元素的前后双指针,遍历,如果整体和偏大,减小右指针,整体和偏小,增大左指针;
    • 当找到一种可能的结果时,要去重左右指针出现的元素;
    • 最后应当注意,找到可能结果后,要显式地对双指针进行移动,否则程序会死循环在这种结果上。
  • 代码实现:

// 思路:排序+双指针
class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) {
        // 1.结果向量
        vector<vector<int>> ret;

        // 2.排序
        sort(nums.begin(), nums.end());

        // 3.遍历第一个元素
        for(int i = 0; i < nums.size(); ++i) {
            // 3.1 第一个元素剪枝
            if(nums[i] > 0) {
                break;
            }
            // 3.2 第一个元素去重
            if(i > 0 && nums[i] == nums[i - 1]) {
                continue;
            }

            // 4.双指针遍历后两个元素,此时需要后双指针元素的和为-nums[i],且上面已经保证nums[i] <= 0
            int left = i + 1, right = nums.size() - 1;
            while(left < right) {
                // 4.1 两元素之和 + nums[i] > 0,移动后面的那个元素
                while(left < right && nums[left] + nums[right] + nums[i] > 0) {
                    --right;
                }
                // 4.2 两元素之和 + nums[i] < 0,移动前面的指针
                while(left < right && nums[left] + nums[right] + nums[i] < 0) {
                    ++left;
                }
                // 4.3 两元素之和 + nums[i] = 0,此时应保证 left < right
                if(left < right && nums[left] + nums[right] + nums[i] == 0) {
                    // 将结果加入向量
                    ret.push_back(vector<int> {nums[i], nums[left], nums[right]});
                    // 出现了一种结果,此时要进行去重——不能再出现与left、right指向值相同的元素
                    while(left < right && nums[right] == nums[right - 1]) {
                        --right;
                    }
                    while(left < right && nums[left] == nums[left + 1]) {
                        ++left;
                    }
                    // 找到了答案,注意对双指针进行移动,否则会死循环在这一个答案上
                    ++left;
                    --right;
                }
            }
        }

        // 5.返回结果
        return ret;
    }
};

9.四数之和

  • 题目链接:18. 四数之和 - 力扣(LeetCode)

  • 思路:

    • 同三数之和,外面两层for循环+双指针;
    • 前两个元素剪枝的时候注意,这里和的目标值不是零,排序之后不能单独判断是否大于target,因为后面的两个数还有可能是负数;
    • 题目中对于向量中的元素范围定义是 <= 109,当计算四个数之和时如果都顶着上极限会超过int的上限231,所以最后判断四个数之和的时候要注意增加一个long的强转
  • 代码实现:

// 思路:同三数之和,外面两层for循环+双指针
class Solution {
public:
    vector<vector<int>> fourSum(vector<int>& nums, int target) {
        // 1.结果向量
        vector<vector<int>> ret;
        
        // 2.排序
        sort(nums.begin(), nums.end());

        // 3.第一层遍历
        for(int i = 0; i < nums.size(); ++i) {
            // 3.1 第一个元素剪枝--注意与三数之和不同的是,此时目标值可能是负的,因此不能单独判断nums[i] > target
            if(nums[i] > 0 && nums[i] > target) {
                break;
            }
            // 3.2 第一个元素的去重
            if(i > 0 && nums[i] == nums[i - 1]) {
                continue;
            }

            // 4.第二层遍历
            for(int j = i + 1; j < nums.size(); ++j) {
                // 4.1 第二个元素剪枝
                if(nums[i] + nums[j] > 0 && nums[i] + nums[j] > target) {
                    break;
                }
                // 4.2 第二个元素的去重
                if(j > i + 1 && nums[j] == nums[j - 1]) {
                    continue;
                }

                // 5.双指针实现O(n)遍历后两个元素
                int left = j + 1, right = nums.size() - 1;
                while(left < right) {
                    // 5.1 如果总和 > target,右指针左移
                    while(left < right && (long)nums[i] + nums[j] + nums[left] + nums[right] > target) {
                        --right;
                    }
                    // 5.2 如果总和 < target,左指针右移
                    while(left < right && (long)nums[i] + nums[j] + nums[left] + nums[right] < target) {
                        ++left;
                    }
                    // 5.3 找到满足要求的组合
                    if(left < right && (long)nums[i] + nums[j] + nums[left] + nums[right] == target) {
                        // 5.3.1 放入结果集合
                        ret.push_back(vector<int> {nums[i], nums[j], nums[left], nums[right]});

                        // 5.3.2 注意去重:调节左右指针,避免左右指针所指元素出现重复
                        while(left < right && nums[right] == nums[right - 1]) --right;
                        while(left < right && nums[left] == nums[left + 1]) ++left;

                        // 5.3.3 注意移动左右指针,如果上述没有去重,下次还会进入这种组合
                        --right;
                        ++left;
                    }
                }
            }
            
        }

        // 6.返回结果
        return ret;
    }
};

10.总结篇

  • 三种哈希结构:数组、set(集合)、map(映射);

  • C++:

    • std::setstd::multisetstd::mapstd::multimap底层实现都是红黑树,key是有序的;
    • std::unordered_setstd::unordered_map 底层实现为哈希;

字符串

1.反转字符串

// 思路:双指针
class Solution {
public:
    void reverseString(vector<char>& s) {
        // 1.定义双指针
        int left = 0, right = s.size() - 1;
        char temp;

        // 2.遍历,相等的时候是同一个,因此只用在left < right的时候判断
        while(left < right) {
            // 2.1 反转
            temp = s[left];
            s[left] = s[right];
            s[right] = temp;

            // 2.2 更新指针
            ++left;
            --right;
        }
    }
};

2.反转字符串Ⅱ

  • 题目链接:541. 反转字符串 II - 力扣(LeetCode)
  • 思路:
    • 双指针+每次反转后让索引i移动2k;
    • 开始的错误思路:用索引来控制每次的2k区间,再去取其中的前k个,逻辑不够简洁;
  • 代码实现:
// 思路:双指针+每次反转后让索引i移动2k
class Solution {
public:
    string reverseStr(string s, int k) {
        for(int i = 0; i < s.size(); i += 2*k) {
            // 反转区间 i ~ i + k - 1,左闭右闭
            // 特殊情况就是新的2k区间不够k个,要调整终点为 s.size() - 1
            reverse(s, i, i + k <= s.size() ? i + k - 1 : s.size() - 1);
        }
        return s;
    }

    void reverse(string& s, int begin, int end) {
        char temp;
        while(begin < end) {
            temp = s[begin];
            s[begin] = s[end];
            s[end] = temp;

            ++begin;
            --end;
        }
    }
};

3.替换数字

  • 题目链接:
  • 思路:
  • 代码实现:

4.翻转字符串里的单词

  • 题目链接:151. 反转字符串中的单词 - 力扣(LeetCode)

  • 思路:

    • 先去除多余的空格,然后双指针将s整体倒序,最后从头开始对每个单词双指针颠倒会单词的正序;
    • 重点:移除多余的空格,需要结合数组中移除指定的元素操作
  • 代码实现:

// 思路:先去除多余的空格,然后双指针将s整体倒序,最后从头开始对每个单词双指针颠倒会单词的正序

class Solution {
public:
    string reverseWords(string s) {
        // 1.移除多余空格
        eraseExtraSpace(s);

        // 2.双指针倒序s整体
        inverseString(s, 0, s.size() - 1);

        // 3.遍历倒序后的s,每次找到空格停止,左闭右开为一个单词,双指针倒序单词回正序
        int wordStartIndex = 0;
        for(int i = 0; i <= s.size(); ++i) {
            // 3.1 如果遇到空格或走到字符串末尾,辨识出一个单词
            if(i == s.size() || s[i] == ' ') {
                inverseString(s, wordStartIndex, i - 1);
                wordStartIndex = i + 1;
            }
        }

        return s;
    }

    // 快慢双指针移除多余元素--空格
    void eraseExtraSpace(string& s) {
        // 思路:快慢双指针,快指针遇到要移除的元素就跳过,而慢指针按需要的字符次序放置快指针对应的字符
        int slowIndex = 0, fastIndex = 0;
        // 1.移除前导空格
        while(fastIndex < s.size() && s[fastIndex] == ' ') {
            ++fastIndex;
        }
        // 2.移除单词之间多余的空格
        for(; fastIndex < s.size(); ++fastIndex) {
            // 2.1 判别单词之间多余的空格——是空格,并且前面还是空格
            if(fastIndex - 1 > 0 && s[fastIndex] == s[fastIndex - 1] && s[fastIndex] == ' ') {
                // 按照快慢双指针在数组中移除元素,直接让快指针+1跳过不赋值
                continue;
            }
            else {
                // 2.2 是有效元素——赋值
                s[slowIndex++] = s[fastIndex]; 
            }
        }
        // 3.移除尾缀多余的空格,最后如果有多余的空格,在上一步只会有一个被移动到slow上一个位置
        if(slowIndex - 1 > 0 && s[slowIndex - 1] == ' ') {
            // 删除这个空格——直接resize字符串就好了
            s.resize(slowIndex - 1);
        } else {
            // 没有尾缀空格
            s.resize(slowIndex);
        }
    }

    // 双指针反转字符串,双指针区间左闭右闭
    void inverseString(string& s, int begin, int end) {
        char temp;
        while(begin < end) {
            temp = s[begin];
            s[begin] = s[end];
            s[end] = temp;
            ++begin;
            --end;
        }
    }
};

5.右旋转字符串

  • 题目链接:
  • 思路:
  • 代码实现:

6.实现strStr()

  • 题目链接:28. 找出字符串中第一个匹配项的下标 - 力扣(LeetCode)

  • 思路:

    • 在文本串里面匹配模式串——如果每次匹配失败,就直接在文本串里遍历用下一个起始位置是O(n*m)的暴力解法;
    • KMP:如果某一位匹配不上,其实文本串中前面匹配的子串已经判断过一波了,是可以用起来的;
    • 怎么用?总要让模式串的前面一部分和文本串刚匹配的子串对应上,而这段子串整体是出现在模式串中的
    • 因此,就是模式串中的这部分从头开始的子串,它的前缀(不包含最后一个字符)和它的后缀(不包含第一个字符)有多少个字符能对上,然后就可以不用匹配前缀这一部分了——前缀表来记录
    • KMP精讲2
    • 前缀表的构造:
      • 遍历模式串的每一位,刚好可以作为后缀子串的末尾,然后在用一个控制相等前缀的末尾;
      • 对比两个字符,如果对不上,索引0~j这一段子串j位置对不上i,next[j - 1]刚好存储着0
        ~j-1中最长相等前后缀,用它来回退j;why?--个人理解:其实此时也是一个模式串与文本串的匹配,我们让j的回退是因为前面这一段可能和i前面的部分重合,而j和i又不重合,只能尽可能把j之前的利用起来(同样利用前缀表),哪怕觉得此时这样回退不如j逐步-1回退好,其实这种想法是行不通的,按这种回退,你得每次一位一位比较,只能是从每个开头的暴力遍历;
  • 代码实现:

// 思路:在文本串里面匹配模式串——如果每次匹配失败,就直接在文本串里遍历用下一个起始位置是O(n*m)的暴力解法
// KMP:如果某一位匹配不上,其实文本串中前面匹配的子串已经判断过一波了,是可以用起来的
// 怎么用?总要让模式串的前面一部分和文本串刚匹配的子串对应上,而这段子串整体是出现在模式串中的
// 因此,就是模式串中的这部分从头开始的子串,它的前缀(不包含最后一个字符)和它的后缀(不包含第一个字符)有多少个字符能对上,然后就可以不用匹配前缀这一部分了——前缀表来记录
class Solution {
public:
    // 构建前缀表,就是一个数组,到这一位索引时,最长相等前后缀
    // 参数:
    // next -- 前缀表数组,开头到这里,最长相等前后缀的长度;
    // s -- 要处理的模式串
    void getNext(int *next, string &s) {
        // 用i遍历当前索引,那么i就是指向后缀末尾的位置,再用j指向前缀末尾的位置
        // 1、初始化,0号位没有前后缀,所以为0,同时j也指向0
        int j = 0;
        next[0] = j;
        // 2、遍历,0号位处理过了,i从1开始
        for(int i = 1; i < s.size(); ++i) {
            // 3、比较i位置和j位置是否相等,不相等,next[j - 1]就记录了最长相等前后缀,这样进行回退
            while(j > 0 && s[i] != s[j]) {
                j = next[j - 1];
            }
            // 4、相等,移动j,为下一个循环i的移动与判断做准备
            if(s[i] == s[j]) {
                ++j;
            }
            // 5、无论是否判断相等与否,为i的最长相等前后缀赋值,由j决定,要么有,要么为0
            next[i] = j;
        }
    }


    int strStr(string haystack, string needle) {
        // 1、为模式串needle构造前缀表
        vector<int> next(needle.size());
        getNext(&next[0], needle);
        // 2、准备索引j,控制模式串中当前能匹配上的最长位置
        int j = 0;
        // 3、遍历文本串
        for(int i = 0; i < haystack.size(); ++i) {
            // 3.1 如果文本串中当前索引i与模式串对应索引i不匹配,那么就用前缀表对j进行回退
            while(j > 0 && haystack[i] != needle[j]) {
                j = next[j - 1];
            }
            // 3.2 如果相等了,就移动j就好了
            if(haystack[i] == needle[j]) {
                ++j;
                // 4、j有移动的时候,可能完成了模式串的匹配,要检查
                if(j == needle.size()) {
                    // 返回haystack中的下标
                    return i - (needle.size() - 1);
                }
            }
        }

        // 5、前面没返回就返回-1
        return -1;
    }
};

7.重复的子字符串

  • 题目链接:459. 重复的子字符串 - 力扣(LeetCode)

  • 思路1:

    • KMP,如果存在重复子串t,那么s = n*t,并且s的最长相等前后缀是(m - 1) * t,此时 s / t 可以整除,因此,求解s的最长前后缀,用s的长度 - 最长前后缀长度(存放在最后一个索引位置),可以被s整除即存在
  • 代码实现1:

class Solution {
public:
    // 计算KMP前后缀
    void getNext(int *next, const string &s) {
        // 1、初始化第0个元素
        int j = 0;
        next[0] = j;
        for(int i = 1; i < s.size(); ++i) {
            // 2、如果不相等,j回退到next[j-1]
            while(j > 0 && s[i] != s[j]) {
                j = next[j - 1];
            }
            // 3、如果相等,j向后
            if(s[i] == s[j]) {
                ++j;
            }
            // 4、更新i位置最长相等前后缀
            next[i] = j;
        }
    }

    bool repeatedSubstringPattern(string s) {
        // 1、计算s的前缀表
        vector<int> next(s.size());
        getNext(&next[0], s);

        // 2、计算差,判断
        int sSize = s.size();
        int maxNextLength = next[sSize - 1];
        // 2.1 如果能整除,存在,注意判断maxNextLength要不为0
        if(maxNextLength != 0 && sSize % (sSize - maxNextLength) == 0) {
            return true;
        }
        // 2.2 不能整除,不存在
        return false;
    }
};
  • 思路2:
    • 移动匹配,如果s由重复子串组成,s+s中一定可以再出现一个s,这样判断就好了;
    • 不过要注意s+s要去掉首尾字符,避免找到原来的两个s;
    • 字符串find()用法:如果没找到,判断是否等于std::string::npos
  • 代码实现2:
class Solution {
public:
    
    bool repeatedSubstringPattern(string s) {
        // 1、s+s
        string t = s + s;
        // 2、掐头去尾
        t.erase(t.begin());
        t.erase(t.end() - 1);
        // 3、再判断包不包含s
        if(t.find(s) != std::string::npos) {
            return true;
        }
        return false;
    }
};

8.总结篇

  • 字符串是若干字符组成的有限序列,可以理解为字符数组;
    • C语言:把一个字符串存入一个数组时,也把结束符 '\0'存入数组,并以此作为该字符串是否结束的标志;
    • C++:提供一个string类,string类会提供 size接口,可以用来判断string类字符串是否结束,就不用'\0'来判断是否结束;
      • vector< char > 和 string 区别:在基本操作上没有区别,但是 string提供更多的字符串处理的相关接口,例如string 重载了+,而vector却没有;
  • 高频算法:
    • 双指针法:反转字符串、反转字符串里的单词并删除冗余空格;
    • 反转字符串:先整体反转在局部反转;
    • KMP:重点理解前缀表;

双指针法

  • 数组篇:移除元素,快慢双指针,快指针用于跳过要删除的元素,慢指针控制保留元素的位置;
  • 字符串篇:
    • 反转字符串:首尾双指针;
    • 替换数字:先统计出要替换的个数,对字符串进行resize,然后双指针从后往前进行替换,以免从前往后时会有原有元素的移动;
    • 反转单词:并删除空格,先按照移除元素的思路对多余空格进行移除,然后整体反转+局部反转实现单词反转;
  • 链表篇:
    • 反转链表:双指针;
    • 删除链表倒数第N个节点:快慢双指针;
    • 链表相交:尾部对齐以后快慢双指针;
    • 环形链表:快慢双指针;
  • N数之和:
    • 两数之和:哈希;
    • 三数之和、四数之和:用哈希逻辑会很复杂,双指针缩减一层时间复杂度;

栈与队列

1.栈与队列理论基础

  • 基础:
    • 栈:先进后出;
    • 队列:先进先出;
  • C++ STL的版本:
    • HP STL,其他版本的C++ STL,一般是以HP STL为蓝本实现出来的;
    • P.J.Plauger STL,参照HP STL实现出来的,被Visual C++编译器所采用,不是开源的;
    • SGI STL,参照HP STL实现,被Linux的C++编译器GCC所采用,SGI STL是开源软件,源码可读性甚高。
  • 栈:
    • 提供push 和 pop 等等接口,所有元素必须符合先进后出规则,所以栈不提供走访功能,也不提供迭代器(iterator)
    • 栈是以底层容器完成其所有的工作,对外提供统一的接口,底层容器是可插拔的(也就是说我们可以控制使用哪种容器来实现栈的功能);
    • 所以STL中栈往往不被归类为容器,而被归类为container adapter(容器适配器)
    • 栈的底层实现可以是vector,deque,list;默认以 deque为缺省情况下栈的底层结构
  • 队列:
    • 同样不允许有遍历行为,不提供迭代器, SGI STL中队列一样是以deque为缺省情况下的底部结构
    • 也不被归类为容器,而被归类为container adapter( 容器适配器)。

2.用栈实现队列

  • 题目链接:232. 用栈实现队列 - 力扣(LeetCode)

  • 思路:

    • 两个栈实现队列先入先出——核心就是如何做先入先出的逻辑:
    • 用一个栈主要用来装元素,另一个栈用来出元素,每次要出元素时,出元素的栈里面空了就把入元素栈的倒着进来就可以正序出去了
  • 代码实现:

class MyQueue {
private:
    std::stack<int> inStack;
    std::stack<int> outStack;
public:
    MyQueue() {

    }
    
    void push(int x) {
        inStack.push(x);
    }
    
    int pop() {
        // 关键点:如果outStack为空,就把inStack里面的元素倒进来,outStack出的时候就是正序了
        if(outStack.empty()) {
            while(!inStack.empty()) {
                outStack.push(inStack.top());
                inStack.pop();
            }
        }
        // 题目保证空队列不会调用pop或者peek
        // 弹出
        // 1、先记录下要弹出的元素
        int ret = outStack.top();
        // 2、弹出
        outStack.pop();
        // 3、返回
        return ret;
    }
    
    int peek() {
        // 技巧:先调用pop,再塞回去
        int ret = this->pop();
        outStack.push(ret);
        return ret;
    }
    
    bool empty() {
        return inStack.empty() && outStack.empty();
    }
};

/**
 * Your MyQueue object will be instantiated and called as such:
 * MyQueue* obj = new MyQueue();
 * obj->push(x);
 * int param_2 = obj->pop();
 * int param_3 = obj->peek();
 * bool param_4 = obj->empty();
 */

3.用队列实现栈

  • 题目链接:225. 用队列实现栈 - 力扣(LeetCode)

  • 思路:

    • 用一个队列就可以实现,实现后入先出的关键点是用队列pop的时候,先将队列头的全都pop再push到队尾,然后就能pop出最后一个元素了
  • 代码实现:

class MyStack {
public:
    std::queue<int> q;

    MyStack() {

    }
    
    void push(int x) {
        q.push(x);
    }
    
    int pop() {
        int qSize = q.size();
        // 将原来末尾的元素移动到队列头
        while(--qSize != 0) {
            q.push(q.front());
            q.pop();
        }
        // 弹出队头元素
        int ret = q.front();
        q.pop();
        return ret;
    }
    
    int top() {
        int ret = this->pop();
        q.push(ret);
        return ret;
    }
    
    bool empty() {
        return q.empty();
    }
};

/**
 * Your MyStack object will be instantiated and called as such:
 * MyStack* obj = new MyStack();
 * obj->push(x);
 * int param_2 = obj->pop();
 * int param_3 = obj->top();
 * bool param_4 = obj->empty();
 */

4.有效的括号

  • 题目链接:20. 有效的括号 - 力扣(LeetCode)

  • 思路:

    • 栈——正确匹配只需要比较栈顶和当前元素,匹配就消去——即出栈;
    • 技巧:每次左括号存入配对元素,如(实际存入),方便相等判断;
    • 对于右括号,与栈顶元素比较,不匹配直接false。
  • 代码实现:

class Solution {
public:
    bool isValid(string s) {
        // 1、创建栈
        std::stack<char> myStack;
        // 2、遍历s
        for(int i = 0; i < s.size(); ++i) {
            char cur = s[i];
            // 2.1 左括号放入其配对括号
            if(cur == '(') myStack.push(')');
            else if(cur == '[') myStack.push(']');
            else if(cur == '{') myStack.push('}');
            // 2.2 右括号的话比较是否配对
            else if(myStack.empty() || cur != myStack.top()) return false;
            // 2.3 匹配消去
            else myStack.pop();
        }
        // 3、判断栈是否为空
        return myStack.empty();
    }
};

5.删除字符串中的所有相邻重复项

// 思路:栈,栈顶元素和当前元素相同就删除
class Solution {
public:
    string removeDuplicates(string s) {
        // 1、定义栈
        std::stack<char> st;
        // 2、遍历字符串
        for(char ch : s) {
            // 2.1 如果ch和栈顶元素相同,删除两项
            if(st.size() > 0 && ch == st.top()) {
                // 当前元素直接跳过,栈顶元素弹出
                st.pop();
            }
            else {
                // 2.2 不相同,或者栈内为空,入栈
                st.push(ch);
            }
        }
        // 3、将栈内元素放入字符串并倒序
        string ret = "";
        while(!st.empty()) {
            ret += st.top();
            st.pop();
        }
        std::reverse(ret.begin(), ret.end());

        return ret;
    }
};

6.逆波兰表达式求值

  • 题目链接:150. 逆波兰表达式求值 - 力扣(LeetCode)

  • 思路:

    • 栈,遇到数字入栈,遇到算符则取出栈顶两个元素计算,并将结果压入栈中;
    • C++语法知识点:
      • 注意传入参数vector<string>& tokens,是一个向量,向量的每个元素是一个字符串,这点需要注意;
      • push进栈的字符串需要注意转换为数字,这里用的是字符串转int:stoi,类似的还有stoll
  • 代码实现:

// 思路:栈,遇到数字入栈,遇到算符则取出栈顶两个元素计算,并将结果压入栈中
class Solution {
public:
    int evalRPN(vector<string>& tokens) {
        // 1、定义栈
        std::stack<int> st;
        // 2、遍历tokens
        for(int i = 0; i < tokens.size(); ++i) {
            string s = tokens[i];
            if(s == "+" || s == "-" || s == "*" || s == "/"){
                // 2.1 如果为运算符,按照四种情况执行运算
                int num2 = st.top();
                st.pop();
                int num1 = st.top();
                st.pop();
                int temp;
                if(s == "+") {
                    temp = num1 + num2;
                }
                else if(s == "-") {
                    temp = num1 - num2;
                }
                else if(s == "*") {
                    temp = num1 * num2;
                }
                else if(s == "/") {
                    temp = num1 / num2;
                }
                // 结果入栈
                st.push(temp);
            }
            else {
                // 2.2 如果是数值,压入栈中,注意将字符串转换为数字
                st.push(stoi(s));
            }
        }
        // 3、栈中最后元素就是结果
        return st.top();
    }
};

7.滑动窗口最大值

  • 题目链接:239. 滑动窗口最大值 - 力扣(LeetCode)

  • 思路:

    • 单调队列,队列里面按照滑动窗口中的元素从队头到队尾单调递减,那么取出队头元素即可;
    • 如何维护单调队列?
      • 对于滑动窗口移入元素,其实不用保持所有元素,进元素的时候如果队尾的元素小于该元素就丢掉,然后把该元素放入,那些被丢掉的元素不可能成为当前窗口内最大值了;
      • 对于滑动窗口移动移出的元素,如果恰等于队头的元素移除;
      • 对于该单调队列按照上述规则可以知道:在该元素之前且比该元素小的按照移入规则不会在队列里(都被丢弃了),在该元素之后比该元素小的可能会成为后面窗口的最大值,此时还在队列里面(同时队列里也不会维持不可能成为最大值的);
    • C++语法知识点:
      • 双端队列deque
        • 队头、队尾插入、移除:q.push_backq.push_frontq.pop_backq.pop_front
        • 获取队头、队尾元素:q.front()q.back()
      • 向量vector
        • 插入元素:vector.push_back()
  • 代码实现:

class Solution {
// 定义单调队列类
private:
    class MonotonousQueue {
        public:
            // 双端队列成员变量
            std::deque<int> q;
            // 移入元素
            void push(int num) {
                // 规则:只要队尾元素小于该元素,直接丢掉
                while(!q.empty() && q.back() < num) {
                    q.pop_back();
                }
                q.push_back(num);
            }
            // 移出元素
            void pop(int num) {
                // 规则:要移除的元素和队头元素相同才丢掉,队头后面的都比队头进来的晚
                if(num == q.front()) {
                    q.pop_front();
                }
            }
            // 获得队头——也就是滑动窗口最大值
            int front() {
                return q.front();
            }

    };
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        // 1、创建自定义单调队列
        MonotonousQueue myMonotonousQueue;
        std::vector<int> ret;
        // 2、先放入开头k个元素
        int i;
        for(i = 0; i < k; ++i) {
            // 2.1 只执行移入操作
            myMonotonousQueue.push(nums[i]);
        }
        // 2.2 获得初始窗口最大值
        ret.push_back(myMonotonousQueue.front());

        // 3、遍历nums
        for(; i < nums.size(); ++i) {
            // 3.1 移出nums[i - k]
            myMonotonousQueue.pop(nums[i - k]);
            // 3.2 移入nums[i]
            myMonotonousQueue.push(nums[i]);
            // 3.3 获得滑动窗口最大值
            ret.push_back(myMonotonousQueue.front());
        }

        return ret;
    }
};

8.前K个高频元素

  • 题目链接:347. 前 K 个高频元素 - 力扣(LeetCode)

  • 思路:

    • 优先级队列,先用map统计各个元素出现的次数,然后放入优先级队列;

    • 放入优先级队列的小技巧:采用小顶堆,容量为k,这样当元素超过k弹出队列头的最小的从而保持队列里面最大的

    • C++语法:

      • 优先级队列创建:

        priority_queue<T, Container, Compare>
        priority_queue<T>        //直接输入元素则使用默认容器和比较函数
            
        a) T就是Type为数据类型
        
        b) Container是容器类型,(Container必须是用数组实现的容器,比如vector,deque等等,但不能用 list。STL里面默认用的是vector)
        
        c) Compare是比较方法,类似于sort第三个参数那样的比较方式,对于自定义类型,需要我们手动进行比较运算符的重载
        
      • 实现比较方法 struct/class,注意决定排序的bool运算规则;

      • pair 与 map;

    • 注意最后从优先级队列取出的时候用参数 k 做遍历终点,不要用 size() 方法,会随队列弹出更新。

  • 代码实现:

class Solution {
public:
    // 小顶堆
    struct compare {
    public:
        // 参数是pair,比较用第二个参数--频次
        bool operator() (const pair<int, int> &l, const pair<int, int> &r) {
            return l.second > r.second;
        }
    };

    vector<int> topKFrequent(vector<int>& nums, int k) {
        // 1、统计频次
        std::unordered_map<int, int> map;
        for(int num : nums) {
            map[num]++;
        }
        // 2、创建小顶堆优先队列
        std::priority_queue<pair<int, int>, vector<pair<int, int>>, compare> priQue;
        // 3、遍历map,放入元素,同时基于小顶堆,当元素超过k时弹出队头最小元素
        for(std::unordered_map<int, int>::iterator it = map.begin(); it != map.end(); ++it) {
            // 3.1 入队
            priQue.push(*it);
            // 3.2 检查队中元素是否超过k,如是,弹出栈顶小元素
            if(priQue.size() > k) {
                priQue.pop();
            }
        }
        // 4、遍历队列取出结果,放入vector
        std::vector<int> ret;
        // for(int i = 0; i < priQue.size(); ++i) {
        for(int i = 0; i < k; ++i) {
            // 取出pair,将第一个元素放入
            ret.push_back(priQue.top().first);
            priQue.pop();
        }
        return ret;
    }
};

9.栈与队列总结

  • 栈与队列的理论基础:
    • C++中stack,queue 是容器适配器,不是容器,底层容器使用不同的容器;
  • 单调队列:保证队列里单调递增或递减,注意和优先级队列区分,单调队列不是单纯对窗口里面的数进行排序,记得其pop、push操作规则;
  • 优先级队列:本质上是披着队列外衣的堆;
    • 堆是一棵完全二叉树,树中每个结点的值都不小于(或不大于)其左右孩子的值。如果父亲结点是大于等于左右孩子就是大顶堆,小于等于左右孩子就是小顶堆。

二叉树

  • 二叉树分类:
    • 满二叉树:二叉树只有度为0的结点和度为2的结点,并且度为0的结点在同一层上;
    • 完全二叉树:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置;
      • 优先级队列其实是一个堆,堆就是一棵完全二叉树,同时保证父子节点的顺序关系;
    • 二叉搜索树:
      • 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
      • 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
      • 它的左、右子树也分别为二叉排序树。
    • 平衡二叉搜索树:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树;
      • C++中map、set、multimap、multiset的底层实现都是平衡二叉搜索树,unordered_map、unordered_set的底层实现是哈希;
  • 二叉树的存储方式:链式存储与顺序存储
    • 链式存储方式就用指针, 顺序存储的方式就是用数组;
  • 二叉树的遍历方式:
    • 深度优先遍历:
      • 前序遍历;
      • 中序遍历;
      • 后序遍历;
    • 广度优先遍历:层序遍历;

回溯算法

贪心算法

动态规划

单调栈

1、每日温度

  • 题目链接:739. 每日温度 - 力扣(LeetCode)
  • 思路:单调栈
    • 要找每个元素后面第一个比他大的元素--栈内元素栈顶到栈底单调递增;
    • 当新遍历一个元素,比较与栈顶元素的大小,当前元素大,对应栈顶元素下一个更大元素出现;
    • 持续遍历,直到将当前元素放入栈中
  • 代码实现:
// 思路:单调栈
// 要找每个元素后面第一个比他大的元素--栈内元素栈顶到栈底单调递增
// 当新遍历一个元素,比较与栈顶元素的大小,当前元素大,对应栈顶元素下一个更大元素出现
// 持续遍历,直到将当前元素放入栈中
class Solution {
public:
    vector<int> dailyTemperatures(vector<int>& temperatures) {
        // 1、创建结果变量
        vector<int> ans = vector<int>(temperatures.size());

        // 2、创建单调栈,并放入第一个元素,注意栈中元素放的是索引
        stack<int> mystack;
        mystack.push(0);
        for(int i = 1; i < temperatures.size(); ++i) {
            // 2.1 持续比较栈顶元素和当前元素
            while(!mystack.empty() && temperatures[mystack.top()] < temperatures[i]) {
                ans[mystack.top()] = i - mystack.top();
                mystack.pop();
            }
            // 2.2 当前元素入栈
            mystack.push(i);
        }

        // 3、返回结果
        return ans;
    }
};
  • 知识点:
    • 栈stack--是容器适配器而不是容器,容器适配器主要有栈适配器、队列适配器和优先队列适配器;
    • 栈可以基于deque或vector进行实现;

2、下一个更大元素Ⅰ

// 思路:
// 关键:nums1是nums2的子集,直接对nums2单调栈处理,找到每个元素的下一更大元素
// 然后遍历,看nums1是否存在对应元素
class Solution {
public:
    vector<int> nextGreaterElement(vector<int>& nums1, vector<int>& nums2) {
        // 1、先按单调栈处理nums2
        // 用map装nums2的单调栈结果,键是元素,值是下一个更大元素
        unordered_map<int, int> myMap;
        // 栈里面装索引
        stack<int> mystack;
        mystack.push(0);
        for(int i = 1; i < nums2.size(); ++i) {
            // 找下一个更大的,栈顶到栈底应当单调递增
            while(!mystack.empty() && nums2[mystack.top()] < nums2[i]) {
                // 设置值
                myMap[nums2[mystack.top()]] = nums2[i];
                // 栈顶元素出栈
                mystack.pop();
            }
            // 当前元素入栈
            mystack.push(i);
        }

        // 2、遍历nums1,看map中是否有下一个更大元素
        // 初始化结果
        vector<int> ans = vector<int>(nums1.size(), -1);
        // 遍历
        for(int i = 0; i < nums1.size(); ++i) {
            if(myMap.find(nums1[i]) != myMap.end()) {
                ans[i] = myMap[nums1[i]];
            }
        }

        return ans;
    }
};
  • 知识点:
    • 初始化为指定个数、指定值的vector:vector<T> vec(nums, val);
    • eg:std::vector<int> myVector(5, 10);

3、下一个更大元素Ⅱ

// 思路:单调栈,循环数组解决--将nums拼接两个,第一个里面对应元素都能找到下一个更大元素
class Solution {
public:
    vector<int> nextGreaterElements(vector<int>& nums) {
        // 1、数据准备
        int numsSize = nums.size();
        // 返回结果
        vector<int> ans(numsSize, -1);
        // nums拼两个
        vector<int> doubleNums(2 * numsSize);
        for(int i = 0; i < 2 * numsSize; ++i) {
            doubleNums[i] = nums[i % numsSize];
        }


        // 2、单调栈逻辑
        stack<int> st;
        // 里面放索引,这样才能对应住位置
        st.push(0);
        for(int i = 1; i < doubleNums.size(); ++i) {
            // 下一个更大,所以栈顶到栈底递增
            while(!st.empty() && doubleNums[st.top()] < doubleNums[i]) {
                // 注意索引会是ans尺寸的两倍
                ans[st.top() % numsSize] = doubleNums[i];
                st.pop();
            }
            st.push(i);
        }

        return ans;
    }
};
  • 知识点:
    • 改进点:可以不用创建出来 doubleNums,直接在遍历的时候按照2倍 nums 的 size ,然后处理好索引和实际 nums 的索引即可。

4、接雨水

  • 题目链接:
  • 思路:
  • 代码实现:

  • 知识点:

5、柱状图中最大的矩形

  • 题目链接:
  • 思路:
  • 代码实现:

  • 知识点:

图论

标签:遍历,nums,int,元素,随想录,C++,next,指针,刷题
From: https://www.cnblogs.com/gq-z/p/18550103

相关文章

  • 【C++复习】栈-下篇
    大家好,这里是不会写开场白的Yinph。今天我们先来复习一下中缀表达式、前缀表达式和后缀表达式,以及如何用栈来实现它们之间的运算。一、中缀表达式‌‌中缀表达式‌是一种算术或逻辑公式的表示方法,其中操作符位于操作数的中间。这种表示方法符合人们的日常书写习惯,因此被广泛使......
  • 【C++】static(静态)
    类外静态变量或函数意味着,当需要将这些变量或函数与实际定义的符号链接时,链接器不会在这个翻译单元的作用域之外寻找那个符号定义,即只会在这个翻译单元内部链接(文件内可用)如果这句话并不理解,可以看一下【C++】HowtheC++CompilerWorks和【C++】HowtheC++LinkerWork......
  • 代码随想录算法训练营第四十七天|Day47 单调栈
    739.每日温度https://programmercarl.com/0739.%E6%AF%8F%E6%97%A5%E6%B8%A9%E5%BA%A6.html思路int*dailyTemperatures(int*temperatures,inttemperaturesSize,int*returnSize){int*answer=(int*)malloc(temperaturesSize*sizeof(int));int*sta......
  • 代码随想录算法训练营第四十八天|Day48 单调栈
    42.接雨水https://programmercarl.com/0042.%E6%8E%A5%E9%9B%A8%E6%B0%B4.html思路inttrap(int*height,intheightSize){intans=0;intleft=0,right=heightSize-1;intleftMax=0,rightMax=0;......
  • 根据二叉树的前序和中序构建树,并按层次输出(C++)vector存树
    L2-006树的遍历#include<bits/stdc++.h>#defineintlonglongusingnamespacestd;#defineendl'\n'intpo[35];intino[35];vector<int>ans[50];intdfs(intl1,intr1,intl2,intr2){ for(inti=l2;i<=r2;i++){ if......
  • 【C++进阶实战】个人财务管理系统
    功能说明添加收入和支出记录:用户可以添加新的收入和支出记录。查看记录:用户可以查看所有的收入和支出记录。生成财务报告:用户可以生成总收入、总支出和剩余金额的报告。设定预算:用户可以设定每月的预算,并查看是否超出预算。文件存储:使用文件来存储记录,以便下次启动程序时仍然......
  • 【C++进阶实战】电子词典
    功能说明查询单词:用户可以输入一个单词,程序将显示该单词的释义。文件存储:使用文件来存储单词和释义,以便下次启动程序时仍然可用。用户界面:提供简单的命令行界面,让用户选择查询单词或退出程序。示例代码词库文件 dictionary.txt假设我们有一个词库文件dictionary.txt,内容......
  • 穿越数据迷宫:C++哈希表的奇幻旅程
    文章目录前言......
  • [C++] 智能指针
    文章目录智能指针的使用原因及场景分析为什么需要智能指针?异常抛出导致的资源泄漏问题分析智能指针与RAIIC++常用智能指针使用智能指针优化代码优化后的代码优化点分析析构函数中的异常问题解决方法RAII和智能指针的设计思路详解什么是RAII?RAII的工作原理智能......
  • 【C++】深入理解自定义 list 容器中的 list_iterator:迭代器实现详解
    个人主页:起名字真南的CSDN博客个人专栏:【数据结构初阶】......