8、面试题5: 替换空格
题目:请实现一个函数,把字符串中的每个空格替换成“%20”。例如,输入“We are happy.”,则输出“We%20are%20happy”。
正常来说,把空格换成%20就意味着要插入两个字符,使得字符串变长,如果不分配足够内存,就会覆盖后面的字符。
从前往后把字符串中的空格替换的过程,假设字符串的长度是n。对于每个空格字符,需要移动后面O(n)个字符,因此对于含有O(n)个空格字符的字符串而言,总的时间效率是O(n2)。
但是上述解法,时间复杂度太高了。
接下来介绍时间复杂度为O(n)的解法:
先遍历一次字符串,统计出字符串中空格的总数,并计算出替换后的字符串总长度。从字符串后面开始复制和替换。准备两个指针:p1和p2,p1指向原始字符串的末尾,而p2指向替换后的字符串的末尾。接下来向前移动指针p1,逐个把它指向的字符复制到p2指向的位置,直到碰到第一个空格为止。碰到第一个空格之后,把p1向前移动一格,在p2之前插入字符串“%20”。由于“%20”的长度为3,同时也要把p2向前移动3格,再重复上述操作,直至p1和p2指向同一个位置。
下面是参考代码:
string replaceSpace(string s)
{
// 先判断是否是空字符串
if(s.size()==0)
return s;
// 统计空格的个数
int count = 0;
for(char&x:s)
if(x==' ')
++count;
// 重新计算字符串的长度
int sOldSize = s.size();
s.resize(sOldSize+2*count);
int sNewSize = s.size();
// 从字符串后面开始往前遍历进行修改
for(int i=sNewSize-1,j=sOldSize-1;j<i;--i,--j)
{
if(s[j]!=' ')
s[i]=s[j];
else{
s[i]='0';
s[i-1]='2';
s[i-2]='%';
i -= 2;
}
}
return s;
}
9、链表
链表是一种动态的数据结构,需要对指针进行操作,插入节点需要申请内存,删除节点需要回收内存,而且链表的相关操作代码量较少,这些都是面试官青睐于考察链表的原因。
单向链表的节点定义:
struct ListNode
{
int m_nValue;
ListNode* m_pNext;
};
往链表末尾添加一个节点:
// 传入需要被插入链表的头指针
void AddToTail(ListNode*pHead, int value)
{
// 先创建一个节点
ListNode* pNew = new ListNode();
pNew->m_nvalue = value;
pNew->m_pNext = nullptr;
// 检查给定的链表是否为空
if(pHead == nullptr)
pHead = pNew;
else{
ListNode* cur = pHead;
while(cur->m_pNext!=nullptr)
cur = cur->m_pNext;
cur->m_pNext = pNew;
}
}
找到第一个含有某值的节点并把其删除:
void RemoveNode(ListNode*pHead, int value)
{
// 先判断是否为空链表
if(pHead==nullptr)
return;
ListNode* pToBeDeleted = nullptr;
// 判断要删除的节点是否为头结点
if(pHead->m_nvalue==value)
{
pToBeDeleted = pHead;
pHead = pHead->m_pNext;
}
else
{
ListNode* cur = pHead;
while(cur->m_pNext!=nullptr&&
cur->m_pNext->m_nValue!=value)
{
cur = cur->m_pNext;
}
if(cur->m_pNext!=nullptr&&
cur->m_pNext->m_nvalue==value)
{
pToBeDeleted = cur->m_pNext;
cur->m_pNext = cur->m_pNext->pNext
}
}
// 如果找到被删除节点,那么就回收内存,把指针指向空
if(pToBeDeleted!=nullptr)
{
delete pToBeDeleted;
pToBeDeleted = nullptr;
}
}
10、面试题6:从尾到头打印链表
题目:输入一个链表的头节点,从尾到头反过来打印出每个节点的值。链表节点定义如下:
struct ListNode
{
int m_value;
ListNode* m_Next;
};
如果可以修改链表,那么就可以把链表反转,再从头到尾打印,但是通常来说,打印只是一个读操作,一般不需要修改内容,所以最好问一下面试官是否可以改变链表的结构。
假设不可以修改链表的结构可以利用栈后进先出的思想,达到从尾到头的打印效果!
void PrintListReversed(ListNode* pHead)
{
std::stack<int>nodes;
ListNode* cur = pHead;
while(cur!=nullptr)
{
nodes.push(cur->m_value);
cur = cur->m_Next;
}
while(!nodes.empty())
{
std::cout<<nodes.top()<<std::endl;
nodes.pop();
}
}
递归写法:
void PrintListReversed(ListNode* pHead)
{
if(pHead != nullptr)
{
if(pHead->m_Next != nullptr)
PrintListReversed(pHead->m_Next);
std::cout<<pHead->m_value<<std::endl;
}
}
递归写法虽然简洁,但是如果链表非常长,会导致函数调用的层级很深,从而使得函数调用栈溢出。显然用栈基于循环实现的代码的鲁棒性要好一些。
11、树
树是一种常用的数据结构,其特点为:除根节点外,每个节点只有一个父节点,根节点没有父节点;除叶节点外,每个节点都有一个或多个子节点,叶节点没有子节点,父节点和子节点是通过指针来连接。
面试常用的是二叉树,二叉树中每个节点最多只能有两个子节点。
常用的遍历方式:
(1)前序遍历:先访问根节点,再访问左子节点,最后访问右子节点。
(2)中序遍历:先访问左子节点,再访问根节点,最后访问右子节点。
(3)后序遍历:先访问左子节点,再访问右子节点,最后访问根节点。
每一种遍历都有循环和递归两种不同的实现方法,每种遍历的递归实现都要比循环实现要简洁得多。
12、面试题7:重建二叉树
题目:输入某二叉树的前序遍历和中序遍历的结果,请重建该查二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。
struct BinaryTreeNode
{
int m_nValue;
BinaryTreeNode* m_pLeft;
BinaryTreeNode* m_pRight;
}
BinaryTreeNode*Construct(int* preorder,int* inorder,int length)
{
if(preorder==nullptr||inorder==nullptr||length<=0)
return nullptr;
return ConstructCore(preorder,preorder+length-1,inorder,inorder+length-1);
}
BinaryTreeNode* ConstructCore(int*startPreorder,int*endPreorder,int*startInorder,int*endInorder)
{
// 前序遍历第一个数字是根节点的值
int rootValue = startPreorder[0];
BinaryTreeNode* root = new BinaryTreeNode();
root->m_nValue = rootValue;
root->m_pLeft = root->m_pRight = nullptr;
// 判断只有一个节点的情况
if(startPreorder == endPreorder)
{
if(startInorder == endInorder && *startPreorder == *startInorder)
return root;
else
throw std::exception("Invalid input");
}
// 在中序遍历序列中找到根节点的值
int* rootInorder = startInorder;
while(rootInorder<=endInorder && *rootInorder != rootVaule)
++rootInorder;
// 如果找到末尾还是没有找到根节点,抛出异常
if(rootInorder == endInorder && *rootInorder != rootValue)
throw std::exception("Invalid input");
// 获取左子树长度
int leftLength = rootInorder-startInorder;
int* leftPreorderEnd = startPreorder + leftLenth;
// 如果左子树的长度大于0就构造左子树
if(leftLenght>0)
{
// 构建左子树
root->m_pLeft = ConstructCore(startPreorder+1,leftPreorderEnd,stratInorder,rootInorder-1);
}
// 如果leftLength+startPreorder<endPreorder说明还有一部分是右子树
if(leftLength<endPreorder-startPreorder)
{
// 构建右子树
root->m_pRight = ConstructCore(leftPreorderEnd+1,endPreorder,rootInorder+1,endInorder);
}
return root;
}
13、面试题8:二叉树的下一个节点
题目:给定一颗二叉树和其中的一个节点,如何找出中序遍历序列的下一个节点?树中的节点除了有两个分别指向左右子节点的指针,还有一个指向父节点的指针。
struct BinaryTreeNode
{
int m_nValue;
BinaryTreeNode* m_pLeft;
BinaryTreeNode* m_pRight;
BinaryTreeNode* m_pParent;
}
BinaryTreeNode* GetNext(BinaryTreeNode* pNode)
{
if(pNode == nullptr)
return nullptr;
BinaryTreeNode* pNext = nullptr;
// 当前节点有右子树的情况
// 找到右子树最左子节点
if(pNode->m_pRight != nullptr)
{
BinaryTreeNode* pRight =
pNode->m_pRight;
while(pRight->m_pLeft
!= nullptr)
pRight = pRight->m_pLeft;
pNext = pRight;
}
// 当前节点没有右子树的情况,
// 往上一直找父节点,找到没有父节点的情况或者,当前节点不是右子树节点
else if(pNode->m_pParent != nullptr)
{
BinaryTreeNode* pCurrent
= pNode;
BinaryTreeNode* pParent
= pNode->m_pParent;
while(pParent != nullptr&&
pCurrent==pParent
->m_pRight)
{
pCurrent = pParent;
pParent = pParent
->m_pParent;
}
pNext = pParent;
}
return pNext;
}
14、面试题9:用两个栈去实现队列
题目:用两个栈实现一个队列。队列的声明如下,请实现它的两个函数appendTail和deleteHead,分别完成在队列尾部插入节点和在队列头部删除节点的功能
template<typename T>class CQueue
{
public:
CQueue(void);
~CQueue(void);
void appendTail(const T& node);
T deleteHead();
private:
stack<T>stack1;
stack<T>stack2;
};
// 尾部插入是插入在stack1,假如需要在头部删除元素,并且stack2为空,那么就先把stack1的元素全部压入stack2中,这时候stack2的栈顶元素就是最先进来的元素,弹出即可。
template<typename T>void CQueue<T>::appendTail(const T& element)
{
stack1.push(element);
}
template<typename T>T CQueue<T>::deleteHead()
{
if(stack2.size()<=0)
{
while(stack1.size()>0)
{
T& data = stack1.top();
stack1.pop();
stack2.push(data);
}
}
if(stack2.size()==0)
{
throw new exception("queue is empty");
}
T head = stack2.top();
stack2.pop();
return head;
}