《剑指Offer》题二十一~题三十
二十一、调整数组顺序使奇数位于偶数前面
题目:输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有奇数位于数组的前半部分,所有偶数位于数组的后半部分。
测试用例:
- 功能测试:输入数组中的奇数、偶数交替出现;输入的数组中所有偶数都出现在奇数的前面;输入的数组中所有奇数都出现在偶数的前面。
- 特殊输入测试:输入nullptr指针;输入的数组只包含一个数字。
只完成功能的解法:
void record_odd_before_even(int *pData, int length)
{
if(pData == nullptr || length == 0) return;
int *pBegin = pData;
int *pEnd = pData + length - 1;
while(pBegin < pEnd) {
if(pBegin < pEnd && !is_even(*pBegin))
pBegin++;
if(pBegin < pEnd && is_even(*pEnd))
pEnd--;
if(pBegin < pEnd) {
int temp = *pBegin;
*pBegin = *pEnd;
*pEnd = temp;
}
}
} /* 判断n是否为偶数 */
bool is_even(int n)
{
bool isEven = ((n & 0x1) == 0);
return isEven;
}
分析:上段代码在扫描传入的数组时,如果发现有偶数出现在奇数的前面,则交换它们的顺序。所以,它维护两个指针,pBegin初始化时指向数组的第一个数字,它只向后移动;第二个指针初始化时指向数组的最后一个数字,它只向前移动。
二十二、链表中倒数第K个节点
题目:输入一个链表,输出该链表中倒数第K个节点。例如,一个链表有6个节点,从头节点开始,它们的值依次是1、2、3、4、5、6。这个链表的倒数第3个节点是值为4的节点。
分析:为了得到倒数第K个节点,很自然地想到先遍历链表,从而得到链表的节点数n,于是获悉倒数第K个节点就是从头节点开始的第n-k+1个节点。
遍历链表两次的解法:
ListNode* get_kth_from_tail(ListNode *pHead, unsigned int k)
{
// 鲁棒性测试点1
if(pHead == nullptr || k == 0) return nullptr;
int cnt = 0;
ListNode *pNode = pHead;
while(pNode != nullptr) { // 第一次遍历链表
pNode = pNode->next;
++cnt;
}
// 鲁棒性测试点2
if(k > cnt) return nullptr;
pNode = pHead;
while(cnt != k) { // 第二次遍历链表
pNode = pNode->next;
--cnt;
}
return pNode;
}
分析:如果面试官告诉我们只需遍历链表一次,我们应该想点办法了。可以定义两个指针,第一个指针pAhead从链表的头指针开始遍历向前走K-1步,第二个指针pBehind保持不动;从第K步开始,第二个指针pBehind也开始从链表的头指针开始遍历。由于两个指针的距离保持在k-1,当pAhead指针到达链表的尾节点时,pBehind指针正好指向倒数第K个节点。
遍历链表一次的解法:
ListNode* get_kth_from_tail(ListNode *pHead, unsigned int k)
{
if(pHead == nullptr || k == 0) return nullptr;
ListNode *pAhead = pHead;
ListNode *pAhead = pHead;
for(int i = 0; i < k - 1; ++i) { // 向前走k-1步
if(pAhead->next != nullptr)
pAhead = pAhead->next;
else // 链表节点数小于k
return nullptr;
}
while(pAhead->next != nullptr) {
pAhead = pAhead->next;
pBehind = pBehind->next;
}
return pBehind;
}
二十三、链表中环的入口节点
题目:如果一个链表中包含环,如何找出环的入口节点?
分析:①判断一个链表中是否包含环;②如果有环,就要找到环的入口。
解法:
/* 返回环的入口节点 */
ListNode* entry_node_of_loop(ListNode *pHead)
{
if(pHead == nullptr) return nullptr;
// 判断是否有环,如果有,则返回一个环中的节点
ListNode *meetNode = meet_node(pHead);
if(meetNode == nullptr) return nullptr;
// 计算环中的节点数
int nodeNumOfLoop = 1;
ListNode *pCurrent = meetNode;
while(pCurrent->next != meetNode){
pCurrent = pCurrent->next;
nodeNumOfLoop++;
}
// 找出环的入口
ListNode *pAhead = pHead;
ListNode *pBehind = pHead;
for(int i = 0; i < nodeNumOfCode; ++i) {
pAhead = pAhead->next;
}
while(pAhead != pBehind) {
pAhead = pAhead->next;
pBehind = pBehind->next;
}
return pAhead;
} /* 判断链表中是否包含环 */
/* 如果有环,则返回两个指针相遇的节点 */
ListNode meet_node(ListNode *pHead)
{
ListNode *pSlow = pHead->next; // 一次走一步
if (pSlow == nullptr) return nullptr;
ListNode *pFast = pSlow->next; // 一次走两步
while(pFast != nullptr) {
if(pSlow == pFast) // 如果相遇即有环
return pFast; // 相遇的节点一定在环中
pSlow = pSlow->next;
pFast = pFast->next;
if(pFast != nullptr)
pFast = pFast->next;
}
return nullptr;
}
小结:利用两个指针pFast和pSlow来判断链表中是否包含环,如果有,就返回它们相遇的节点,显然这个节点是环中的一个节点。根据环中的一个节点,我们就可以遍历该环以得到环中的节点数。根据节点数,结合面试题22,可以利用两个指针来得到环的入口节点。
二十四、反转链表
题目:定义一个函数,输入一个链表的头节点,反转该链表并输出反转后链表的头节点。
测试用例:
- 输入的链表头指针是nullptr。
- 输入的链表只有一个节点。
- 输入的链表有多个节点。
解法:
ListNode* reverse_list(ListNode *pHead)
{
if(pHead == nullptr) return nullptr;
ListNode *pRevHead = nullptr; // 反转后的链表的头节点
ListNode *pPre = nullptr;
ListNode *pCur = pHead;
while(pCur != nullptr) {
ListNode *pNext = pCur->next; // 在反转后当前节点和之后的节点会断开,故把后面的节点保存下来
if(pNext == nullptr)
pRevHead = pCur;
pCur->next = pPre;
pPre = pCur;
pCur = pNext;
}
return pRevHead;
}
分析:反转一个链表,需要调整链表中指针的方向,显然需要3个指针。其中,最重要的是保存当前要调整指针方向的节点的下一个节点pNext,即pCur->next。
拓展:用递归实现同样的反转链表的功能。
二十五、合并两个排序的链表
题目:输入两个递增排序的链表,合并这两个链表并使新链表中的节点仍然是递增排序的。
测试用例:
- 功能测试:输入的两个链表有多个节点;节点的值互不相同或者存在值相等的多个节点。
- 特殊输入测试:两个链表的一个或者两个头节点为nullptr指针;两个链表中只有一个节点。
递归解法:
ListNode* Merge(ListNode *pHead1, ListNode *pHead2)
{
if(pHead1 == nullptr) return pHead2;
if(pHead2 == nullptr) return pHead1;
ListNode *pMergedHead = nullptr;
if(pHead1->value < pHead2->value) {
pMergedHead = pHead1;
pMergedHead->next = Merge(pHead1->next, pHead2);
}
else {
pMergedHead = pHead2;
pMergedHead->next = Merge(pHead1, pHead2->next);
}
return pMergedHead;
}
分析:每次比较两个链表的头节点,较小的被加入到合并的链表中,并更新原来链表的头节点,然后再次比较两个链表的头节点。本题需要注意的是如果传入的是空链表时,我们应该如何处理。
二十六、树的子结构
题目:输入两棵二叉树A和B,判断B是不是A的子结构。
分析:要查找树A中是否存在和树B一样的子树,需要①在树A中找到和树B的根节点的值一样的节点R,②判断树A中以R为根节点的子树是不是包含和树B一样的结构。
测试用例:
- 功能测试:树B是或者不是树A的子结构。
- 特殊输入测试:两棵二叉树的一个或者两个根节点为nullptr指针;二叉树的所有节点都没有左子树或者右子树。
二十七、二叉树的镜像
题目:请完成一个函数,输入一棵二叉树,该函数输出它的镜像。
题意:求树的镜像的过程,其实就是在遍历树的同时交换非叶节点的左、右子节点。具体来说,就是先前序遍历这棵树的每个节点,如果遍历到的节点有子节点,就交换它的两个子节点。
测试用例:
- 功能测试:普通的二叉树;二叉树的所有节点都没有左子树或者右子树;只有一个节点的二叉树。
- 特殊输入测试:二叉树的根节点为nullptr指针。
解法:
void mirror_recursive(BinaryTreeNode *pRoot)
{
if(pRoot == nullptr) return;
if(pRoot->lChild == nullptr && pRoot->rChild == nullptr) return;
BinaryTreeNode *pTemp = pRoot->lChild;
pRoot->lChild = pRoot->rChild;
pRoot->rChild = pTemp;
if(pRoot->lChild) mirror_recursive(pRoot->lChild);
if(pRoot->rChild) mirror_recursive(pRoot->rChild);
}
扩展:上面的代码是用递归实现的。如果要求用循环,则该如何实现?
二十八、对称的二叉树
题目:请实现一个函数,用来判断一棵二叉树是不是对称的。如果一棵二叉树和它的镜像一样,那么它是对称的。
分析:可以先得到该二叉树的镜像,然后来比较该镜像和原二叉树是否相同,但上题只是完成二叉树的镜像,因为我还不会如何去复制一个二叉树,并且这个方法的效率也不高。
前序遍历和自定义的对称前序遍历解法:
bool is_symmetrical(BinaryTreeNode *pRoot)
{
return is_symmetrical(pRoot, pRoot);
} bool is_symmetrical(BinaryTreeNode *pRoot1, BinaryTreeNode *pRoot2)
{
if(pRoot1 == nullptr && pRoot2 == nullptr) return true;
if(pRoot1 == nullptr || pRoot2 == nullptr) return false;
if(pRoot1->value != pRoot2->value) return false;
return is_symmetrical(pRoot1->lChild, pRoot2->rChild) && is_symmetrical(pRoot1->rChild, pRoot2->lChild);
}
小结:针对前序遍历定义一种对称的遍历算法,即先遍历父节点,再遍历它的右子节点,最后遍历它的左子节点。我们可以通过比较二叉树的前序遍历序列和对称前序遍历序列来判断二叉树是不是对称的。
二十九、顺时针打印矩阵
题目:输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字。
分析:此题可借助图形来帮助思考,我们可以把矩阵想象成若干个圈。我们可以用一个循环来打印矩阵,每次打印矩阵中的一个圈。要注意的是,我们应理清循环继续的条件以及打印时每一步的前提条件。
解法:
void PrintMatrixClockwisely(int** numbers, int columns, int rows)
{
if(numbers == nullptr || columns <= 0 || rows <= 0)
return; int start = 0; while(columns > start * 2 && rows > start * 2)
{
PrintMatrixInCircle(numbers, columns, rows, start); ++start;
}
} void PrintMatrixInCircle(int** numbers, int columns, int rows, int start)
{
int endX = columns - 1 - start;
int endY = rows - 1 - start; // 从左到右打印一行
for(int i = start; i <= endX; ++i)
{
int number = numbers[start][i];
printNumber(number);
} // 从上到下打印一列
if(start < endY)
{
for(int i = start + 1; i <= endY; ++i)
{
int number = numbers[i][endX];
printNumber(number);
}
} // 从右到左打印一行
if(start < endX && start < endY)
{
for(int i = endX - 1; i >= start; --i)
{
int number = numbers[endY][i];
printNumber(number);
}
} // 从下到上打印一行
if(start < endX && start < endY - 1)
{
for(int i = endY - 1; i >= start + 1; --i)
{
int number = numbers[i][start];
printNumber(number);
}
}
} void printNumber(int number)
{
printf("%d\t", number);
}
三十、包含min函数的栈
题目:定义栈的数据结构,请在该类型中实现一个能够得到栈的最小元素的min函数。在该栈中,调用min、push及pop的时间复杂度都是O(1)。
分析:初看本题时,会发现无从下手。既要push/pop的时间为O(1),又要min的时间为O(1),看上去像是不可能的事。但可以借助两个栈来实现本题的要求,其中数据栈实现push/pop的功能,而辅助栈实现min的功能,而关键之处是维持辅助栈的内容。
解法:
stack<int> dataSck; // 数据栈
stack<int> minSck; // 辅助栈 void push(int val)
{
dataSck.push(val);
if(!minSck.empty()) {
if(val < minSck.top()) minSck.push(val);
else minSck.push(minSck.top());
}
else {
minSck.push(val);
}
} void pop()
{
assert(dataSck.size() > 0 && minSck.size() > 0);
dataSck.pop();
minSck.pop();
} void min()
{
assert(dataSck.size() > 0 && minSck.size() > 0);
return minSck.top();
}
小结:①把每次的最小元素(之前的最小元素和新压入栈的元素两者的较小值)都保存起来放到辅助栈中,要保证数据栈和辅助栈的元素个数一致;②保证辅助栈的栈顶一直都是最小元素;③要想到这个思路,其实只要举个例子,多做几次入栈、出栈的操作就能看出问题,并想到也要把最小元素用另外的辅助栈保存。
《剑指Offer》题二十一~题三十的更多相关文章
- 剑指Offer(二十一):栈的压入、弹出序列
剑指Offer(二十一):栈的压入.弹出序列 搜索微信公众号:'AI-ming3526'或者'计算机视觉这件小事' 获取更多算法.机器学习干货 csdn:https://blog.csdn.net/b ...
- 《剑指offer》第十一题(旋转数组的最小数字)
// 面试题:旋转数组的最小数字 // 题目:把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转. // 输入一个递增排序的数组的一个旋转,输出旋转数组的最小元素.例如数组 // {3, ...
- 《剑指offer》第二十一题(调整数组顺序使奇数位于偶数前面)
// 面试题21:调整数组顺序使奇数位于偶数前面 // 题目:输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有 // 奇数位于数组的前半部分,所有偶数位于数组的后半部分. #inclu ...
- 刷题-力扣-剑指 Offer 15. 二进制中1的个数
剑指 Offer 15. 二进制中1的个数 题目链接 来源:力扣(LeetCode) 链接:https://leetcode-cn.com/problems/er-jin-zhi-zhong-1de- ...
- 剑指offer:二维数组中的查找
目录 题目 解题思路 具体代码 题目 题目链接 剑指offer:二维数组中的查找 题目描述 在一个二维数组中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺 ...
- 剑指Offer(二十二):从上往下打印二叉树
剑指Offer(二十二):从上往下打印二叉树 搜索微信公众号:'AI-ming3526'或者'计算机视觉这件小事' 获取更多算法.机器学习干货 csdn:https://blog.csdn.net/b ...
- 剑指Offer(二十五):复杂链表的复制
剑指Offer(二十五):复杂链表的复制 搜索微信公众号:'AI-ming3526'或者'计算机视觉这件小事' 获取更多算法.机器学习干货 csdn:https://blog.csdn.net/bai ...
- 【剑指Offer】二叉搜索树的第k个结点 解题报告(Python)
[剑指Offer]二叉搜索树的第k个结点 解题报告(Python) 标签(空格分隔): 剑指Offer 题目地址:https://www.nowcoder.com/ta/coding-intervie ...
- 剑指Offer(二十六):二叉搜索树与双向链表
剑指Offer(二十六):二叉搜索树与双向链表 搜索微信公众号:'AI-ming3526'或者'计算机视觉这件小事' 获取更多算法.机器学习干货 csdn:https://blog.csdn.net/ ...
- 剑指Offer(二十八):数组中出现次数超过一半的数字
剑指Offer(二十八):数组中出现次数超过一半的数字 搜索微信公众号:'AI-ming3526'或者'计算机视觉这件小事' 获取更多算法.机器学习干货 csdn:https://blog.csdn. ...
随机推荐
- 模板——最小生成树kruskal算法+并查集数据结构
并查集:找祖先并更新,注意路径压缩,不然会时间复杂度巨大导致出错/超时 合并:(我的祖先是的你的祖先的父亲) 找父亲:(初始化祖先是自己的,自己就是祖先) 查询:(我们是不是同一祖先) 路径压缩:(每 ...
- jquery 插件学习
练习jquery上的一个插件编写 1.标准的3个基本内容,根目录里面创建2个文件夹(存放css和js)和1个html页面文件: 2.测试的主html页面代码 <!DOCTYPE html> ...
- 论反向ajax
在讨论反向ajax之前,说一下我大二写的一个聊天程序,通过前端ajax不断向服务器发送请求(通过http连接),服务器进行一次数据库查询,然后返回结果,断开与服务器的链接(绝大部分都是无用的结果)来维 ...
- day 25 模块与包
一.模块 模块就是一个包含了python定义和申明的文件,文件名就是模块的名字加上.py的后缀/ 模块的分类: 1.使用python编写的py文件 2.已被编译位共享库或者DLL或 ...
- Python环境搭建(Windows)
·Python环境搭建(Windows) @ 下载Python Python官网:https://www.python.org/ Python帮助文档下载地址:https://www.python ...
- js分片上传大文件,前端代码
首先导入jQuery.form.js文件,下面src是相对于改js文件位置, <script type="text/JavaScript" src="jquery/ ...
- 分享Centos6.5升级glibc过程
默认的Centos6.5 glibc版本最高为2.12, 而在进行Nodejs开发时项目所依赖的包往往需要更高版本的glibc库支持, 因此在不升级系统的前提下, 需要主动更新系统glibc库. 一般 ...
- lvs健康检查脚本第三版
如下是学习完马哥视频lvs后改写的健康检查脚本第三版.利用工作之余三四个小时时间才把整个逻辑搞清楚,有时候自己都有点蒙圈,尤其是在写到while循环的时候.总的来说非常感谢马哥的慷慨解囊!脚本原稿及思 ...
- XNA+WPF solution worked
Cory Petosky's website Edit 11/17/2010: While this article's XNA+WPF solution worked when I wrote it ...
- 成都Uber优步司机奖励政策(2月16日)
滴快车单单2.5倍,注册地址:http://www.udache.com/ 如何注册Uber司机(全国版最新最详细注册流程)/月入2万/不用抢单:http://www.cnblogs.com/mfry ...