练习题(2024/4/7)

发布于:2024-04-07 ⋅ 阅读:(153) ⋅ 点赞:(0)

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

给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。

示例 1:

输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]

示例 2:

输入:head = [1], n = 1
输出:[]

示例 3:

输入:head = [1,2], n = 1
输出:[1]

提示:

  • 链表中结点的数目为 sz
  • 1 <= sz <= 30
  • 0 <= Node.val <= 100
  • 1 <= n <= sz

1  双指针建立虚拟头节点思路:

建立虚拟头节点的目的通常是为了简化链表操作的逻辑,并且更方便地处理边界情况。在链表操作中,有时需要对链表头部进行插入、删除或者其他操作,而这些操作可能会导致需要特别处理头部指针的情况,特别是当链表为空时。使用虚拟头节点可以使得链表的头部始终存在,即使链表为空,也可以通过虚拟头节点来进行操作,这样就避免了对头指针进行特殊处理的情况,简化了代码逻辑。

虚拟头节点并不存储实际的数据,它只是一个辅助节点,用于指向链表的真实头节点,这样操作链表时就不需要对头指针是否为空进行判断。此外,虚拟头节点还可以避免对头节点进行特殊处理,从而提高代码的可读性和可维护性。

双指针的经典应用,如果要删除倒数第n个节点,让fast移动n步,然后让fast和slow同时移动,直到fast指向链表末尾。删掉slow所指向的节点就可以了。

1定义fast指针和slow指针,初始值为虚拟头结点,如图:

2fast和slow同时移动,直到fast指向末尾,如图:

3删除slow指向的下一个节点,如图:

代码:

class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        // 创建虚拟头节点
        ListNode* dummyHead = new ListNode(0);
        dummyHead->next = head; // 虚拟头节点指向真实头节点
        ListNode* slow = dummyHead; // 慢指针从虚拟头节点开始
        ListNode* fast = dummyHead; // 快指针从虚拟头节点开始
        while(n-- && fast != NULL) {
            fast = fast->next; // 快指针先走n步
        }
        fast = fast->next; // 快指针再提前走一步,因为需要让慢指针指向删除节点的上一个节点
        while (fast != NULL) {
            fast = fast->next;
            slow = slow->next; // 快慢指针同时向后移动,直到快指针到达链表末尾
        }
        slow->next = slow->next->next; // 删除第n个节点
        
        // 释放被删除节点的内存,C++释放内存的逻辑
        // ListNode *tmp = slow->next;
        // slow->next = tmp->next;
        // delete tmp;
        
        return dummyHead->next; // 返回真实头节点
    }
};

2 栈思想:

我们也可以在遍历链表的同时将所有节点依次入栈。根据栈「先进后出」的原则,我们弹出栈的第 nnn 个节点就是需要删除的节点,并且目前栈顶的节点就是待删除节点的前驱节点。这样一来,删除操作就变得十分方便了。

代码:

class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        // 创建虚拟头节点
        ListNode* dummyhead = new ListNode(0, head);
        stack<ListNode*> stk; // 使用栈来存储节点,以便找到倒数第n个节点的前一个节点
        ListNode* cur = dummy; // 当前指针从虚拟头节点开始
        while (cur) {
            stk.push(cur); // 将当前节点压入栈中
            cur = cur->next; // 移动到下一个节点
        }
        for (int i = 0; i < n; ++i) {
            stk.pop(); // 弹出前n个节点
        }
        ListNode* prev = stk.top(); // 获取倒数第n个节点的前一个节点
        prev->next = prev->next->next; // 删除第n个节点
        ListNode* ans = dummy->next; // 获取真实头节点
        delete dummy; // 释放虚拟头节点的内存
        return ans; // 返回真实头节点
    }
};

 2有效的括号

给定一个只包括 '('')''{''}''['']' 的字符串 s ,判断字符串是否有效。

有效字符串需满足:

  1. 左括号必须用相同类型的右括号闭合。
  2. 左括号必须以正确的顺序闭合。
  3. 每个右括号都有一个对应的相同类型的左括号。

示例 1:

输入:s = "()"
输出:true

示例 2:

输入:s = "()[]{}"s
输出:true

示例 3:

输入:s = "(]"
输出:false

提示:

  • 1 <= s.length <= 104
  • s 仅由括号 '()[]{}' 组成

栈思路:

先来分析一下 这里有三种不匹配的情况,

  1. 第一种情况,字符串里左方向的括号多余了 ,所以不匹配。

  1. 括号匹配1

  2. 第二种情况,括号没有多余,但是 括号的类型没有匹配上。 

    括号匹配2

  3. 第三种情况,字符串里右方向的括号多余了,所以不匹配

括号匹配3

第一种情况:已经遍历完了字符串,但是栈不为空,说明有相应的左括号没有右括号来匹配,所以return false

第二种情况:遍历字符串匹配的过程中,发现栈里没有要匹配的字符。所以return false

第三种情况:遍历字符串匹配的过程中,栈已经为空了,没有匹配的字符了,说明右括号没有找到对应的左括号return false

那么什么时候说明左括号和右括号全都匹配了呢,就是字符串遍历完之后,栈是空的,就说明全都匹配了。

代码:

class Solution {
public:
    bool isValid(string s) {
        if (s.size() % 2 != 0) return false; // 如果字符串长度为奇数,一定不符合要求
        stack<char> st; // 使用栈来匹配括号
        for (int i = 0; i < s.size(); i++) {
            if (s[i] == '(') st.push(')'); // 左括号入栈对应的右括号
            else if (s[i] == '{') st.push('}'); // 左括号入栈对应的右括号
            else if (s[i] == '[') st.push(']'); // 左括号入栈对应的右括号
            // 如果栈为空或者栈顶字符与当前字符不匹配,则返回false
// // 第三种情况:遍历字符串匹配的过程中,栈已经为空了,没有匹配的字符了,说明右括号没有找到对应的左括号 return false
// 第二种情况:遍历字符串匹配的过程中,发现栈里没有我们要匹配的字符。所以return false
            else if (st.empty() || st.top() != s[i]) return false;
            else st.pop(); // 匹配到一对括号,弹出栈顶元素
        }
 // 字符串遍历完成后,栈为空表示所有括号匹配成功,否则返回false
// 第一种情况:此时我们已经遍历完了字符串,但是栈不为空,说明有相应的左括号没有右括号来匹配,所以return false,否则就return true
        return st.empty();
    }
};

3合并两个有序链表

将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。 

示例 1:

输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]

示例 2:

输入:l1 = [], l2 = []
输出:[]

示例 3:

输入:l1 = [], l2 = [0]
输出:[0]

提示:

  • 两个链表的节点数目范围是 [0, 50]
  • -100 <= Node.val <= 100
  • l1 和 l2 均按 非递减顺序 排列

  • 递归思路方法
    • 首先处理边界情况,即其中一个链表为空时,直接返回另一个链表。
    • 接着比较两个链表的头节点,将值较小的节点作为合并后链表的头节点。
    • 然后递归地将较小节点的下一个节点与另一个链表进行合并,直到其中一个链表为空。
    • 最后返回合并后的头节点即可。

代码:

class Solution {
public:
    ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
        if(list1 ==nullptr){ // 如果list1为空,直接返回list2
            return list2;
        }
        if(list2==nullptr){ // 如果list2为空,直接返回list1
            return list1;
        }
        if(list1->val <=list2->val){ // 如果list1的值小于等于list2的值
            list1->next=mergeTwoLists(list1->next,list2); // 递归地将list1的下一个节点与list2合并
            return list1; // 返回合并后的list1
        }
        // 如果list2的值小于list1的值或者list1为空,则将list1与list2的下一个节点进行合并
        list2->next=mergeTwoLists(list1,list2->next);
        return list2; // 返回合并后的list2
    }
};

4 寻找用户推荐人

表: Customer

+-------------+---------+
| Column Name | Type    |
+-------------+---------+
| id          | int     |
| name        | varchar |
| referee_id  | int     |
+-------------+---------+
在 SQL 中,id 是该表的主键列。
该表的每一行表示一个客户的 id、姓名以及推荐他们的客户的 id。

找出那些 没有被 id = 2 的客户 推荐 的客户的姓名。

以 任意顺序 返回结果表。

结果格式如下所示。

示例 1:

输入: 
Customer 表:
+----+------+------------+
| id | name | referee_id |
+----+------+------------+
| 1  | Will | null       |
| 2  | Jane | null       |
| 3  | Alex | 2          |
| 4  | Bill | null       |
| 5  | Zack | 1          |
| 6  | Mark | 2          |
+----+------+------------+
输出:
+------+
| name |
+------+
| Will |
| Jane |
| Bill |
| Zack |
+------+

思路

  1. 使用子查询

    • 使用子查询来查找目标用户的推荐人ID。
    • 将子查询结果作为条件,从"Customer"表中选择推荐人的信息。

代码:


select name
from Customer 
where referee_id  is null or  referee_id !=2;

 5. 2016年的投资

Insurance 表:

+-------------+-------+
| Column Name | Type  |
+-------------+-------+
| pid         | int   |
| tiv_2015    | float |
| tiv_2016    | float |
| lat         | float |
| lon         | float |
+-------------+-------+
pid 是这张表的主键(具有唯一值的列)。
表中的每一行都包含一条保险信息,其中:
pid 是投保人的投保编号。
tiv_2015 是该投保人在 2015 年的总投保金额,tiv_2016 是该投保人在 2016 年的总投保金额。
lat 是投保人所在城市的纬度。题目数据确保 lat 不为空。
lon 是投保人所在城市的经度。题目数据确保 lon 不为空。

编写解决方案报告 2016 年 (tiv_2016) 所有满足下述条件的投保人的投保金额之和:

  • 他在 2015 年的投保额 (tiv_2015) 至少跟一个其他投保人在 2015 年的投保额相同。
  • 他所在的城市必须与其他投保人都不同(也就是说 (lat, lon) 不能跟其他任何一个投保人完全相同)。

tiv_2016 四舍五入的 两位小数 。

查询结果格式如下例所示。

示例 1:

输入:
Insurance 表:
+-----+----------+----------+-----+-----+
| pid | tiv_2015 | tiv_2016 | lat | lon |
+-----+----------+----------+-----+-----+
| 1   | 10       | 5        | 10  | 10  |
| 2   | 20       | 20       | 20  | 20  |
| 3   | 10       | 30       | 20  | 20  |
| 4   | 10       | 40       | 40  | 40  |
+-----+----------+----------+-----+-----+
输出:
+----------+
| tiv_2016 |
+----------+
| 45.00    |
+----------+
解释:
表中的第一条记录和最后一条记录都满足两个条件。
tiv_2015 值为 10 与第三条和第四条记录相同,且其位置是唯一的。

第二条记录不符合任何一个条件。其 tiv_2015 与其他投保人不同,并且位置与第三条记录相同,这也导致了第三条记录不符合题目要求。
因此,结果是第一条记录和最后一条记录的 tiv_2016 之和,即 45 。

思路:

  1. 首先,我们需要找到满足条件一的投保人,也就是在 2015 年的投保额至少跟另一个投保人的投保额相同的投保人。
  2. 其次,我们需要找到满足条件二的投保人,也就是他们所在的城市必须与其他投保人都不同,即在纬度和经度上都没有完全相同的。
  3. 然后,我们将这两个条件组合起来,在满足这两个条件的投保人中,计算他们在 2016 年的投保金额之和,并对结果进行四舍五入,保留两位小数。

代码:

-- 查询满足条件的投保人的投保金额之和
select round(sum(tiv_2016), 2) as tiv_2016
from Insurance
-- 条件1: 他在 2015 年的投保额 (tiv_2015) 至少跟一个其他投保人在 2015 年的投保额相同
where tiv_2015 in (
    -- 子查询1:找到在 2015 年有重复投保额的投保人
    select tiv_2015
    from Insurance
    group by tiv_2015
    having count(*) > 1
)
-- 条件2: 他所在的城市必须与其他投保人都不同(也就是说 (lat, lon) 不能跟其他任何一个投保人完全相同)
and (lat, lon) not in (
    -- 子查询2:找到在 (lat, lon) 上有重复值的投保人
    select lat, lon
    from Insurance
    group by lat, lon
    having count(*) > 1
);

6订单最多的客户

表: Orders

+-----------------+----------+
| Column Name     | Type     |
+-----------------+----------+
| order_number    | int      |
| customer_number | int      |
+-----------------+----------+
在 SQL 中,Order_number是该表的主键。
此表包含关于订单ID和客户ID的信息。

查找下了 最多订单 的客户的 customer_number 。

测试用例生成后, 恰好有一个客户 比任何其他客户下了更多的订单。

查询结果格式如下所示。

示例 1:

输入: 
Orders 表:
+--------------+-----------------+
| order_number | customer_number |
+--------------+-----------------+
| 1            | 1               |
| 2            | 2               |
| 3            | 3               |
| 4            | 3               |
+--------------+-----------------+
输出: 
+-----------------+
| customer_number |
+-----------------+
| 3               |
+-----------------+
解释: 
customer_number 为 '3' 的顾客有两个订单,比顾客 '1' 或者 '2' 都要多,因为他们只有一个订单。
所以结果是该顾客的 customer_number ,也就是 3 。

思路:

  1. 首先,我们需要按照客户编号将订单表进行分组,这样每个客户的订单都会被分到相应的组中。
  2. 然后,我们对每个客户的订单数量进行统计,使用 count(*) 函数来计算每个客户的订单数量。
  3. 接下来,我们按照订单数量降序排序,这样订单数量最多的客户会排在前面。
  4. 最后,我们使用 limit 1 语句来限制结果集只返回第一个结果,即订单数量最多的客户的客户编号。

代码:


select customer_number
from Orders
group by customer_number
order by count(*) desc
limit 1;