剑指Offer题目汇总(C++版)

最近,鉴于目前网上关于《剑指Offer》C++版的汇总资料特别少或者不规范,特整理了一版书中重要的考题,以备学习使用。

目录

一、位运算

1. 二进制中1的个数

2. 二进制中0的个数

3. 二进制高位连续0的个数

二、二叉树

1. 二叉搜索树第k个结点

2. 从上往下打印二叉树

3. 二叉树打印成多行

4. 数据流中位数

5. 二叉树中和为某一值的路径

6. 重建二叉树

7. 树的子结构

8. 二叉树的镜像

9. 二叉树的下一个节点

三、字符串

1. 正则表达式的匹配

2. 表示数值的字符串

3. 第一个只出现一次的字符

4. 左旋转字符串

四、数组

1. 构建乘积数组

2. 二维数组的查找

3. 连续子数组的最大和


一、位运算

1. 二进制中1的个数

题目:输入一个整数,输出该数二进制表示中1的个数,负数用补码表示。

思路:&运算通常用于二进制的取位操作,n&(n-1) 操作相当于把二进制表示中最右边的1变成0。

int NumberOf1(int n)
{
    int count = 0;
    while (n != 0)
    {
        n = n & (n-1);
        ++count;
    }
    return count;
}

2. 二进制中0的个数

题目:输入一个整数,输出该二进制表示中0的个数,负数用补码表示。

思路:n&1操作相当于判断二进制最后一位数是否位1,左移相当乘2,右移除2,左移补0,有符号右移,正数补0,负数补1,无符号右移,均补0。每次无符号右移一位,同1做&运算,可以判断最后一位是否为1。

int NumberOf0(int n)
{
    int count = 0;
    while (n != 0)
    {
        if ((n&1) != 1)
            count++;
        n>>=1;
    }
    return count;
}

3. 二进制高位连续0的个数

题目:输入一个整数,输出该二进制中高位连续0的个数。

思路:0x80000000的二进制是1000 0000 0000 0000 0000 0000 0000 0000,仅最高位为1,每次左移一位,与最高位为1的二进制进行&操作,可以判断高位连续为0的个数。

int numberOfLeading0(int n)
{
    if (n == 0)
        return 32;
    int count = 0;
    int mask = 0x80000000;
    int m = n & mask;
    while (m == 0)
    {
        ++count;
        n <<= 1;
        m = n & mask;
    }
    return count;
}

二、二叉树

1. 二叉搜索树第k个结点

题目:输入一颗二叉搜索树,输出第k个节点。

思路:二叉搜索树是满足根节点值大于左节点,小于右节点的。中序遍历二叉搜索树可得到有序的,判断当前是否为k即可。

struct treeNode {
     int val;
     treeNode * left;
     treeNode * right;
     treeNode(int x) : val(x), left(NULL), right(NULL) {}
 };

 treeNode *inorder(treeNode *root, int &k)
 {
    if (root == NULL) return NULL;
    treeNode * node = NULL;

    node = inorder(root->left,k);

    if (k-- == 1)
        node = root;
    if (k > 0)
        node = inorder(root->right, k);
     return node;
 }

 treeNode *KthNode(treeNode * pRoot, int k)
 {
     return inorder(pRoot, k);
 }

2. 从上往下打印二叉树

题目:输入一颗二叉树,输出从上到下,从左到右节点的值。

思路:可借助队列先进先出的特性,先层次遍历二叉树,依次存入队列中,最后再逐个弹出。

struct treeNode {
     int val;
     treeNode * left;
     treeNode * right;
     treeNode(int x) : val(x), left(NULL), right(NULL) {}
 };

vector<int> PrintFromTopToBottom(treeNode *root)
{
    vector<int> vec;
    queue<treeNode*> queue;

    if (root == NULL) return vec;
    queue.push(root);

    while(!queue.empty())
    {
        vec.push_back(queue.front()->val);
        if (queue.front()->left != NULL)
            queue.push(queue.front()->left);
        if (queue.front()->right != NULL)
            queue.push(queue.front()->right);
        queue.pop();
    }
}

3. 二叉树打印成多行

题目:输入一颗二叉树,输出从上到下,从左到右节点的值,且每一层输出在一行。

思路:可借助队列先进先出的特性,先层次遍历二叉树,依次存入队列中,最后再逐个弹出。

vector<vector<int>> Print(treeNode *root)
{
    vector<vector<int>> vec;
    queue<treeNode*> queue;

    if (root == NULL) return vec;
    queue.push(root);

    while(!queue.empty())
    {
        int n = queue.size(); //n用于保存每一次队列的元素个数,即二叉树每一层节点的个数。
        vector<int> v;
        while(v.size() < n)
        {
            v.push_back(queue.front()->val);
            if(queue.front()->left)
                queue.push(queue.front()->left);
            if(queue.front()->right)
                queue.push(queue.front()->right);
            queue.pop();
        }
        vec.push_back(v);
    }
}

4. 数据流中位数

题目:输入一个数据流,输出数据流中的中位数。

思路:最容易的办法是如果排序取中间值,显然这种不是面试官希望的答案。简单的办法是采取优先队列priority_queue,优先队列具有队列的所有特性,包括基本操作,只是在这基础上添加了内部的一个排序,它本质是一个堆实现的。把列表分成两半,分别用两个优先级队列来保存,左侧为数字越大优先级越高,右侧为数字越小优先级越高。那么取出两个优先级的top值即可。 

priority_queue<int, vector<int>, greater<int>> Rqueue;  //最小堆
priority_queue<int, vector<int>, less<int>> Lqueue;     //最大堆

void addNum(int num)
{
    Lqueue.push(num);
    if ((Lqueue.size() - Rqueue.size())>1)
    {
        Rqueue.push(Lqueue.top());
        Lqueue.pop();
    }
    if(Rqueue.size() > 0 && Lqueue.top() > Rqueue.top())
    {
        int tmp = Lqueue.top();
        Lqueue.pop();
        Lqueue.push(Rqueue.top());
        Rqueue.top();
        Rqueue.push(tmp);
    }
}

double findMedian()
{
    if (Lqueue.size() == Rqueue.size()) return  1.0*(Lqueue.top()+Rqueue.top())/2;
    else return Lqueue.top();
}

5. 二叉树中和为某一值的路径

题目:输入一颗二叉树的跟节点和一个整数,输出二叉树中结点值的和为输入整数的所有路径,返回的数组长度大的靠前。

思路:使用前序遍历的方式访问节点,使用二维向量vec存储全部路径,使用一维向量tmp存储当前路径。遍历二叉树的过程:按前序遍历顺序访问每一个节点。访问每个结点时,将结点添加到路径向量tmp中。如果当前结点是叶子结点,则判断当前路径是否是符合条件的路径,符合条件的路径存入到二维向量vec;如果当前结点不是叶子结点,则递归当前节点的左右子节点。

struct treeNode {
 int val;
 treeNode * left;
 treeNode * right;
 treeNode(int x) : val(x), left(NULL), right(NULL) {}
};

vector<vector<int>> vec;
vector<int> tmp;

void search(treeNode* root, int target)
{
    tmp.push_back(root->val);

    if (root->left == NULL && root->right == NULL) {
        if (root->val == target) {
            vec.push_back(tmp);
        }
    }

    if (root->left != NULL)
        search(root->left, target-root->val);

    if (root->right != NULL)
        search(root->right,target-root->val);

    if (!tmp.empty())
         tmp.pop_back();

}

void findTarget(treeNode* root, int target)
{
    if (root == NULL) return;

    search(root,target);
}

6. 重建二叉树

题目:输入某二叉树的前序遍历和中序遍历的结果,且前序遍历和中序遍历的结果中都不含重复的数字,输出重建二叉树。

思路:根据前序遍历找到根节点,再根据中序遍历找到左右子树,最后根据前序遍历得到左右子树的前序遍历和中序遍历,进行递归求解。

struct treeNode {
 int val;
 treeNode * left;
 treeNode * right;
 treeNode(int x) : val(x), left(NULL), right(NULL) {}
};

treeNode* ReConstructTree(vector<int>& pre, vector<int>& mid)
{
    if (pre.empty() || mid.empty())
        return NULL;

    treeNode *head = new treeNode(pre[0]);

    vector<int> leftpre, leftmid;
    vector<int> rightpre, rightmid;

    size_t root = 0;

    // 寻找根节点
    for (size_t i = 0;i < mid.size();++i)
    {
        if (mid[i] == pre[0])
        {
            root = i;
            break;
        }
    }

    // 左子树
    for (size_t i = 0;i < root;++i)
    {
        leftpre.push_back(pre[i+1]);
        leftmid.push_back(mid[i]);
    }

    // 右子树
    for (size_t i = root+1;i < mid.size();++i)
    {
        rightpre.push_back(pre[i]);
        rightmid.push_back(mid[i]);
    }

    head->left  = ReConstructTree(leftpre,leftmid);
    head->right = ReConstructTree(rightpre,rightmid);

    return head;
}

7. 树的子结构

题目:输入两棵二叉树A,B,判断B是不是A的子结构。

思路:从树A的根节点开始寻找与B根节点相同的节点,如果相同,则继续比较其他节点,不相同,则继续。

struct treeNode {
 int val;
 treeNode * left;
 treeNode * right;
 treeNode(int x) : val(x), left(NULL), right(NULL) {}
};

bool IsEqualTree(treeNode* pRoot1, treeNode* pRoot2)
{
    if (pRoot2 == NULL)
        return true;
    if (pRoot1 == NULL)
        return false;
    if (pRoot1->val != pRoot2->val)
        return false;

    return IsEqualTree(pRoot1->left,pRoot2->left) && IsEqualTree(pRoot1->right,pRoot2->right);
}

bool HasSubTree(treeNode* pRoot1, treeNode* pRoot2)
{
    bool result = false;
    if (pRoot2 != NULL && pRoot1 != NULL)
    {
        if (pRoot1->val == pRoot2->val)  //头结点相同
        {
            result = IsEqualTree(pRoot1,pRoot2); //比较子节点
        }
        if (!result)
        {
            result = HasSubTree(pRoot1->left, pRoot2); //继续找头结点相同的节点
        }
        if (!result)
        {
            result = HasSubTree(pRoot1->right, pRoot2);
        }
    }
    return result;
}

8. 二叉树的镜像

题目:输入一颗二叉树,输出源二叉树的镜像。

思路:递归转换二叉树即可。

struct treeNode {
 int val;
 treeNode * left;
 treeNode * right;
 treeNode(int x) : val(x), left(NULL), right(NULL) {}
};

treeNode* imageTree(treeNode* pRoot)
{
    if (!pRoot) return NULL;

    treeNode* iRoot = new treeNode(pRoot->val);

    iRoot->left  = imageTree(pRoot->right);
    iRoot->right = imageTree(pRoot->left);

    return iRoot;
}

9. 二叉树的下一个节点

题目:给定一个二叉树和其中的一个结点,请找出中序遍历顺序的下一个结点并且返回。注意,树中的结点不仅包含左右子结点,同时包含指向父结点的指针。

思路:1.如果存在右子节点,则右子节点的最左节点为下一个节点; 2.如果不存在右子节点,且该节点是父节点的左子节点,则父节点为下一个节点; 3.如果不存在右子节点,且该节点是父节点的右子节点,则向上找到一个节点是该其父节点的左孩子,则该其父节点为下一个节点。

struct treeNode {
    int val;
    treeNode * left;
    treeNode * right;
    treeNode * next;
    treeNode(int x) : val(x), left(NULL), right(NULL), next(NULL) {}
};

treeNode* GetNext(treeNode *pNode)
{
    if (pNode == NULL)
        return NULL;
    // 1.如果存在右子节点,则右子节点的最左节点为下一个节点
    if (pNode->right)
    {
        pNode = pNode->right;
        while(pNode->left)
        {
            pNode = pNode->left;
        }
        return pNode;
    }
    // 2.如果不存在右子节点,且该节点是父节点的左子节点,则父节点为下一个节点
    if (!pNode->right)
    {
        if(pNode->next && pNode->next->left == pNode)
        {
            return pNode->next;
        }
    }
    // 3.如果不存在右子节点,且该节点是父节点的右子节点,则向上找到一个节点是该其父节点的左孩子,则该其父节点为下一个节点
    if (!pNode->right)
    {
        if(pNode->next && pNode->next->right == pNode)
        {
            pNode = pNode->next;
            while (pNode->next)
            {
                if (pNode->next->left == pNode)
                    return pNode->next;
                pNode = pNode->next;
            }
        }
    }
    return NULL;
}

三、字符串

1. 正则表达式的匹配

题目:输入一个字符串和其模式字符串,判断两者是否正则匹配。模式中的字符'.'表示任意一个字符,而'*'表示前一个字符重复出现任意次(包含0次)。

思路:按照pattern下个字符为'*'进行递归判断。

bool match(const char* str, const char* pattern)
{
    if (str == NULL || pattern == NULL)
        return false;
    // 字符串都匹配结束,返回true
    if (*str == '\0' && *pattern == '\0')
        return true;
    // pattern匹配结束,str还没匹配结束,返回false
    if (*str != '\0' && *pattern == '\0')
        return false;
    // pattern下个字符为'*'
    if (*(pattern+1) == '*')
    {
        // 1. 当前字符相等,则str后移一位或者pattern后移二位
        if (*str == *pattern || (*str != '\0' && *pattern == '.'))
        {
            return match(str+1, pattern) || match(str, pattern+2);
        }
        // 2. 当前字符不相等,则pattern后移二位
        else
        {
            return match(str, pattern+2);
        }
    }
    // pattern下个字符不是'*'
    else
    {
        // 当前字符相等,则str和pattern均后移一位,继续比较下一位
        if (*str == *pattern || (*str != '\0' && *pattern == '.'))
            return match(str+1, pattern+1);
        else
            return false;
    }
}

2. 表示数值的字符串

题目:输入一个字符串,判断该字符串是否表示数值(包括整数、小数、正负数、科学计数法等)

思路:按照当前字符为正负号、小数点、指数e进行循环判断。

bool IsNumeric(char * str)
{
    if (str == NULL)
        return false;
    // 第一位为符号位
    if (*str == '+' || *str == '-')
        ++str;
    // 第一位不为数字,且不为'.'
    else if ((*str < '0' || *str > '9') && *str != '.')
        return false;
    bool flag = false; //是否出现过'.'或者'e'
    while (*str != '\0')
    {
        // 当前字符为符号,且前一个字符不为'e',不为最后一个数
        if ((*str == '+' || *str == '-') && *(str-1) != 'e' && *(str-1) != 'E' && *(str+1) >= '0' && *(str+1) <= '9')
            return false;
        // 当前字符为'.'
        if (*str == '.')
        {
            // 已经出现或者最后一位为'.'或者'e'或者'E'
            if (flag || *(str+1) == '\0')
                return false;
            else
                flag = true;
        }
        // 当前字符为'e'或者'E'
        else if (*str == 'e' || *str == 'E')
        {
            // 已经出现或者最后一位为'.'或者'e'或者'E'
            if (flag || *(str+1) == '\0')
                return false;
            else {
                flag = true;
            }
        }
        else if(*str < '0' || *str > '9')
        {
            return false;
        }
        ++str;
    }
    return true;
}

3. 第一个只出现一次的字符

题目:在一个字符串(0<=字符串长度<=10000,全部由字母组成)中找到第一个只出现一次的字符,并返回它的位置, 如果没有则返回 -1(需要区分大小写)。

思路:利用map形成键值对,用于存储字符串中每个字符出现的次数,最后再遍历一遍字符串,找到第一个map值为1的索引即可。

int FirstNotRepeatingChar(string str)
{
    std::map<char,int> map;
    for (int i=0;i < str.size();++i)
    {
        map[str[i]]++;
    }
    for (int i=0;i < str.size();i++)
    {
        if (map[str[i]] == 1)
            return i;
    }
    return -1;
}

4. 左旋转字符串

题目:对于一个给定的字符序列S,请你把其循环左移K位后的序列输出。例如,字符序列S=”abcXYZdef”,要求输出循环左移3位后的结果,即“XYZdefabc”。

思路:拼接两次字符序列,然后从第n位开始取字符序列长度的字符串即可。例如,SS=”abcXYZdefabcXYZdef”,左移3位的话,从SS的第3个字符开始取,则翻转后的字符串为“XYZdefabc”。

string reversal(string str, int k)
{
    string doubleS = str + str;
    string reverS;
    for (int i=k;i < str.size()+k;i++)
    {
        reverS += doubleS[i];
    }
    return reverS;
}

四、数组

1. 构建乘积数组

题目:给定一个数组A[0,1,...,n-1],请构建一个数组B[0,1,...,n-1],其中B中的元素B[i]=A[0]*A[1]*...*A[i-1]*A[i+1]*...*A[n-1],不能使用除法。

思路:用矩阵的方式,先计算左下三角,再计算右上三角,将B[i]=A[0]*A[1]*...*A[i-1]*A[i+1]*...*A[n-1]看成两部分来计算,即:A[0]*A[1]*...*A[i-1]和 *A[i+1]*...*A[n-1]两部分的乘积就是B[i]。

vector<int> multiply(vector<int> A) {
    int num = A.size();
    vector<int> B1(num,1);
    vector<int> B2(num,1);
    vector<int> B(num,1);

    // 计算左下角B1
    for (int i=1;i < num;++i)
    {
        B1[i] = B1[i-1]*A[i-1];
    }
    // 计算右上角B2
    for (int i=num-2;i >= 0;--i)
    {
        B2[i] = B2[i+1]*A[i+1];
    }
    // 计算矩阵B
    for (int i=0;i < num;i++)
    {
        B[i] = B1[i]*B2[i];
    }
    return B;
}

2. 二维数组的查找

题目:在一个二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。

思路:从右上角开始,若小,向下走,删除一行,若大,向左走,删除一列。

bool FindElement(vector<vector<int>> vecs, int target)
{
    if (vecs.empty())
        return false;
    int m = 0;
    int n = vecs[0].size()-1;
    while (m < vecs.size() && n >= 0)
    {
        if (vecs[m][n] == target)
            return true;
        else if (vecs[m][n] < target)
            m++;
        else
            n--;
    }
    return false;
}

3. 连续子数组的最大和

题目:给一个数组,返回它的最大连续子序列的和。

思路:利用动态规划解决,dp[i] = max(vec[i],dp[i-1]+vec[i])

int MaxContinuesSubarraySum(vector<int> vec)
{
    vector<int> dp(vec.size(),0);
    for (int i = 1;i < vec.size();i++)
    {
        dp[i] = vec[i] > (dp[i-1]+vec[i])?vec[i]:(dp[i-1]+vec[i]);
    }
    int max = 0;
    for (int i = 0;i < dp.size();++i)
    {
        if (max < dp[i])
            max = dp[i];
    }
    return max;
}

参考文献

1. 《剑指offer Java版本》

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值