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月加油!

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