剑指offer总结

这篇博客总结了《剑指Offer》中涉及的算法题,包括数组中重复的数字、二维数组查找、字符串处理、链表操作、树结构问题、栈与队列的应用、递归与回溯法等,覆盖了数据结构与算法的多个方面。
摘要由CSDN通过智能技术生成

一、赋值运算符函数

思路:先用new分配新内容,再用delete释放已有内容。

CMyString& CMyString::operator=(const CMyString &str)
{
    // m_pData是类的一个私有成员属性
    if (this != &str) {
        CMyString strTemp(str); // 拷贝构造str放入strTemp
        char* pTemp = strTemp.m_pData; // 取出str的m_pData暂存
        strTemp.m_pData = m_pData; // 把要抛弃的值存入strTemp
        m_pData = pTemp; // 把暂存的值放入m_pData
    }
    return *this; // 函数结束,strTemp被自动释放
}

二、实现单例模式

思路:利用静态构造函数。解决创建时机过早的问题。

public sealed class Singleton5
{
    Singleton5()
    {
    }
    public static Singleton5 Instance
    {
        get
        {
            return Nested.instance;
        }
    }
    // 第一次通过属性Singleton5.Instance得到实例时,会走动调用Nested创建instance
    class Nested
    {
        static Nested()
        { 
        }
        internal static readonly Singleton5 instance = new Singleton5();
    }
}

三、数组中重复的数字

描述:长度n,数字在0~n-1。

题目一、可以修改数组

描述:在一个长度为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内。请找出数组中任意一个重复的数字。

思路:检查每个下标i,看numbers[i] == i, 如果不是,把numbers[i]和numbers[numbers[i]]交换。如果numbers[i]==i,说明找到了。

时间O(n) 空间O(1)

int findRepeatNumber(vector<int>& nums) {
    int n = nums.size();
    int i;
    for (i = 0; i < n; ++i) {
        while (nums[i] != i) {
            if (nums[i] == nums[nums[i]]) {
                return nums[i];
            }
            swap(nums[i], nums[nums[i]]);
        }
    }
    return nums[i];
}
题目二、不修改数组

思路:长度为n,数字范围0~n-1,取中间数字n/2,统计前半段比n/2小的数字的个数。出现次数大于n/2说明前半段有重复数字,继续分半。

int getDuplication(const int* numbers, int length)
{
    if (numbers == nullptr || length <= 0)
        return -1;
    int start = 1;
    int end = length - 1;
    while (end >= start) {
        int middle = ((end - start) >> 1) + start; // 取中位数
        int count = countRange(numbers, length, start, middle); // 统计数组中值在中位数前的个数
        // 首尾指向同一个位置,如果count>1说明有超过一个值为end的数,即为结果
        if (end == start) {
            if (count > 1)
                return start;
            else
                break;
        }
        // 如果count在前半段出现的次数比前半段从start到middle的数字多说明前半段有重复数组
        if (count > (middle - start + 1))
            end = middle;
        else
            start = middle + 1;
    }
    return -1;
}
int countRange(const int* numbers, int length, int start, int end)
{
    if (numbers == nullptr)
        return 0;
    int count = 0;
    for (int i = 0; i < length; ++i)
        if (numbers[i] >= start && numbers[i] <= end)
            ++count;
    return count;
}

四、二维数组中的查找

描述:每一行从左到右递增。每一列从上到下递增。输入一个整数,判断数组中是否含有该整数

思路:每次选取查找范围内的右上角数字。如果大于number则这一列不会有number,剔除。以此类推。如果小于number说明只会出现在右侧,但是右侧都已经被剔除了,所以只会在下面,剔除这一行。以此类推。

bool Find(int* matrix, int rows, int columns, int number)
{
    bool found = false;
    if (matrix != nullptr && rows > 0 && columns > 0) {
        int row = 0;
        int column = columns - 1;
        while (row < rows && column >= 0) {
            if (matrix[row * columns + column] == number) {
                found = true;
                break;
            } else if (matrix[row * columns + column] > number) {
                --column;
            } else {
                ++row;
            }
        }
    }
    return found;
}

五、替换空格

思路:先遍历一遍字符串统计空格总数。准备两个指针P1,P2。P1指向原始字符串末尾,P2指向替换后的字符串末尾。向前移动P1逐个复制到P2,直到碰到空格。碰到空格后,P1向前1格,P2向前移动3格插入"%20"。

时间O(n)

// length为字符数组的总容量
void ReplaceBlank(char string[], int length)
{
    if (string == nullptr || length <= 0)
        return;
    int originalLength = 0; // 字符串实际长度
    int numberOfBlank = 0;
    int i = 0;
    // 统计空格数量
    while (string[i] != '\0') {
        ++originalLength;
        if (string[i] == ' ')
            ++numberOfBlank;
        ++i;
    }
    int newLength = originalLength + numberOfBlank * 2;
    if (newLength > length)
        return; // 超过总容量
    int indexOfOriginal = originalLength; // P1
    int indexOfNew = newLength; // P2
    while (indexOfOriginal >= 0 && indexOfNew > indexOfOriginal) {
        // P1遍历到了空格
        if (string[indexOfOriginal] == ' ') {
            string[indexOfNew--] = '0';
            string[indexOfNew--] = '2';
            string[indexOfNew--] = '%';
        } else {
            string[indexOfNew--] = string[indexOfOriginal];
        }
        --indexOfOriginal;
    }
}

六、从尾到头打印链表

思路:栈,后进先出

struct ListNode
{
    int m_nKey;
    ListNode* m_pNext;
};
void PrintListReversingly_Iteratively(ListNode* pHead)
{
    std::stack<ListNode*> nodes;
    ListNode* pNode = pHead;
    while (pNode != nullptr) {
        nodes.push(pNode);
        pNode = pNode->m_pNext;
    }
    while (!nodes.empty()) {
        pNode = nodes.top();
        printf("%d\t", pNode->m_nValue);
        nodes.pop();
    }
}

七、重建二叉树

描述:输入二叉树前序和中序的结果,重建二叉树。结果中不含重复数字。

思想:前序第一个数字是根节点,在中序中找到它。根前面的就是左子树的值

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 != rootValue)
        ++rootInorder;
    // 没找到根节点
    if (rootInorder == endInorder && *rootInorder != rootValue)
        throw std::exception("Invalid input.");
    // 左子树长度
    int leftLength = rootInorder - startInorder;
    int* leftPreorderEnd = startPreorder + leftLength;
    if (leftLength > 0) {
        // 构建左子树
        root->m_pLeft = ConstructCore(startPreorder + 1, leftPreorderEnd, startInorder, rootInorder - 1);
    }
    if (leftLength < endPreorder - startPreorder) {
        // 构建右子树
        root->m_pRight = ConstructCore(leftPreorderEnd + 1, endPreorder, rootInorder + 1, endInorder);
    }
    return root;
}
class Solution {
public:
    vector<int> Preorder ;
    map<int,int> dic;
    
    TreeNode* build(int pre_root ,int in_left ,int in_right){
        //如果左边界大于右边界说明到过了叶子
        if(in_left > in_right){
            return NULL;
        }
        //pre_root 是先序里面的索引 !!
        TreeNode* root = new TreeNode(Preorder[pre_root]);
        //获取先序中的节点在中序中的节点, 即index 左边就是这节点的左子树,index右边就是节点的右子树
        int index = dic[Preorder[pre_root]];
        //当前节点左树即为先序索引+1 (没了话会在下一次迭代返回NULL)
        root->left = build(pre_root+1,in_left,index-1);
        //当前节点右树即为 根结点在前序中的索引+左树所有节点数(即节点在中序中的索引)-左边界+1 ,下一次的左边界为根在中序的索引+1  
        root->right = build(pre_root+index-in_left+1,index+1 ,in_right);
        return root;
    }

    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
        //赋值至外部变量
        Preorder = preorder;
        //使用map映射inorder的值和索引,提高找到索引效率
        for(int i=0;i<inorder.size();i++){
            dic[inorder[i]] = i;
        }
        return build(0,0,preorder.size()-1);
    }
};

八、二叉树的下一个节点

描述:给定一棵二叉树和其中的一个节点,找出中序下一个节点。(parent,left,right)

思路:①如果节点有右子树,下一个节点就是其右子树中最左子节点。②如果没有右子树,而且它是父节点的左子节点,下一个节点就是其父节点。③如果没有右子树,它还是它父节点的右子节点,则沿着父节点指针向上遍历,直到找到一个它是父节点的左子节点的节点。

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;
}

九、用两个栈实现队列

描述:实现appendTail, deleteHead

思路:先进后出->先进先出。stack2不空时,栈顶是最先进入队列的元素可以弹出。stack2空时,把stack1中逐个压入stack2,弹出

class CQueue {
    stack<int> stack1,stack2;
public:
    CQueue() {
        while (!stack1.empty()) {
            stack1.pop();
        }
        while (!stack2.empty()) {
            stack2.pop();
        }
    }
    
    void appendTail(int value) {
        stack1.push(value);
    }
    
    int deleteHead() {
        // 如果第二个栈为空
        if (stack2.empty()) {
            while (!stack1.empty()) {
                stack2.push(stack1.top());
                stack1.pop();
            }
        } 
        if (stack2.empty()) {
            return -1;
        } else {
            int deleteItem = stack2.top();
            stack2.pop();
            return deleteItem;
        }
    }
};

补充:两个队列实现一个栈

十、斐波那契数列

long long Fibonacci(unsigned n)
{
    int result[2] = {0, 1};
    if (n < 2)
        return result[n];
    long long fibNMinusOne = 1;
    long long fibNMinusTwo = 0;
    long long fibN = 0;
    for (unsigned int i = 2; i <= n; ++i) {
        fibN = fibNMinusOne + fibNMinusTwo;
        fibNMinusTwo = fibNMinusOne;
        fibNMinusOne = fibN;
    }
    return fibN;
}
题目二、青蛙跳台

描述:一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。

int numWays(int n) {
    long long res = 0, tmp1 = 1, tmp2 = 1;
    if (0 == n || 1 == n) {
        return 1;
    } else {
        for (int i = 2; i <= n; ++i) {
            res = (tmp1 + tmp2) % 1000000007;
            tmp2 = tmp1;
            tmp1 = res;
        }
    }
    return res;
}

十一、旋转数组的最小数字

描述:旋转数组->把一个数组最开始的若干个元素搬到数组的末尾。输入一个递增排序的数组的一个旋转,输出旋转数组的最小元素。例如,数组 [3,4,5,1,2][1,2,3,4,5] 的一个旋转,该数组的最小值为1。

思路:二分查找找到两个子数组分界线。两个指针分别指向首位。①一般情况,首指针>=尾指针。如果中间元素>=首指针,说明最小元素位于中间元素后面,更新首指针。同理更新尾指针,不断缩小范围。最终会指向两个相邻元素,尾指针指向的就是目标。②旋转后为排序数组本身,第一个元素就是目标。所以把indexMid初始化为首指针。③10111,11101采取顺序查找

int minArray(vector<int>& numbers) {
    int low = 0;
    int high = numbers.size() - 1;
    while (low < high) {
        int pivot = low + (high - low) / 2;
        // 最小值在pivot左侧
        if (numbers[pivot] < numbers[high]) {
            high = pivot;
        }
        // 最小值在pivot右侧
        else if (numbers[pivot] > numbers[high]) {
            low = pivot + 1;
        }
        // low = high时
        else {
            high -= 1;
        }
    }
    return numbers[low];
}

十二、矩阵中的路径

描述:判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一格开始,每一步可以在矩阵中向左、右、上、下移动一格。如果一条路径经过了矩阵的某一格,那么该路径不能再次进入该格子。

思路:回溯法。

public:
	bool exist(vector<vector<char>>& board, string word) {
        rows = board.size();
        cols = board[0].size();
        // 遍历所有的格子,以它们为起点
        for(int i = 0; i < rows; i++) {
            for(int j = 0; j < cols; j++) {
                if(dfs(board, word, i, j, 0)) return true;
            }
        }
        return false;
    }
private:
    int rows, cols;
    bool dfs(vector<vector<char>>& board, string word, int i, int j, int k) {
        if(i >= rows || i < 0 || j >= cols || j < 0 || board[i][j] != word[k]) return false;
        if(k == word.size() - 1) return true;
        // 经过并且采纳的格子不能重复经过
        board[i][j] = '\0';
        bool res = dfs(board, word, i + 1, j, k + 1) || dfs(board, word, i - 1, j, k + 1) || dfs(board, word, i, j + 1, k + 1) || dfs(board, word, i , j - 1, k + 1);
        // 把之前标记为经过的格子恢复
        board[i][j] = word[k];
        return res;
    }

十三、机器人的运动范围

描述:地上有一个m行n列的方格,从坐标 [0,0] 到坐标 [m-1,n-1] 。一个机器人从坐标 [0, 0] 的格子开始移动,它每次可以向左、右、上、下移动一格(不能移动到方格外),也不能进入行坐标和列坐标的数位之和大于k的格子。例如,当k为18时,机器人能够进入方格 [35, 37] ,因为3+5+3+7=18。但它不能进入方格 [35, 38],因为3+5+3+8=19。请问该机器人能够到达多少个格子?

思路:回溯法

// 求一个数字的位数和
int get(int x) {
    int res = 0;
    for (; x; x /= 10) {
        res += x % 10;
    }
    return res;
}
int movingCount(int m, int n, int k) {
    if (!k) {
        return 1;
    }
    // vis存储1/0, 表示这个格子能否到达。
    vector<vector<int>> vis(m, vector<int>(n, 0));
    int ans = 1;
    vis[0][0] = 1;
    for (int i = 0; i < m; ++i) {
        for (int j = 0; j < n; ++j) {
            if ((i == 0 && j == 0) || get(i) + get(j) > k) {
                continue;
            }
            // 边界判断。更新当前格子,看是否能到达。
            if (i - 1 >= 0) {
                vis[i][j] |= vis[i - 1][j];
            }
            if (j - 1 >= 0) {
                vis[i][j] |= vis[i][j - 1];
            }
            // 如果当前格子可以到达,vis[i][j]为1,更新结果
            ans += vis[i][j];
        }
    }
    return ans;
}

十四、剪绳子

描述:给你一根长度为 n 的绳子,请把绳子剪成整数长度的 m 段(m、n都是整数,n>1并且m>1),每段绳子的长度记为 k[0],k[1]…k[m-1] 。请问 k[0]*k[1]*…*k[m-1] 可能的最大乘积是多少?例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。

思路:贪婪算法。当n>=5时,尽可能多的剪长度为3的绳子。当剩下长度为4时,把剩下的剪成两段2.

int cuttingRope(int n) {
    if (n <= 3) {
        return n - 1;
    }
    // 尽可能多的剪去长度为3的绳子段
    int timesOf3 = n / 3;
    // 当剩下长度为4时
    if (n - timesOf3 * 3 == 1) {
        --timesOf3;
    }
    int timesOf2 = (n - timesOf3 * 3) / 2;
    return pow(3, timesOf3) * pow(2, timesOf2);
}

十五、二进制中1的个数

描述:请实现一个函数,输入一个整数(以二进制串形式),输出该数二进制表示中 1 的个数。例如,把 9 表示成二进制是 1001,有 2 位是 1。因此,如果输入 9,则该函数输出 2。

思路:如果一个二进制数!=0说明至少一位是1.①最右边一位是1,减去1时相当于给最后一位取反。②最后一位是0,减去1会把最右边的1变成0.

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

十六、数值的整数次方

描述:实现函数double Power(double base, int exponent),求base的exponent次方。不得使用库函数,同时不需要考虑大数问题。

思路:如果求x的32次方,可以把16次方做平方。以此类推,32次方需要5次乘法。
a n = { a n / 2 ⋅ a n / 2 如果  n 为偶数 a ( n − 1 ) / 2 ⋅ a ( n − 1 ) / 2 ⋅ a 如果  n 为奇数 a^n = \begin{cases} a^{n/2} \cdot a^{n/2} & \quad \text{如果 } n \text{为偶数}\\ a^{(n-1)/2} \cdot a^{(n-1)/2}\cdot a & \quad \text{如果 } n \text{为奇数} \end{cases} an={ an/2an/2a(n1)/2a(n1)/2a如果<

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值