记录找工作过程中的刷题笔记

链表

定义

    
struct  ListNode
{
    int val;
    ListNode* next;
    ListNode(int x) :val(x), next(nullptr);
};
​
ListNode* createLinkedList(const std::vector<int>& values) {
    if (values.empty()) {
        return nullptr;
    }
​
    ListNode* head = new ListNode(values[0]);
    ListNode* current = head;
​
    for (size_t i = 1; i < values.size(); ++i) {
        current->next = new ListNode(values[i]);
        current = current->next;
    }
​
    return head;
}
void printLinkedList(ListNode* head) {
    ListNode* current = head;
    while (current != nullptr) {
        std::cout << current->val << " ";
        current = current->next;
    }
    std::cout << std::endl;
}
​

203. 移除链表元素

细节

  • while 中的中止条件及区别,若要处理最后的节点应用while (cur != nullptr),此时cur会停留在最后一个节点上

  • 使用 cur != nullptr 作为终止条件,循环会在当前节点为 nullptr 时终止

  • while (cur ->next != nullptr)会停留在最后一个节点上因此不会处理最后一个节点

方法

  • 很简单,注意应在cur->next->val == val跳过就行

  • 卧槽,写错了 ,由于判断的是if(cur->next->val == val)的值因此while (cur ->next != nullptr)

  • 会停留在最后一个,但在上一个时,其已经判断了最后一个值是否为val,所以处理了最后一个节点

  • 为避免连续出现val , 应用if - else 而不是无脑 cur = cur->next;

           while (cur->next != NULL) {
                if(cur->next->val == val) {
                    ListNode* tmp = cur->next;
                    cur->next = cur->next->next;
                    delete tmp;
                } else {
                    cur = cur->next;
                }
            }

707. 设计链表

细节

  • 私有变量,虚拟头节点(不能是头节点!!!)以及链表的长度

  • MyLinkedList(),功能为初始化两个私有变量

  • get 函数中,index和数组一样第0个指的是 头节点!因此index的越界判定为if (index >= _size || index < 0 )

方法

  • 挨着挨着写,注意节点的释放!要养成习惯

  • 关注index越界判定 应该是 >= 还是>

206. 反转链表

细节

  • 可以新建一个链表用前插法来反转,我一直这样写的,这样空间复杂度较高

  • 直接反转 ,需要用一个变量存储 head —>next

  • 反转是一前一后俩指针进行反转

  • 中止条件为head != nullptr ,原因是要处理最后一个节点

  • pre指针一开始的赋值为ListNode* pre = NULL;这里不是虚拟头节点很关键,因为虚拟头节点值为0,会导致反转后的链表多出一个0

方法

  while (head != nullptr) {
            tem = head->next;
            head->next = pre;
            pre = head;
            head = tem;
        }

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

细节

  • 要求时间发复杂度O(n),空间复杂度为O(1)

  • 用三个节点进行前中后的操作进行交换,同时用一个节点去记录pre 后面的节点

  • 分两个步骤: 交换、前移

  • 循环不断前移,注意tem 记录的是pre 下一个节点,因此其为空时只需进行交换,而不用再往前移动

  • 会有两种情况:奇数 和 偶数个节点 ,奇数个节点会因为 pre == nullptr 而终止,

  • 而偶数个节点则tem 先为空 此时只需进行交换,而不用再往前移动

19. 删除链表的倒数第 N 个结点

细节

  • 最初想法:先计数然后在便利,时间复杂度O(n),但空间复杂度更大

  • 实际做法:快慢指针,pre 从虚拟头节点移动n个,此时 last 会停在 pre 前 n + 1 个 ,便于删除

  • 执行删除操作! over

面试题 02.07. 链表相交

细节

  • 相交的节点不是值相等而应该判断指针是否相等

  • 相同长度两个链表如何判断是否有相交节点? 从起点开始同时向前移,判断if (curA == curB)return curA;

  • 那对于不同长度的链表,则需要计算链表的长度,然后移动较长的链表至较短链表的头部

  • 也就是说将不同长度的链表转换为相同长度的链表后,进行判断

    • tips:通过以下代码交换,可简化代码,因为A始终为长的链表

              if (lenA < lenB) {
                  swap(lenA, lenB);
                  swap(curA, curB);
              }

142. 环形链表 II

思路

  • 快慢指针,快指针走两步,慢指针走一步,若是存在环则一定会遇见

  • 不存在环的判定条件为:while (fast != nullptr &&fast -> next != nullptr ),因为走两步,所以两个都要判断

  • 若停在最后一个节点,那么fast -> next ->next 就无法访问

  • 相遇后,slow 的路程为 t fast 路程为 2t ,那么环长度为 t

  • slow回原点和fast同时出发,一定会在环的入口相遇!

哈希表

常见的三种哈希结构

当我们想使用哈希法来解决问题的时候,我们一般会选择如下三种数据结构。

  • 数组

  • set (集合)

  • map(映射)

这里数组就没啥可说的了,我们来看一下set。

在C++中,set 和 map 分别提供以下三种数据结构,其底层实现以及优劣如下表所示:

集合底层实现是否有序数值是否可以重复能否更改数值查询效率增删效率
std::set红黑树有序O(log n)O(log n)
std::multiset红黑树有序O(logn)O(logn)
std::unordered_set哈希表无序O(1)O(1)

std::unordered_set底层实现为哈希表,std::set 和std::multiset 的底层实现是红黑树,红黑树是一种平衡二叉搜索树,所以key值是有序的,但key不可以修改,改动key值会导致整棵树的错乱,所以只能删除和增加。

映射底层实现是否有序数值是否可以重复能否更改数值查询效率增删效率
std::map红黑树key有序key不可重复key不可修改O(logn)O(logn)
std::multimap红黑树key有序key可重复key不可修改O(log n)O(log n)
std::unordered_map哈希表key无序key不可重复key不可修改O(1)O(1)

相关注意事项

  • 底层实现为红黑树的键值是有序不可修改,改动key值会导致整棵树的错乱,所以只能删除和增加。

242. 有效的字母异位词

细节

  • 可以通过数组模拟哈希表,26位刚好,对应位加一进行记录

  • 第二个字符串的时候对应位减一

  • 判断是否为全0,全0return true

349. 两个数组的交集

细节

  • 去重用unordered_set,同时可以快速插入:unordered_set<int> mySet(nums1.begin(),nums1.end());

  • unordered_set是一个无序表,不能存储重复的值,具有去重的作用。

  • 导入mySet后,与nums2进行对比,若出现相同的值就存入unordered_set<int> result

  • 最终return的值为一个数组,将其转换为数组即可return vector<int>(result.begin(), result.end());

202. 快乐数

细节

  • 题目内容 : 无限循环 但始终变不到 1,也就是说会出现重复的值,因此用哈希法

  • while(1)进行循环,循环中干 3 件事情

    • 计算每位数平方的和,也就是sum

    • sum == 1 直接返回

    • 先 在哈希表中查找是否有这个值 ,如果有证明开始无限循环了 ,return false 否则就将其insert

    • n = sum ; sum = 0;

1. 两数之和

细节

  • 由于需要存储下标,因此不能采用set 结构体 ,map 同时存储值和下标

  • 因此使用map进行存储,存储target - nums[i]的对应值

  • 先判断if (record.find(target - nums[i]) != record.end()),如果满足条件 return vector<int>{i, record.at(target - nums[i]) }

Map 相关操作

  • 具体值的设定: Map[key] = val;

  • 访问对饮key 的值 val = Map.at(key);

  • 判断键值是否存在:Map.count(key),如果值为1则存在,值为0则不存在;

  • 也可用record.find(target - nums[i]) != record.end(),注意find 返回的是一个迭代器

  • 变量则使用迭代器进行遍历,auto iter = map.find(target - nums[i]);

  • 获取map的大小:size_t mapSize = myMap.size();

迭代器

迭代器是一种用于遍历容器(如数组、链表、哈希表等)中元素的对象。

在C++中,迭代器提供了一种统一的方式来访问容器中的元素,无需关心容器的内部实现细节。

454. 四数相加 II

思路

  • 哈希表存储前两个数组的和,记录出现的次数

  • 次数的用处:遍历后两个数组和时,查找记录的哈希表中是否存在的同时! 所记录的次数就是当前和与前面数组的和为0 的总次数

  • 用count 记录

class Solution {
public:
    int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
        unordered_map<int ,int > myMap;
        int length = nums1.size();
    
        int count = 0;
        for (int i = 0; i < length; i++)
        {   
            for (int j = 0; j < length; j++)
            {
                int sum = 0;
                sum = nums1[i] + nums2[j];
                myMap[sum]++;
            }
        }
        for (int i = 0; i < length; i++)
        {
            for (int j = 0; j < length; j++) {
                int sum = 0;
                sum = nums3[i] + nums4[j];
                if (myMap.find(0 - sum) != myMap.end() ) {
                    count+=myMap[0 -sum];                   
                    }
​
                }
                
           
        }
        return count;
    }
};

383. 赎金信

二叉树

递归遍历

方法论:

  • 关注前中后,指的是访问根节点的顺序,同时得知道两个顺序才能还原二叉树

  • 无非是动作的排列组合,具体实现如下(中序为例子)

    1. 判断是否为空,很关键,if (cur == NULL) return; 无论如何都要先判断截止条件

    2. 左 :递归执行 traversal(cur->left, vec);

    3. 中: vec.push_back(cur->val);

    4. 右: traversal(cur->right, vec); // 右

    class Solution {
    public:
        void traversal(TreeNode* cur, vector<int>& vec) {
            if (cur == NULL) return;
    ​
            traversal(cur->left, vec);  // 左
            vec.push_back(cur->val);    // 中
            traversal(cur->right, vec); // 右
        }
        vector<int> inorderTraversal(TreeNode* root) {
            vector<int> result;
            traversal(root, result);
            return result;
        }
    };

迭代遍历(非递归)

前序细节

  • 用栈模拟递归前序遍历

  • 先放根节点 ,然后先放右节点 后放左节点

  • 然后依次弹出

  • 弹出时需要判断if (tem != nullptr)result.push_back(tem->val);若为空就 continue;

    class Solution {
    public:
        vector<int> preorderTraversal(TreeNode* root) {
            vector<int> result;
            TreeNode* tem;
            stack<TreeNode*> st;
            st.push(root);
            while (!st.empty()) {
                tem = st.top();
                st.pop();
                if (tem != nullptr)result.push_back(tem->val);
                else continue;
                st.push(tem->right);
                st.push(tem->left);
    ​
            }
            return result;
        }
    };

后序细节

  • 后续顺序为 左右中 ,因此可以根据前序遍历的数据,先产生中右左的数据 ,然后reverse(result );

中序细节

  • 访问顺序和处理顺序不同

  • 先一直向左访问,并且用栈存储访问过的节点,直到叶子节点

  • 若当前节点为空就从栈中取元素,然后访问处理右节点

  • key:左为空的话,就弹出该节点,右为空则说明是叶子节点,弹出该节点的父节点

    class Solution {
    public:
        vector<int> inorderTraversal(TreeNode* root) {
            vector<int> res;
            stack<TreeNode*>st; 
            TreeNode *cur = root;
            while(cur != nullptr || !st.empty()){
                if(cur != nullptr){
                    st.push(cur);
                    cur = cur ->left;
    ​
                }else{
                    cur = st.top();
                    st.pop();
                    res.push_back(cur ->val);
                    cur = cur ->right;
                }
            }
            return res;
        }
    };

迭代遍历统一写法(看不懂)

层序遍历

  • 队列实现层序遍历将每层的内容存在队列中,并且记录队列长度size

  • 上一次队列的长度决定弹出多少,弹出的同时将所弹出的节点的子节点压入队列中

  • 用一个数组记录 每层弹出的内容

  • 每层遍历完,压入二维数组中

class Solution {
public:
    vector<vector<int>> levelOrder(TreeNode* root) {
    vector<vector<int>> res ;
    queue<TreeNode* > myque;
​
    if(root != nullptr) myque.push(root);
    while(!myque.empty()){
        int size = myque.size();
        vector <int> tem ;
        while(size -- ){
           
            TreeNode *node = myque.front();
            myque.pop();
            tem.push_back(node ->val);
            if(node ->left) myque.push(node ->left);
            if(node ->right) myque.push(node ->right);
        }
        res.push_back(tem);
    }
    return res;
    }
};

199. 二叉树的右视图

429. N 叉树的层序遍历

  • 和二叉树的层序遍历一样,但要关注如何访问子节点的问题

  • for(Node* child : node->children) 为访问所有子节点

  • 因此只需要把层序遍历的先左后右 换成for(Node* child : node->children)myque.push(child);

515. 在每个树行中找最大值

  • 依旧是层序遍历

  • 注意需要记录每层的最大值,因此遍历过程中需要更新最大值,而值一开始设定为INT_MIN

  • 方便更新

116. 填充每个节点的下一个右侧节点指针

104. 二叉树的最大深度

111. 二叉树的最小深度

  • 以上三题均是层序遍历,至于深度应该可以用深度优先遍历的方式但还是采用广度优先遍历

对称二叉树

  • 运用递归判断,从root出发,对比两个子树的内外值是否相等

  • 递归三部曲,先明确终止条件的书写,然后写递归步骤,然后return

104. 二叉树的最大深度

  • 不用层序遍历采用递归的办法

  • 终止条件:root 为空时终止,也就是传入的指针为空时停止

  • 递归操作:判断左子树的最大深度,判断又子树的最大深度

  • 最终返回:左子树和右子树深度的最大值 + 1;

class solution {
public:
    int getdepth(TreeNode* node) {
        if (node == NULL) return 0;
        int leftdepth = getdepth(node->left);       // 左
        int rightdepth = getdepth(node->right);     // 右
        int depth = 1 + max(leftdepth, rightdepth); // 中
        return depth;
    }
    int maxDepth(TreeNode* root) {
        return getdepth(root);
    }
};

111. 二叉树的最小深度

  • 同样采用递归的方式

  • 刚开始会以为和最大一样:但是!!!对于以下二叉树不能是深度为1,因此终止条件需要调整

    image-20230825162050663

  • 具体终止条件如下,思路和求取最大深度相同。

class Solution {
public:
    int minDepth(TreeNode* root) {
        if(!root ) return 0;
        if(root ->left == nullptr && root -> right != nullptr){
            return minDepth(root -> right ) + 1;
        }
        if(root ->right == nullptr && root -> left != nullptr){
            return minDepth(root -> left ) + 1;
        }
        int right = minDepth(root -> right) ;
        int left = minDepth(root -> left) ;
        return min(left , right) + 1 ;
    }
};
​

222. 完全二叉树的节点个数

  • 先求左子树个数再求右子树个数 ,和最大深度一样

  • 确定中止条件 if(root == null) return 0 ;

  • left = countNodes(root ->left)

  • right= countNodes(root ->right)

  • return right + left + 1

110. 平衡二叉树

  • 和最大深度类似,重要还是要取出左右子树的高度

  • 左右子树高度做差,大于1 则返回 -1 ,否则返回该子树的最大深度 方便上层继续判断

  • 为什么要返回该子树的最大深度,因为先计算的左边整个子树的深度再右边

  • 所以一定要记录,不然在计算右边整个子树时就无法判断了

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值