《剑指offer》题目 C++详细题解

news2024/11/26 6:22:52

JZ15 二进制中1的个数

核心考点:二进制计算

思路一:使用一个循环,因为我们知道整型变量只有32位,所以循环结束的条件就是到32,从最低位开始,逐位检查数字 n 的二进制表示,利用位运算中的与运算,将当前位移到最低位,然后与 1 进行按位与运算,判断结果是否为 1,如果当前位为 1,则计数器 count 加 1,循环结束后,计数器 count 中保存的就是数字 n 的二进制表示中 1 的个数。

class Solution {
public:
    int NumberOf1(int n) {
       int count = 0;
       int i = 0;
       while(i < 32)
       {
          if((n >> i) & 1 == 1) // 使用按位与进行比较
             count++;
          i++;
       }
       return count;
    }
};

但是上面的算法的时间复杂度是O(N)的,有没有更简单更便捷的算法呢?

思路二:使用 while 循环,只要 n 不为 0,就继续循环。循环中,执行 n &= (n - 1) 操作。该操作利用了二进制数中 1 的性质:将一个数减 1,然后与原数进行按位与运算,可以将最右边的一个 1 变成 0,每次执行 n &= (n - 1) 操作后,n 中的 1 的个数减少 1,因此代码将计数器 count 加 1,循环结束后,计数器 count 中保存的就是 n 的二进制表示中 1 的个数,代码返回 count。

class Solution {
public:
    int NumberOf1(int n) {
       int count = 0;
       while(n)
       {
           n &= (n - 1);
           count++;
       }
       return count;
    }
};

这种方法比直接遍历二进制位效率更高,因为每次循环都只进行一次位运算,可以更快地将 n 中的 1 个数进行统计。

JZ22 链表中倒数最后k个结点

核心考点:链表,前后指针的使用,边界条件的检查

由于题目中明确了这是一个单链表,所以我们不可以从后向前遍历找到最后k个节点,所以此时我们可以使用前后双指针的思路,前指针先走k步,随后前后两个指针同时向后走,当前指针到达结尾的时候,后指针所在的位置就是倒数最后k个节点 。

/**
 * struct ListNode {
 *	int val;
 *	struct ListNode *next;
 *	ListNode(int x) : val(x), next(nullptr) {}
 * };
 */
class Solution {
public:
    ListNode* FindKthToTail(ListNode* pHead, int k) {
        ListNode* fast = pHead;
        ListNode* slow = pHead;
        // 前指针先走k步
        while(k--)
        {
            if(fast == nullptr)
                return nullptr;
            fast = fast->next;
        } 
        // 前后指针同时走
        while(fast)
        {
            fast = fast->next;
            slow = slow->next;
        }
        return slow;
    }
};

JZ24 反转链表

核心考点:链表操作,思维缜密程度

这个题目的做法有很多种,我们依次来写一下。

思路一:定义三个指针,整体右移,边移动边翻转,保证不会断链

/**
 * struct ListNode {
 *	int val;
 *	struct ListNode *next;
 *	ListNode(int x) : val(x), next(nullptr) {}
 * };
 */
class Solution {
public:
    ListNode* ReverseList(ListNode* head) {
        if(!head || !head->next)
            return head;
        // 至少两个节点
        ListNode* n1, *n2, *n3;
        n1 = head;
        n2 = head->next;
        n3 = head->next->next; // 可能为空
        
        while(n3 != nullptr)
        {
            n2->next = n1; // 翻转
            // 向后移动
            n1 = n2;
            n2 = n3;
            n3 = n3->next; // 不断链
        }

        // 走到这里n3为空
        n2->next = n1;
        head->next = nullptr;
        return n2;
    }
};

思路二:采用头插的方法进行翻转,此时拿原始链表的时候注意不要断链

/**
 * struct ListNode {
 *	int val;
 *	struct ListNode *next;
 *	ListNode(int x) : val(x), next(nullptr) {}
 * };
 */
class Solution {
public:
    ListNode* ReverseList(ListNode* head) {
        if(!head || !head->next)
            return head;
        // 至少两个节点
        
        ListNode* newhead = nullptr;
        ListNode* cur = head;
        while(cur)
        {
            // 把原链表的第一个节点拿下来
            ListNode* tmp = cur;
            cur = cur->next;
            // 头插
            tmp->next = newhead;
            newhead = tmp;
        }
        return newhead;
    }
};

思路三:使用递归来解决,过程中出现重复子问题

如果我们先逆序前两个节点,会出现什么问题呢?

我们会发现此时会出现断链的情况,会把节点值为3的节点丢失,从而找不到后面的节点,会造成内存泄漏的问题,所以先直接修改前两个是不合理的,所以我们需要换一种思路,以一种宏观的思路,相信dfs一定可以能帮我们完成的逆序的任务,给dfs一个头节点,dfs帮我完成逆序,返回逆序之后的头节点,那么此时就有:

此时刚好就完成了我们的逆序工作,如果上面的思路不好理解,我们可以把链表当作一棵树,只不过此时是一个单边树,此时我们对这棵树仅需进行以此后序遍历即可

/**
 * struct ListNode {
 *	int val;
 *	struct ListNode *next;
 *	ListNode(int x) : val(x), next(nullptr) {}
 * };
 */
class Solution {
public:
    ListNode* ReverseList(ListNode* head) {
        if (head == nullptr || head->next == nullptr) 
            return head;
        ListNode* newhead = ReverseList(head->next);
        
        head->next->next = head;
        head->next = nullptr;

        // 返回逆序之后的头节点
        return newhead;
    }
};

JZ25 合并两个排序的链表

核心考点:链表合并

对于这道题,解题的思路有很多种,我们可以一个一个节点的归并,当然,也可以采用递归完成。

思路一:通过循环遍历两个有序链表,每次选择较小的节点插入到新的链表,最终得到一个合并后的有序链表。

/**
 * struct ListNode {
 *	int val;
 *	struct ListNode *next;
 *	ListNode(int x) : val(x), next(nullptr) {}
 * };
 */
class Solution {
public:
    ListNode* Merge(ListNode* pHead1, ListNode* pHead2) {
        if(pHead1 == nullptr)
            return pHead2;
        if(pHead2 == nullptr)
            return pHead1;
        
        ListNode* cur1 = pHead1;
        ListNode* cur2 = pHead2;
        // 标明新链表的头尾节点,闭区间
        ListNode* newhead = nullptr;
        ListNode* tail = nullptr;
        while(cur1 && cur2)
        {
            // 选出较小的节点
            ListNode* tmp = nullptr;
            if(cur1->val >= cur2->val)
            {
                tmp = cur2;
                cur2 = cur2->next;
            }
            else
            {
                tmp = cur1;
                cur1 = cur1->next;
            }
            // 尾插到新链表
            if(newhead == nullptr)
            {
                newhead = tail = tmp;
            }
            else
            {
                tail->next = tmp;
                tail = tail->next;
            }
        }
        // 合并后,cur1或者cur2可能不为空,此时直接链接即可
        if(cur1)
            tail->next = cur1;
        if(cur2)
            tail->next = cur2;
        return newhead;
    }
};

思路二:通过递归调用 Merge 函数,每次将两个链表首节点进行比较,并将较小的节点作为合并后的新链表的头节点,并将较小的节点的 next 指针指向递归调用 Merge 函数的结果,最终得到一个合并后的有序链表。

/**
 * struct ListNode {
 *	int val;
 *	struct ListNode *next;
 *	ListNode(int x) : val(x), next(nullptr) {}
 * };
 */
class Solution {
public:
    ListNode* Merge(ListNode* pHead1, ListNode* pHead2) {
        if(pHead1 == nullptr)
            return pHead2;
        if(pHead2 == nullptr)
            return pHead1;
        
        ListNode* head = nullptr;

        // 1.找到较小的节点,head
        // 并从原链表中删除该节点
        if(pHead1->val < pHead2->val)
        {
            head = pHead1;
            pHead1 = pHead1->next;
        }
        else
        {
            head = pHead2;
            pHead2 = pHead2->next;
        }
        //2.出现子问题,此时变少了一个节点
        // 但是此时依然是合并链表
        head->next = Merge(pHead1, pHead2);

        return head;
    }
};

想清楚上面的方法,此时我们的代码就能更优化一点。

/**
 * struct ListNode {
 *	int val;
 *	struct ListNode *next;
 *	ListNode(int x) : val(x), next(nullptr) {}
 * };
 */
class Solution {
public:
    ListNode* Merge(ListNode* pHead1, ListNode* pHead2) {
        if(pHead1 == nullptr)
            return pHead2;
        if(pHead2 == nullptr)
            return pHead1;
        
        if(pHead1->val < pHead2->val)
        {
            pHead1->next = Merge(pHead1->next, pHead2);
            return pHead1;
        }
        else
        {
            pHead2->next = Merge(pHead1, pHead2->next);
            return pHead2;
        }
    }
};

JZ26 树的子结构

核心考点:二叉树理解,二叉树遍历

解题思路:二叉树都是递归定义的,所以递归操作是比较常见的做法,首先明白子结构怎么理解,可以理解成子结构是原树的子树(或者一部分),也就是说,B要是A的子结构,B的根节点+左子树+右子树,都在A中存在且构成树形结构,所以我们比较的过程要分为两步

  • 1.先确定起始位置
  • 2.在确定从该位置开始,后续的左右子树的内容是否一致
/*
struct TreeNode {
    int val;
    struct TreeNode *left;
    struct TreeNode *right;
    TreeNode(int x) :
            val(x), left(NULL), right(NULL) {
    }
};*/
class Solution {
  private:
    bool IsSameTree(TreeNode* root1, TreeNode* root2) {
        if (!root2) // 子树已经遍历完毕
            return true;
        if (!root1) // 母树已经遍历完毕
            return false;

        if (root1->val != root2->val)
            return false;

        // 此位置处根节点值相等
       	bool left = IsSameTree(root1->left, root2->left);
        bool right = IsSameTree(root1->right, root2->right);
        return left && right;
    }

  public:
    bool HasSubtree(TreeNode* pRoot1, TreeNode* pRoot2) {
        if (pRoot1 == nullptr || pRoot2 == nullptr)
            return false;

        bool result = false;
        // 先寻找起始位置
        if (pRoot1->val == pRoot2->val) {
            // 判断两棵树是否相同
            result = IsSameTree(pRoot1, pRoot2);
        }
        // 左子树和pRoot2比较
        if (result == false) {
            result = HasSubtree(pRoot1->left, pRoot2);
        }
        // 右子树和pRoot2比较
        if (result == false) {
            result = HasSubtree(pRoot1->right, pRoot2);
        }
        return result;
    }
};

JZ27 二叉树的镜像

核心考点:二叉树操作

这个题目我们仔细观察可以发现,所谓的二叉树镜像本质是自底向上进行左右左右子树交换的过程,既然是自底向上,那就说明这个题目的思想是后序遍历,这样这个题目就比较好写啦

class Solution {
public:
    TreeNode* Mirror(TreeNode* pRoot) {
        if(!pRoot)
            return pRoot;
        if(!pRoot->left && !pRoot->right)
            return pRoot;
        
        if(pRoot->left)
            Mirror(pRoot->left);
        if(pRoot->right)
            Mirror(pRoot->right);

        TreeNode* tmp = pRoot->left;
        pRoot->left = pRoot->right;
        pRoot->right = tmp;
        
        return pRoot;
    }
};

JZ76 删除链表中重复的结点

核心操作:链表操作,临界条件检查,特殊情况处理

解题思路:通过快慢指针的方式限定范围,从而达到去重的效果,这里需要注意处理全部相同,全部不同和部分相同的三种情况的方式。

/*
struct ListNode {
    int val;
    struct ListNode *next;
    ListNode(int x) :
        val(x), next(NULL) {
    }
};
*/
class Solution {
public:
    ListNode* deleteDuplication(ListNode* pHead) {
        if(pHead == nullptr)
            return pHead;
        
        // 申请带头结点
        ListNode* head = new ListNode(0);
        head->next = pHead;

        // 定义重复区域的区间,左开右闭
        ListNode* prev = head;
        ListNode* tail = head->next;
        while(tail != nullptr)
        {
            // 1.先确定重复区域的起始位置
            while(tail->next != nullptr && tail->val != tail->next->val)
            {
                prev = tail;
                tail = tail->next;
            }
            // 2.确定重复区域的结束位置
            while(tail->next != nullptr && tail->val == tail->next->val)
            {
                tail = tail->next;
            }
            // 走到这里有三种情况
            // 1.tail->next为空, 有重复区间
            // 2.tail->next不为空, 有重复区间
            // 3.链表没有重复结点,没有重复区间
            if (prev->next != tail)
            {
                prev->next = tail->next;  
            }
            tail = tail->next;
            
        }
        ListNode* next = head->next;
        delete head;
        return next;
    }
};

JZ30 包含min函数的栈

核心考点:栈的规则性设计

解题思路:很容易想到,在栈内部保存min变量,每次更新的时候,都对min变量进行更新。//但是,面试官很容易就会问到:如果想拿出第二小,第三小的值怎么拿?用上面的办法就不行了,为了满足通用,我们使用一个辅助栈,内部保存元素的个数和数据栈完全一样,不过,辅助栈内部永远保存本次入栈的数为所有数据的最小值(注意:辅助栈内部元素可能会出现“必要性”重复),我们这里是为了实现算法,所以就不从0开始实现stack了,我们直接使用c++库里面实现的栈就好,题面说了,保证测试中不会当栈为空的时候,对栈调用pop()或者min()或者top()方法,所以,后面的代码对空的检验可有可无。

注意:最小栈的元素个数和数据栈的个数相同,最小栈的栈顶永远保存着当前个数的数据栈中的最小值,其中,最小栈可能出现重复的数组。

class Solution {
public:
    void push(int value) {
        st.push(value);
        if(minst.empty() || value < minst.top())
            minst.push(value); // 插入小值
        else
            minst.push(minst.top());  // 插入栈顶的值
    }
    void pop() {
        st.pop();
        minst.pop();
    }
    int top() {
        return st.top();
    }
    int min() {
        return minst.top();
    }
    stack<int> st;  // 数据栈
    stack<int> minst; // 最小栈
};

JZ31 栈的压入、弹出序列

核心考点:栈的理解

思路:将入栈序列的元素压入辅助栈。检查辅助栈的栈顶元素是否等于出栈序列的元素,如果相等,则执行出栈操作,继续检查直到栈顶元素不再等于出栈序列的元素或者栈为空。当入栈序列中的所有元素都被处理完后,检查出栈序列是否也到达了末尾。如果出栈序列也到达了末尾,说明所有元素都按照正确的顺序弹出了栈,因此此时是一个有效的弹出序列;反之则不是。

class Solution {
public:
    bool IsPopOrder(vector<int>& pushV, vector<int>& popV) {
        stack<int> st;
        int curpush = 0, curpop = 0;
        while(curpush < pushV.size())
        {
            // 入栈序列直接入栈
            st.push(pushV[curpush++]);
            // 出栈序列和栈顶元素相同,就一直出栈
            while(!st.empty() && st.top() == popV[curpop])
            {
                st.pop();
                curpop++;
            }
        }
        return curpop == pushV.size() ? true : false;
    }
};

JZ32 从上往下打印二叉树

核心考点:二叉树层序遍历

这个题目本质是二叉树的层序遍历,我们可以直接借助一个队列来完成这个题目,首先我们创建一个队列用来暂存待处理的节点,并创建一个用来存储层序遍历的结果的vector容器。将根节点压入队列,当队列不为空时,循环执行以下操作:从队列头部取出一个节点,将节点的值添加到结果中。如果节点有左子树,则将左子树节点加入队列。如果节点有右子树,则将右子树节点加入队列。当队列变为空时,所有节点都已经处理完毕,返回结果即可。

/*
struct TreeNode {
	int val;
	struct TreeNode *left;
	struct TreeNode *right;
	TreeNode(int x) :
			val(x), left(NULL), right(NULL) {
	}
};*/
class Solution {
public:
    vector<int> PrintFromTopToBottom(TreeNode* root) {
		vector<int> ret; // 返回层序遍历的结果
		queue<TreeNode*> q;
		if(root == nullptr)
			return ret;
		// 根节点先入队列
		q.push(root);
		
		while(q.size())
		{
			// 访问当前结点并出队列
			TreeNode* t = q.front();
			q.pop();
			ret.push_back(t->val);

			// 左子树入队列
			if(t->left)
				q.push(t->left);
			// 右子树入队列
			if(t->right)
				q.push(t->right);
		}
		return ret;
    }
};

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2035567.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Ubuntu22.04自动化安装Redis脚本(实测可用)

redis自动化安装脚本 #!/bin/bash# 检查 Redis 是否已安装 if dpkg -l | grep -q redis-server; thenecho "Redis 已安装"exit 0 elseecho "Redis 未安装&#xff0c;正在安装..."# 更新包列表sudo apt-get update# 修复系统中的破损依赖关系sudo apt --f…

教你用JQ怎么循环遍历数据,学会直接月入过万,不够我给你补!

1.先看数据类型。这是一个标准得json返回值 {"code": 200,"msg": "请求成功&#xff01;","data": [{"itemName": "给阿姨倒一杯卡布奇诺","unit": "颗","count": 6},{"item…

c语言 图片.bmp读写示例

1 图片.bmp数据结构 BMP&#xff08;Bitmap&#xff09;文件格式是一种简单的位图图像格式&#xff0c;其数据结构分为几个主要部分&#xff1a;文件头、信息头、调色板&#xff08;可选&#xff09;和像素数据。下面是各部分的详细说明。 文件头&#xff08;File Header&…

zabbix看图表的时候标题是乱码

直接进入到&#xff1a;/usr/share/zabbix/assets/fonts 然后进入到windows下边fonts选择一个自己喜欢的字体&#xff0c;上传到/usr/share/zabbix/assets/fonts 然后把内容graphfont.ttff覆盖即可

《网络编程实战系列》(17)网络桥接模式

文章目录 **桥接模式的基本原理****桥接模式的应用场景****桥接模式的优缺点****桥接模式的实现****总结**桥接模式(Bridge Mode)是一种网络配置模式,用于将多个网络接口或网络段连接在一起,使其在逻辑上形成一个单一的网络。这种模式常用于在不同网络之间传递数据包,并使…

超详解Haproxy七层代理及配置

1.七层、四层负载及正、反向代理 1.1四层与七层负载均衡的区别 所谓的四到七层负载均衡&#xff0c;就是在对后台的服务器进行负载均衡时&#xff0c;依据四层的信息或七层的信息来决定怎么样转发流量四层的负载均衡&#xff0c;就是通过发布三层的IP地址(VIP)&#xff0c;然…

C语言—函数栈帧

函数&#xff0c;一般都有返回值&#xff0c;函数名&#xff0c;参数&#xff0c;再下来还有什么mian函数&#xff0c;函数写出来就是要被调用的&#xff0c;上面图片上的代码&#xff0c;main函数和myadd函数&#xff0c;都要在自己的栈结构什么形成自己的栈&#xff0c;可以帮…

如何获取VS Code扩展的版本更新信息

获取VS Code 扩展的版本更新的需求 因为企业内部有架设私有扩展管理器的要求&#xff0c;但是对于一些官方市场的插件&#xff0c;希望可以自动获取这些扩展的更新并上传至私有扩展管理器。于是就有了本篇介绍的需求&#xff1a; 通过API的方式获取VS Code 扩展的更新。 关于…

Spring Boot集成sentinel快速入门Demo

1.什么是sentinel&#xff1f; 随着微服务的流行&#xff0c;服务和服务之间的稳定性变得越来越重要。Sentinel 是面向分布式、多语言异构化服务架构的流量治理组件&#xff0c;主要以流量为切入点&#xff0c;从流量路由、流量控制、流量整形、熔断降级、系统自适应过载保护、…

python从入门到精通:判断语句

目录 前言 1、布尔类型和比较运算符 2、if语句的基本格式 3、if else语句 4、if elif else语句 5、判断语句的嵌套 6、实战演练 前言 逻辑判断是生活中常见的行为。同样&#xff0c;在程序中&#xff0c;进行逻辑判断也是最为基础的功能。 判断是程序最基础最核心的逻辑…

远程桌面工具企业版:Splashtop Enterprise

在当今全球化和数字化的商业环境中&#xff0c;远程工作和分布式团队合作成为了新常态。企业需要一种高效、安全的远程桌面解决方案&#xff0c;能够满足不断变化的业务需求&#xff0c;同时确保数据的安全性和员工的生产力。Splashtop Enterprise 是一款为企业量身定制的远程桌…

haproxy负载均衡之-调度算法详解

HAProxy的调度算法分为静态调度算法、动态调度算法和其他调度算法静态算法&#xff1a;按照事先定义好的规则轮询公平调度&#xff0c;不关⼼后端服务器的当前负载、链接数和响应速度等&#xff0c;且⽆法实时修改权重&#xff0c;只能靠重启HAProxy⽣效。动态算法&#xff1a;…

【NI-DAQmx入门】LabVIEW数据采集基础应用程序框架

对于可管理规模的 LabVIEW 程序&#xff0c;分析现有程序或设计新程序的方法通常是从整体到具体&#xff0c;即从高级到低级的分析和设计。从一开始就直接深入细节可能会效率较低。 在设计阶段&#xff0c;开发人员首先将程序垂直划分为几个层级。从最顶层开始&#xff0c;他们…

强化学习之Actor-Critic算法(基于值函数和策略的结合)——以CartPole环境为例

0.简介 DQN算法作为基于值函数的方法代表&#xff0c;基于值函数的方法只学习一个价值函数。REINFORCE算法作为基于策略的方法代表&#xff0c;基于策略的方法只学习一个策略函数。Actor-Critic算法则结合了两种学习方法&#xff0c;其本质是基于策略的方法&#xff0c;因为其目…

element时间段选择器或时间选择器 只设置默认起始时间或者结束时间,不显示问题

element时间段选择器或时间选择器 只设置默认起始时间或者结束时间&#xff0c;不显示问题 <div v-for"(item,index) in [a,b]":key"item"><el-date-pickerv-if"b"v-model"value1[item]"type"datetimerange"value-…

16s功能注释Bugbase的安装使用--本地版

文章目录 概述介绍下载安装程序下载并配置环境安装依赖R包并显示帮助运行示例数据Bug及解决方法-☆ 使用输入文件准备-☆下载Greengenes数据库在QIIME2中操作R语言操作 运行Bugbase 概述 Bugbase依赖于Greegenes1与R 但是R现已更新到4.4以上&#xff0c;安装R包时会不兼容且输…

【时时三省】(C语言基础)结构体初阶

山不在高&#xff0c;有仙则名。水不在深&#xff0c;有龙则灵。 ----CSDN 时时三省 结构体的声明 结构的基础知识: 结构是一些值的集合&#xff0c;这些值称为成员变量。结构的每个成员可以是不同类型的变量 数组: 是一组相同类型的元素的集合 结构体: 也是一些值得集合…

python-二进制?十进制?(赛氪OJ)

[题目描述] 给定两个十进制整数 : A&#xff0c;B。 你需要把它们的二进制形式以十进制的运算法则相加输出结果。 例如&#xff1a; A3 , B2的时候&#xff0c;A 的二进制表示是 : 11 , &#x1d435;B 的二进制表示是 10 &#xff0c;你需要输出答案为 : 21。 输入格式…

基于 Flutter 从零开发一款产品(一)—— 跨端开发技术介绍

前言 相信很多开发者在学习技术的过程中&#xff0c;常常会陷入一种误区当中&#xff0c;就是学了很多技术理论知识&#xff0c;但是仍做不出什么产品出来&#xff0c;往往学了很多干货&#xff0c;但是并无实际的用处。其实&#xff0c;不论是做什么&#xff0c;我们都需要从…

嵌入式linux系统镜像制作day1

点击上方"蓝字"关注我们 01、前言 嵌入式设备&#xff08;例如心电图检测仪&#xff0c;售票系统等&#xff09;。尽管&#xff0c;嵌入式设备像那些智能手机一样&#xff0c;绝大多数都使用同样的硬件和软件&#xff0c;包括系统芯片SoC、储存、连接和多媒体接口、…