二十一、调整数组顺序使奇数位于偶数前面

题目:输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有奇数位于数组的前半部分,所有偶数位于数组的后半部分。

测试用例:

  • 功能测试:输入数组中的奇数、偶数交替出现;输入的数组中所有偶数都出现在奇数的前面;输入的数组中所有奇数都出现在偶数的前面。
  • 特殊输入测试:输入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》题二十一~题三十的更多相关文章

  1. 剑指Offer(二十一):栈的压入、弹出序列

    剑指Offer(二十一):栈的压入.弹出序列 搜索微信公众号:'AI-ming3526'或者'计算机视觉这件小事' 获取更多算法.机器学习干货 csdn:https://blog.csdn.net/b ...

  2. 《剑指offer》第十一题(旋转数组的最小数字)

    // 面试题:旋转数组的最小数字 // 题目:把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转. // 输入一个递增排序的数组的一个旋转,输出旋转数组的最小元素.例如数组 // {3, ...

  3. 《剑指offer》第二十一题(调整数组顺序使奇数位于偶数前面)

    // 面试题21:调整数组顺序使奇数位于偶数前面 // 题目:输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有 // 奇数位于数组的前半部分,所有偶数位于数组的后半部分. #inclu ...

  4. 刷题-力扣-剑指 Offer 15. 二进制中1的个数

    剑指 Offer 15. 二进制中1的个数 题目链接 来源:力扣(LeetCode) 链接:https://leetcode-cn.com/problems/er-jin-zhi-zhong-1de- ...

  5. 剑指offer:二维数组中的查找

    目录 题目 解题思路 具体代码 题目 题目链接 剑指offer:二维数组中的查找 题目描述 在一个二维数组中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺 ...

  6. 剑指Offer(二十二):从上往下打印二叉树

    剑指Offer(二十二):从上往下打印二叉树 搜索微信公众号:'AI-ming3526'或者'计算机视觉这件小事' 获取更多算法.机器学习干货 csdn:https://blog.csdn.net/b ...

  7. 剑指Offer(二十五):复杂链表的复制

    剑指Offer(二十五):复杂链表的复制 搜索微信公众号:'AI-ming3526'或者'计算机视觉这件小事' 获取更多算法.机器学习干货 csdn:https://blog.csdn.net/bai ...

  8. 【剑指Offer】二叉搜索树的第k个结点 解题报告(Python)

    [剑指Offer]二叉搜索树的第k个结点 解题报告(Python) 标签(空格分隔): 剑指Offer 题目地址:https://www.nowcoder.com/ta/coding-intervie ...

  9. 剑指Offer(二十六):二叉搜索树与双向链表

    剑指Offer(二十六):二叉搜索树与双向链表 搜索微信公众号:'AI-ming3526'或者'计算机视觉这件小事' 获取更多算法.机器学习干货 csdn:https://blog.csdn.net/ ...

  10. 剑指Offer(二十八):数组中出现次数超过一半的数字

    剑指Offer(二十八):数组中出现次数超过一半的数字 搜索微信公众号:'AI-ming3526'或者'计算机视觉这件小事' 获取更多算法.机器学习干货 csdn:https://blog.csdn. ...

随机推荐

  1. Jboot使用appassembler-maven-plugin插件生成启动脚本

    appassembler-maven-plugin介绍: 使用appassembler-maven-plugin插件可自动生成跨平台的启动脚本,可省去手工写脚本的麻烦,而且还可以生成jsw的后台运行程 ...

  2. multiprocessing中进程池,线程池的使用

    multiprocessing 多进程基本使用 示例代码1 import time import random from multiprocessing import Process def run( ...

  3. border-radius__边框圆角

    1.四个参数: border-radius: 值1 值2 值3 值4;顺序:从左开始,顺时针顺 div{ width: 200px; height: 100px; background-color: ...

  4. python教程(二)·条件语句

    条件语句一般用来判断给定的条件是否成立,根据结果来执行不同的代码,也就是说,有了条件语句,才可以根据不同的情况做不同的事,从而控制程序的流程. 布尔类型 前面说到数据类型的时候,其中有一种叫 &quo ...

  5. SAP Odata実行命令(2)

    前言 $ skiptokenは.アプリケーションに送信されるエントリ数を制限するために使用されます. 膨大な数のエントリが要求された場合.これはパフォーマンスの向上にも役立ちます.次のリンクがアプリケ ...

  6. vue cli3超详细创建多页面配置

    1.首先按照vue cli3 给的入门文档下载个vue cli3 如果之前下载了vue cli2的要先卸载之前的 2.检查安装是否成功 3.ok,现在环境搭建好了,新建项目 vue create he ...

  7. Java设计模式(17)——行为模式之观察者模式(Observer)

    一.概述 概念 UML简图 我们根据一个示例得类图来分析角色 角色 抽象主题:保存观察者聚集(集合),管理(增删)观察者 抽象观察者:定义具体观察者的抽象接口,在得到主题通知后更新自己 具体主题:将有 ...

  8. Prism for WPF 搭建一个简单的模块化开发框架(五)添加聊天、消息模块

    原文:Prism for WPF 搭建一个简单的模块化开发框架(五)添加聊天.消息模块 中秋节假期没事继续搞了搞 做了各聊天的模块,需要继续优化 第一步画页面 页面参考https://github.c ...

  9. java web 开发模式

    1.Model1 javaBean+jsp:jsp直接操作数据库,不安全,开发维护复杂 2.Model2:MVC 原理:把Model1的操作javaBean操作抽取为控制层 实现:控制层使用servl ...

  10. BZOJ1003_物流运输_KEY

    题目传送门 这是一道DP+最短路径的好题. 首先预处理每天每个点的最短路径. 用SPFA进行处理.即cost[i][j]为第i天到底j天的1到M点的最小花费. 就可以水水的DP. 设f[i]为第i天的 ...