6月&剑指offer刷题记录

剑指offer刷题记录

一、贪心题

1、剪绳子

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

解题:

此题为贪心题,每次减掉3,最后剩下的有三种情况:

​ **情况1:**为0时,n%3==0,最大乘积为pow(3, n/3)

​ **情况2:**为1时,n%3==1,最大乘积为 pow(3, n/3-1) * 4 // 如n=10,余数为1,3 * 3 * 3 * 1 < 3 * 3 * 4

​ **情况3:**为2时,n%3==2,最大乘积为pow(3, n/3) * 2

int cutRope(int number) {
        if(number == 2 || number == 3)
            return number-1;
        int res = number/3;
        int m = number % 3;
        
        if(m == 0)
            return pow(3,res);
        else if(m == 1)
            return pow(3,res-1) * 4;
        else if(m == 2)
            return pow(3,res) * 2;
}

2、两两配对(腾讯笔试)

​ 小Q有M(M为偶数)名员工, 第i名员工完成工作的时候有一个拖延时间值ti。现在小Q手里有M/2份工作需要完成, 每一份工作都需要安排两名员工参与, 对于第i份工作所需完成的时间为两名员工的拖延时间值总和。

​ 现在M/2份工作同时开始进行,小Q希望所有工作结束的时间尽量早, 请你帮小Q设计一个优秀的员工分配方案,使得用尽量少的时间完成所有工作,并输出工作所需的最短时间。

示例1

输入

3
1 8
2 5
1 2

输出

10

说明

​ 拖延值为8的和拖延值为2的组队,两名拖延值为5的组队,所以完成工作的时间为10,这是时间最短的方案。

【解题思路】

贪心。最大的和最小的组合,次大的和次小的组合。

#include<bits/stdc++.h>

using namespace std;

struct node {
    int cnt;
    int time;
}pe[100005];

bool compare(node a, node b) {
    return a.time < b.time;
}

int main()
{
    int t;
    cin >> t;
    for(int i = 0; i < t; i++) {
        cin >> pe[i].cnt >> pe[i].time;
    }

    sort(pe,pe+t,compare);

    int res = 0;
    int left = 0, right = t-1;

    while(left <= right) {
        int number = min(pe[left].cnt,pe[right].cnt);
        
        number /= (left == right) ? 2 : 1;   //若left == right,number折半

        res = max(res,pe[left].time+pe[right].time);

        pe[left].cnt -= number; 
        pe[right].cnt -= number;

        left += (pe[left].cnt == 0) ? 1 : 0;
        right += (pe[right].cnt == 0) ? -1 : 0;
    }
    cout << res << endl;
    return 0;
}

二、递归题

1、机器人的运动范围

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

解题:

​ 此题为典型递归题,利用dfs深度搜索,从起点0,0开始,此解妙处在于利用一维数组flag记录访问状态,每次搜索时求得座标(i , j)在一维数组表示的值。index = i*cols+j; cols 为列值。还有就是单独写个函数,用来求行列的位数之和并返回。

int flag[10000];

int movingCount(int threshold, int rows, int cols)
{
    memset(flag,0,sizeof(flag));
    int res = dfs(0,0,threshold,rows,cols);
    return res;
}

int dfs(int i,int j,int thre,int rows,int cols) {
    int index = i*cols+j;  //用一维数组简化访问状态
    if(flag[index] || i < 0 || i >= rows || j < 0 || j >= cols ||
    (num(i)+num(j) > thre))
    return 0;
    flag[index] = 1;

    //判断上、下、左、右是否满足移动
    return dfs(i,j-1,thre,rows,cols) + 
    dfs(i,j+1,thre,rows,cols) + 
    dfs(i-1,j,thre,rows,cols) + 
    dfs(i+1,j,thre,rows,cols) + 1;
}

//计算行与列座标的位数之和
int num(int n) {
    int sum = 0;
    while(n) {
        sum += n%10;
        n /= 10;
    }
    return sum;
}

三、回溯题

1、矩阵中的路径

​ 请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一个格子开始,每一步可以在矩阵中向左,向右,向上,向下移动一个格子。如果一条路径经过了矩阵中的某一个格子,则该路径不能再进入该格子。 例如

a b c e

s f c s

a d e e

矩阵中包含一条字符串"bcced"的路径,但是矩阵中不包含"abcb"路径,因为字符串的第一个字符b占据了矩阵中的第一行第二个格子之后,路径不能再次进入该格子。

题解:

​ 此题利用递归+回溯即可求解。这里我是选择使用c++解题的,所以hasPath传参为指针,其实也就是一维数组,这里可以根据座标(i , j)求得matrix中的位置,因此首先求得matrix的长度,然后初始化一个访问状态flag数组,用来记录是否访问过某个座标。此题需要寻得str路径,那么此题可以直接遍历rows,cols中的所有座标。

bool hasPath(char* matrix, int rows, int cols, char* str)
{
    int n = strlen(matrix);
    int flag[n+5];   
    memset(flag,0,sizeof(flag));

    for(int i = 0; i < rows; i++) {
        for(int j = 0; j < cols; j++) {
            if(judgePath(matrix,i,j,rows,cols,str,flag,0)){
                return true;
            }
        }
    }
    return false;
}
    
bool judgePath(char* matrix, int i, int j, int rows, int cols, char* str, 
               int flag[], int k) {
    int index = i * cols + j;     //求得二维座标在一维中的index
    if(i < 0 || i >= rows || j < 0 || j >= cols || 
       matrix[index] != str[k] || flag[index] == 1)
        return false;

    flag[index] = 1;

    if(k == strlen(str)-1)    //当匹配到str中最后一位时,匹配成功
        return true;

    //判断上、下、左、右是否满足
    if(judgePath(matrix,i-1,j,rows,cols,str,flag,k+1) ||
       judgePath(matrix,i+1,j,rows,cols,str,flag,k+1) ||
       judgePath(matrix,i,j-1,rows,cols,str,flag,k+1) ||
       judgePath(matrix,i,j+1,rows,cols,str,flag,k+1)) {
        return true;
    }
    
    //此处需要更改状态,恰是回溯最重要的一步,因为起点可以为任何一个点
    flag[index] = 0;
    
    return false;
}

四、队列题

1、滑动窗口的最大值

​ 给定一个数组和滑动窗口的大小,找出所有滑动窗口里数值的最大值。例如,如果输入数组{2,3,4,2,6,2,5,1}及滑动窗口的大小3,那么一共存在6个滑动窗口,他们的最大值分别为{4,4,6,6,6,5}; 针对数组{2,3,4,2,6,2,5,1}的滑动窗口有以下6个: {[2,3,4],2,6,2,5,1}, {2,[3,4,2],6,2,5,1}, {2,3,[4,2,6],2,5,1}, {2,3,4,[2,6,2],5,1}, {2,3,4,2,[6,2,5],1}, {2,3,4,2,6,[2,5,1]}。

解题:

​ 方法1:最直接方法,两个循环模拟窗口判断

vector<int> maxInWindows(const vector<int>& num, unsigned int size)
{
    vector<int> res;
    int len = num.size();
    if(len < size || size == 0)    //若数组长度小于窗口大小,或窗口大小为0,返回空res
        return res;

    for(int i = 0; i <= len-size; i++) {    //滑动窗口个数
        int maxn = num[i];

        for(int j = i+1; j < i+size; j++) {   //滑动窗口值的比较
            if(maxn < num[j])
                maxn = num[j];
        }
        res.push_back(maxn);
    }

    return res;
}

​ 方法2:双向队列

vector<int> maxInWindows(const vector<int>& num, unsigned int size)
{
    vector<int> res;
    deque<int> q;

    int len = num.size();
    if(len < size || size == 0)   //若数组长度小于窗口大小,或窗口大小为0,返回空res
        return res;

    for(int i = 0; i < len; i++) {
        //从后面依次弹出队列中比当前num值小的元素,同时也能保证队列首元素为当前窗口最大值下标
        while(q.size() && num[q.back()] <= num[i])  
            q.pop_back();
        
        //队首元素已不再是新窗口的最大值,需弹出
        while(q.size() && i-q.front()+1 > size)
            q.pop_front();

        q.push_back(i);

        if(i+1 >= size)  //当够窗口大小时,即可添加值了
            res.push_back(num[q.front()]);
    }

    return res;
}

五、数组题

1、数据流中的中位数

​ 如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。我们使用Insert()方法读取数据流,使用GetMedian()方法获取当前读取数据的中位数。

解题:

定义一个数组,没插入一次进行一次排序,获取中位数时利用数组长度判断一下

vector<int> nums;
    
void Insert(int num)
{
    nums.push_back(num);
    sort(nums.begin(), nums.end());
}

double GetMedian()
{ 

    int len = nums.size();
    if(len % 2 == 1)
        return nums[len/2]*1.0;

    return (nums[len/2]+nums[len/2-1])/2.0;
}

2、表示数值的字符串

​ 请实现一个函数用来判断字符串是否表示数值(包括整数和小数)。例如,字符串"+100",“5e2”,"-123",“3.1416"和”-1E-16"都表示数值。 但是"12e",“1a3.14”,“1.2.3”,"±5"和"12e+4.3"都不是。

解题:

​ 简单模拟

bool isNumeric(char* string)
{
    // 标记符号、小数点、e是否出现过
    bool sign = false, decimal = false, isE = false;

    for(int i = 0; i < strlen(string); i++) {
        if(string[i] == 'e' || string[i] == 'E') {
            if(i == strlen(string)-1)
                return false;   // e后面一定要接数字
            if(isE)
                return false;    // e不能出现两次
            isE = true;
        } else if(string[i] == '+' || string[i] == '-') {
            // 第二次出现+-符号,则必须紧接在e之后
            if(sign && string[i-1] != 'e' && string[i-1] != 'E')
                return false;
            // 第一次出现+-符号,且不是在字符串开头,则也必须紧接在e之后
            if(!sign && i > 0 && string[i-1] != 'e' && string[i-1] != 'E')
                return false;
            sign = true;
        } else if(string[i] == '.') {
            // e后面不能接小数点,小数点不能出现两次
            if(isE || decimal)
                return false;
            decimal = true;
        } else if (string[i] < '0' || string[i] > '9') // 不合法字符
            return false;
    }

    return true;
}

3、字符流中第一个不相等的字符

​ 请实现一个函数用来找出字符流中第一个只出现一次的字符。例如,当从字符流中只读出前两个字符"go"时,第一个只出现一次的字符是"g"。当从该字符流中读出前六个字符“google"时,第一个只出现一次的字符是"l"。

​ 如果当前字符流没有存在出现一次的字符,返回#字符。

解题:

​ 利用一个int型数组表示256个字符,这个数组初值置为0,遍历已给的字符串,计算出每个字符出现的次数,返回结果时直接从前往后遍历,返回第一个统计值为1的字符

string str;
int flag[256];

//插入一个字符
void Insert(char ch)
{
    str += ch;
    flag[ch]++;
}

//返回字符串中第一个出现一次的字符
char FirstAppearingOnce()
{
    for(int i = 0; i < str.size(); i++) {
        if(flag[str[i]] == 1) {
            return str[i];
        }
    }

    return '#';
}

4、构建乘积数组

​ 给定一个数组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[0] = A[1] * A[2] * … * A[n-1],B[n-1] = A[0] * A[1] * … * A[n-2];)

解题:

​ 题目读了好久才读懂的。题目意思可以简化成一个矩阵,左对角线上的值为除了该元素的其他值的乘积

img

那么改题的解可以理解为:先累乘下三角行的乘积,然后累乘上三角行的乘积,最后返回最终求的结果即可

vector<int> multiply(const vector<int>& A) {
    int len = A.size();
    vector<int> res(len);     //累乘结果

    res[0] = 1;

    for(int i = 1; i < len; i++) {     //下三角行的乘积,累乘到res[i]行中
        res[i] = res[i-1]*A[i-1];
    }

    int t = 1;    //累乘变量
    for(int i = len-2; i >= 0; i--) {    //上三角行的乘积,累乘到res[i]行中
        t *= A[i+1];
        res[i] *= t;
    }

    return res;   //返回结果
}

5、数组中重复的数字

​ 在一个长度为n的数组里的所有数字都在0到n-1的范围内。 数组中某些数字是重复的,但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任意一个重复的数字。 例如,如果输入长度为7的数组{2,3,1,0,2,5,3},那么对应的输出是第一个重复的数字2。

解题:

​ 遍历数组时,利用map统计数字的次数,若大于1,返回结果

bool duplicate(int numbers[], int length, int* duplication) {
    map<int,int> mp;

    for(int i = 0; i < length; i++) {
        mp[numbers[i]]++;
        if(mp[numbers[i]] > 1) {
            *duplication = numbers[i];
            return true;
        }
    }

    return false;
}

6、把字符串转换为整数

​ 将一个字符串转换成一个整数,要求不能使用字符串转换整数的库函数。 数值为0或者字符串不是一个合法的数值则返回0

输入描述:

输入一个字符串,包括数字字母符号,可以为空

输出描述:

如果是合法的数值表达则返回该数字,否则返回0

示例1

输入

+2147483647
1a33

输出

2147483647
0

解题:

​ 模拟题,注意边界处理

int StrToInt(string str) {
    int len = str.size();
    if(len == 0)
        return 0;

    int sum = 0,i;

    if(str[0] == '+' || str[0] == '-')
        i = 1;
    else
        i = 0;

    for(; i < len; i++) {
        if(!(str[i] >= '0' && str[i] <= '9'))
            return 0;
        else {
            if(str[0] != '-' && (INT_MAX - (sum*10)) < (str[i] - '0'))
                return 0;
            if((INT_MAX - (sum*10)) < (str[i] - '1'))
                return 0;
            sum = sum*10 + (str[i] - '0');
        }
    }
    if(str[0] == '+' || (str[0] >= '0' && str[0] <= '9'))
        return sum;
    else
        return sum * -1;
}

六、二叉树

1、二叉搜索树的第k个结点

​ 给定一棵二叉搜索树,请找出其中的第k小的结点。例如, (5,3,7,2,4,6,8) 中,按结点数值大小顺序第三小结点的值为4。

解题:

​ 此题利用二叉搜索树的性质,使用中序遍历结果是有序的,中序遍历每一个结点存入数组,最后返回第k个结点即可,注意边界的处理

vector<TreeNode*> res;   //存放二叉搜索树的每一个结点

TreeNode* KthNode(TreeNode* pRoot, int k)
{
    if(pRoot == NULL || k <= 0)         //若二叉树为空,或k<=0时返回NULL
        return NULL;

    midVisit(pRoot);     //中序遍历二叉树
    
    int len = res.size();     
    if(k > len)          //若第K个结点不存在,返回NULL
        return NULL;

    return res[k-1];     //下标从0开始,因此返回k-1
}

void midVisit(TreeNode* root){
    if(root == NULL)
        return;

    midVisit(root->left);

    res.push_back(root);

    midVisit(root->right);
}

2、把二叉树打印成多行

​ 从上到下按层打印二叉树,同一层结点从左至右输出。每一层输出一行。

解题:

​ 层次遍历二叉树,利用队列先进先出性质,按顺序访问每一层的每一个结点

vector<vector<int> > Print(TreeNode* pRoot) {
    vector<vector<int> > res;

    if(pRoot == NULL)
        return res;

    queue<TreeNode*> que;
    que.push(pRoot);

    while(!que.empty()) {
        int n = que.size();
        vector<int> de;
        for(int i = 0; i < n; i++) {
            TreeNode* root = que.front();
            que.pop();
            de.push_back(root->val);

            if(root->left != NULL)
                que.push(root->left);
            if(root->right != NULL)
                que.push(root->right);
        }
        res.push_back(de);
    }
    return res;
}

3、按Z字形顺序打印二叉树

​ 请实现一个函数按照之字形打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右至左的顺序打印,第三行按照从左到右的顺序打印,其他行以此类推。

解题:

​ 与层次遍历二叉树相似,利用队列先进先出性质,按顺序访问每一层的每一个结点。不同的是每隔一层需要反转后再加入结果集。

vector<vector<int> > Print(TreeNode* pRoot) {
    vector<vector<int> > res;

    if(pRoot == NULL)
        return res;

    queue<TreeNode*> que;
    que.push(pRoot);

    int flag = 1;   // z字形遍历标记

    while(!que.empty()) {
        int n = que.size();
        vector<int> de;
        for(int i = 0; i < n; i++) {
            TreeNode* root = que.front();
            que.pop();
            de.push_back(root->val);

            if(root->left != NULL)
                que.push(root->left);
            if(root->right != NULL)
                que.push(root->right);
        }
        if(flag%2 == 1)        //模2为1时直接添加,为0时需要先反转后再加入res 
            res.push_back(de);
        else
            reverse(de.begin(),de.end()),res.push_back(de);

        flag++;
    }
    return res;

}

4、对称的二叉树

​ 请实现一个函数,用来判断一颗二叉树是不是对称的。注意,如果一个二叉树同此二叉树的镜像是同样的,定义其为对称的。

解题:

​ 利用递归方法,首先根节点以及其左右子树,左子树的左子树和右子树的右子树比较,左子树的右子树和右子树的左子树比较:

​ 情况1:若左右同时为NULL,返回true

​ 情况2:若左右有一个为NULL,返回false

​ 情况3:若左右值不相等,返回false

bool isSymmetrical(TreeNode* pRoot)
{
    if(pRoot == NULL)
        return true;

    return judge(pRoot->left,pRoot->right);
}

bool judge(TreeNode* left, TreeNode* right) {
    if(left == NULL && right == NULL)  //若左右同时为NULL,返回true
        return true; 

    if(left == NULL || right == NULL)   //若左右有一个为NULL,返回false
        return false;

    if(left->val != right->val)     //若左右值不相等,返回false
        return false;

    //递归:左子树的左子树和右子树的右子树比较,左子树的右子树和右子树的左子树比较
    return judge(left->left,right->right) && judge(left->right,right->left);
}

5、二叉树的下一个结点

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

解题:

​ 当节点为空时,直接返回

​ 当右节点存在时,返回右节点的最左结点

​ 当右节点不存在时,向上寻找公共父节点

TreeLinkNode* GetNext(TreeLinkNode* pNode)
{
    if(pNode == NULL)
        return NULL;

    if(pNode->right != NULL)
    {
        pNode = pNode->right;
        while(pNode->left != NULL) {
            pNode = pNode->left;
        }
        return pNode;
    }

    while(pNode->next != NULL) {
        TreeLinkNode* node = pNode->next;
        if(pNode == node->left)
            return node;
        pNode = pNode->next;
    }

    return NULL;
}

七、链表题

1、删除链表中的重复结点

​ 在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。 例如,链表1->2->3->3->4->4->5 处理后为 1->2->5

解题:

​ 建立头结点,以方便碰到第一个与第二个节点就相同的情况;

​ 设置 pre ,last 指针, pre指针指向当前确定不重复的那个节点,而last指针用于往后遍历判断;

ListNode* deleteDuplication(ListNode* pHead)
{
    if(pHead == NULL || pHead->next == NULL)
        return pHead;

    ListNode* head = new ListNode(0);     //建立头结点
    head->next = pHead;

    ListNode* pre = head;          //当前确定不重复结点,用于直接指向
    ListNode* last = pHead;		  //用于遍历判断的结点

    while(last != NULL) {
        if(last->next != NULL && last->val == last->next->val) {
            while(last->next != NULL && last->val == last->next->val) {
                last = last->next;
            }
            pre->next = last->next;
            last = last->next;
        } else {
            pre = pre->next;       //改变当前确定不重复结点
            last = last->next;
        }
    }
    return head->next;            //返回结果为头结点的下一个结点
}

2、链表中环的入口结点

​ 给一个链表,若其中包含环,请找出该链表的环的入口结点,否则,输出null。

解题:

​ 利用环的特性,建立快慢指针:fast、slow, fast每次移动两个结点,slow每次移动一个结点,若链表存在环,则fast与last指针一定会相遇,此时任意一个结点回到头结点(如slow=pHead),然后再与fast指针并进,每次移动一个结点,直到指针相遇时,即是环的最终入口。

​ 大伙们只需在纸上画一下模拟一遍就能很快的理解了。

ListNode* EntryNodeOfLoop(ListNode* pHead)
{
    if(pHead == NULL || pHead->next == NULL || pHead->next->next == NULL)
        return NULL;

    ListNode *fast = pHead->next->next, *slow = pHead->next;

    while(fast != slow) {
        if(fast->next == NULL || fast->next->next == NULL)
            return NULL;
        fast = fast->next->next;
        slow = slow->next;
    }

    slow = pHead;
    while(fast != slow) {
        fast = fast->next;
        slow = slow->next;c
    }

    return fast;
}

后序会不断更新……
6月已结束,7月加油!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章