剑指offer_Cpp(2)

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;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

czy1219

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值