剑指offer学习笔记:5.3 时间效率与空间效率的平衡

面试题34:丑数

我们把只包含因子2,3,5的数称为丑数。求按从小到大排列的第1500个丑数。例如,6,8都是丑数,但是14不是。习惯上,我们把1当做第一个丑数。
leetcode链接 https://leetcode-cn.com/problems/chou-shu-lcof/

class Solution {
public:
   int min(int a, int b, int c)
   {
       int tmp;
       if (a > b) {tmp = b;}
       else {tmp = a;}
       if (c < tmp) { tmp = c; }
       return tmp;
   }
   int nthUglyNumber(int n) {
       if (n <= 0)
       {
           return 0;
       }
       int result[n];
       result[0] = 1;
       int index = 1;
       int* T2 = result;
       int* T3 = result;
       int* T5 = result;
       while(index < n)
       {
           result[index] = min(2*(*T2), 3*(*T3), 5*(*T5));
           while(2*(*T2) <= result[index])
           {
               T2++;
           }
           while(3*(*T3) <= result[index])
           {
               T3++;
           }
           while(5*(*T5) <= result[index])
           {
               T5++;
           }
           ++index;
       }
       return result[n-1];
   }
};

解题思路:第一种思路遍历,判断每个数是不是丑数。会超时

class Solution {
public:
    bool checkUgly(int i)
    {
        while(i % 2 == 0)
        {
            i = i / 2;
        }
        while(i % 3 == 0)
        {
            i = i / 3;
        }
        while(i % 5 == 0)
        {
            i = i / 5;
        }
        return i == 1;
    }
    int nthUglyNumber(int n) {
        int result = 0;
        int i = 1;
        while(result < n)
        {
            if (checkUgly(i))
            {
                // cout << i << endl;
                result++;
            }
            i++;
        }
        return --i;
    }
};

解题思路二:根据丑数的定义,丑数应该是另一个数*2,*3或*5的结果。因此只要创建一个数组,里面的数字是排好序的丑数,每一个丑数都是前面丑数*2,*3,*5获得即可。因此重点在于怎么排好序。
我们首先考虑数组中已经有若干排好序的数组,,并将最大的丑数记录为M,接下来我们将分析如何生成下一个丑数。改丑数一定是前面某一个丑数*2, *3, *5的结果,因此我们先考虑将所有丑数都*2。这样一定会有若干小于M的结果和若干大于M的结果。由于是按照顺序生成的,小于M的结果一定已经在数组中,可以忽略,在若干大于M的数中,我们只取第一个大于M的结果称为M2,因为数组是按由小到大顺序生成的,其他更大的结果以后再说。同理我们可以找到M3,M5,并从M2,M3,M5中选取最小值,作为下一个丑数。
前面分析的时候,将已有所有丑数都进行*2操作,但是其实这个是没有必要的。因为数组中的数是按大小排序的,一定存在某一个丑数T2,其前面每个丑数乘2后小于M,后面每个都大于M,我们只需要记下这个丑数的位置,每次生成新的丑数的时候,去更新T2的位置。同理T3和T5。

class Solution {
public:
    int min(int a, int b, int c)
    {
        int tmp;
        if (a > b) {tmp = b;}
        else {tmp = a;}
        if (c < tmp) { tmp = c; }
        return tmp;
    }
    int nthUglyNumber(int n) {
        if (n <= 0)
        {
            return 0;
        }
        int result[n];
        result[0] = 1;
        int index = 1;
        int* T2 = result;
        int* T3 = result;
        int* T5 = result;
        while(index < n)
        {
            result[index] = min(2*(*T2), 3*(*T3), 5*(*T5));
            while(2*(*T2) <= result[index])
            {
                T2++;
            }
            while(3*(*T3) <= result[index])
            {
                T3++;
            }
            while(5*(*T5) <= result[index])
            {
                T5++;
            }
            ++index;
        }
        return result[n-1];
    }
};

面试题35:第一个只出现一次的字符

在字符串中找到第一个只出现一次的字符。如输入"abaccdeff",输出b。
leetcode链接 https://leetcode-cn.com/problems/di-yi-ge-zhi-chu-xian-yi-ci-de-zi-fu-lcof/

class Solution {
public:
   char firstUniqChar(string s) {
       int arr[256];
       for(int i = 0; i < 256; i++)
       {
           arr[i] = 0;
       }
       for(int i = 0; i < s.length(); i++)
       {
           arr[s[i] - '0']++;
       }
       for(int i = 0; i < s.length(); i++)
       {
           if (arr[s[i] - '0'] == 1)
           {
               return s[i];
           }
       }
       char result = ' ';
       return result;
       /*  map
       map<char, int> sNum;
       for(int i = 0; i < s.length(); i++)
       {
           sNum[s[i]]++;
       }
       for(int i = 0; i < s.length(); i++)
       {
           if (sNum[s[i]] == 1)
           {
               return s[i];
           }
       }
       char result = ' ';
       return result;
       */
   }
};

解题思路:先遍历一遍,用hash表记录每个字符出现次数。然后遍历第二遍,遍历时判断出现次数是1,返回字符,程序结束。书中用了256的int数组,每个位置存字符出现次数模拟hash表,不明白为什么不用map。然后自己尝试了下,发现map时间会长好多。
map和数组模拟

本题扩展:考虑如果字符换成汉字,有成千上万个,怎么解决
答:我感觉,用map吧,不用考虑预留空间问题。确定字符种类是的就用数组模拟hash,否则就用map。

相关题目:定义一个函数,输入两个字符串,从第一个字符串删除在第二个字符串中出现的所有字符。

int main() {
   string a = "aabbcde";
   string b = "aac";
   map<char, int> bMap;
   for(int i = 0; i < b.length(); i++)
   {
       bMap[b[i]]++;
   }
   string result = "";
   for(int i = 0; i < a.length(); i++)
   {
       if (bMap[a[i]] == 0)
       {
           result += a[i];
       }
   }
   a = result;
   cout << a << endl;
   }

解题思路:用hash表存储第二个字符串,判断是不是在hash表中即可。如果第一个字符串长度是n,则时间复杂度o(n)

相关题目:定义一个函数,删除其中重复出现的字符

int main() {
   string a = "aabbcde";
   string b = "aac";
   map<char, int> bMap;
   for(int i = 0; i < b.length(); i++)
   {
       bMap[b[i]]++;
   }
   string result = "";
   for(int i = 0; i < b.length(); i++)
   {
       if (bMap[b[i]] == 1)
       {
           result += b[i];
       }
   }
   cout << result;
   }

解题思路:把出现过元素存入hash表,初始化将所有元素设为false,遍历每个元素进行判断,第一次出现改为true,若再次遇到发现是true,则删除。时间复杂度o(n)

相关题目:在英语中,如果两个单词出现的字母相同,并且每个字母出现的次数也相同,那么这两个单词互为变位词。写一个函数,判断两个字符串是不是互为变位词。
leetcode链接 https://leetcode-cn.com/problems/valid-anagram/

class Solution {
public:
   bool isAnagram(string s, string t) {
       int sHash[256], tHash[256];
       if (s.length() != t.length())
       {
           return false;
       }
       for(int i = 0; i < 256; i++)
       {
           sHash[i] = 0;
           tHash[i] = 0;
       }
       for(int i = 0; i < s.length(); i++)
       {
           sHash[s[i]]++;
       }
       for(int i = 0; i < t.length(); i++)
       {
           tHash[t[i]]++;
       }
       for(int i = 0; i < 256; i++)
       {
           if (sHash[i] != tHash[i])
           {
               return false;
           }
       }
       return true;
   }
};

解题思路:先判断长度是否相等,不相等false。第一个串遍历,建立hash表。第二个串遍历,相同元素hash值-1,若有元素数量不够-1,则返回false。

面试题36:数组中的逆序对

在数组中的两个数字,如果前面一个数大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求其中逆序对的总数。
leetcode链接 https://leetcode-cn.com/problems/shu-zu-zhong-de-ni-xu-dui-lcof/

解题思路:将数组分成子数组,统计子数组内部逆序对的数目,然后再统计相邻子数组之间逆序对的数目。在统计逆序对的过程中,还需要对数组进行排序。其实就是归并排序的变种。
判断逆序对过程

面试题37:两个链表的第一个公共结点

输入两个链表,找到他们的第一个公共节点。
leetcode链接 https://leetcode-cn.com/problems/liang-ge-lian-biao-de-di-yi-ge-gong-gong-jie-dian-lcof/

/**
* Definition for singly-linked list.
* struct ListNode {
*     int val;
*     ListNode *next;
*     ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
   ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
       stack<ListNode*> stackA, stackB;
       while(headA != NULL)
       {
           stackA.push(headA);
           headA = headA->next;
       }
       while(headB != NULL)
       {
           stackB.push(headB);
           headB = headB->next;
       }
       ListNode* result = NULL;
       while(!stackA.empty() && !stackB.empty())
       {
           if (stackA.top() != stackB.top())
           {
               break;
           }
           result = stackA.top();
           stackA.pop();
           stackB.pop();
       }
       return result;
   }
};

解题思路:
思路1:考虑两个链表有公共节点,则两个链表尾节点一定相同,找到链表尾节点,往前回溯,即可找到公共节点。考虑链表只能从前往后,而从尾节点往前回溯是相反的,因此用栈结构进行存储

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        stack<ListNode*> stackA, stackB;
        while(headA != NULL)
        {
            stackA.push(headA);
            headA = headA->next;
        }
        while(headB != NULL)
        {
            stackB.push(headB);
            headB = headB->next;
        }
        ListNode* result = NULL;
        while(!stackA.empty() && !stackB.empty())
        {
            if (stackA.top() != stackB.top())
            {
                break;
            }
            result = stackA.top();
            stackA.pop();
            stackB.pop();
        }
        return result;
    }
};

思路2:不使用辅助内存。先遍历两个链表,求得其长度相差p。将指向较长链表指针先移动p步,之后两个链表指针一起移动,相遇即为公共节点。

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        int lenA = 0, lenB = 0;
        ListNode* tmpA = headA;
        ListNode* tmpB = headB;
        while(tmpA != NULL)
        {
            tmpA = tmpA->next;
            lenA++;
        }
        while(tmpB != NULL)
        {
            tmpB = tmpB->next;
            lenB++;
        }
        if (lenA > lenB)
        {
            while(lenA != lenB)
            {
                headA = headA->next;
                lenA--;
            }
        }
        if (lenB > lenA)
        {
            while(lenA != lenB)
            {
                headB = headB->next;
                lenB--;
            }
        }
        while(headA != NULL && headB != NULL)
        {
            if (headB == headA)
            {
                return headA;
            }
            headA = headA->next;
            headB = headB->next;
        }
        return NULL;
    }
};

相关题目:求二叉树中两个叶子节点的最近公共祖先
leecode链接 https://leetcode-cn.com/problems/er-cha-shu-de-zui-jin-gong-gong-zu-xian-lcof/

/**
* Definition for a binary tree node.
* struct TreeNode {
*     int val;
*     TreeNode *left;
*     TreeNode *right;
*     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
   TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
       // 1.递归法 遍历树,找到p和q分布在其左右两个子树中的节点,即为最近公共祖先
       // 简洁写法,每次都不能第一时间转过来
       if (root == NULL || root == p || root == q)
       {
           return root;
       }
       TreeNode* left = lowestCommonAncestor(root->left, p, q);
       TreeNode* right = lowestCommonAncestor(root->right, p, q);
       if (left == NULL)
       {
           return right;
       }
       if (right == NULL)
       {
           return left;
       }
       return root;
   }
};

思路1:如果二叉树中有指向根节点的指针,这个和上面那个链表题是完全一样的。观察下图,二叉树就是两个公共节点链表旋转90度

有公共节点的链表

思路2:遍历,当两个叶子节点分散在某个根节点的左右子树中。当前根节点就是最近的公共祖先。但是这个方法,每次需要遍历每个节点左子树和右子树,存在很大冗余。https://www.jianshu.com/p/68de70bbf8eb

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    bool findNode(TreeNode* root, TreeNode* tmp)
    {
        // 匹配那个繁琐写法的
        if (root == NULL)
        {
            return false;
        }
        if (root == tmp)
        {
            return true;
        }
        // cout << root->val << endl;
        bool left = findNode(root->left, tmp);
        bool right = findNode(root->right, tmp);
        return left || right;
    }
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        // 1.递归法 遍历树,找到p和q分布在其左右两个子树中的节点,即为最近公共祖先
        // 简洁写法,每次都不能第一时间转过来
        if (root == NULL || root == p || root == q)
        {
            return root;
        }
        TreeNode* left = lowestCommonAncestor(root->left, p, q);
        TreeNode* right = lowestCommonAncestor(root->right, p, q);
        if (left == NULL)
        {
            return right;
        }
        if (right == NULL)
        {
            return left;
        }
        return root;
        /* 繁琐但是好理解的写法
        // 需要写一个函数,传入根节点,判断p和q是否在其子树中
        if (root == NULL)
        {
            return NULL;
        }
        if (findNode(p, q))
        {
            return p;
        }
        if (findNode(q, p))
        {
            return q;
        }
        bool pFindRight = findNode(root->right, p);
        bool qFindRight = findNode(root->right, q);
        bool pFindLeft = findNode(root->left, p);
        bool qFindLeft = findNode(root->left, q);
        // cout << "pFindRight: " << pFindRight << " qFindRight: " << qFindRight << " pFindLeft: " << pFindLeft << " qFindLeft: " << qFindLeft << endl;
        if (pFindRight && qFindRight)
        {
            return lowestCommonAncestor(root->right, p, q);
        }
        else if (pFindLeft && qFindLeft)
        {
            return lowestCommonAncestor(root->left, p, q);
        }
        else if ((pFindLeft && qFindRight) || (pFindRight && qFindLeft))
        {
            return root;
        }
        else {
            return NULL;
        }
        return NULL;
        */
    }
};

思路3:将节点1和节点2的路径用栈记录下来,然后回溯栈。这样遍历一遍树结构就可以了。

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    int pn = 0;
    int qn = 0;
    void searchTree(TreeNode* root, TreeNode* p, TreeNode* q, stack<TreeNode*>& resultP, stack<TreeNode*>& resultQ, stack<TreeNode*>& tmp)
    {
        if (root == NULL)
        {
            return;
        }
        if (!resultP.empty() && !resultQ.empty())
        {
            return;
        }
        if (root == p)
        {
            stack<TreeNode*> a = tmp;
            resultP.push(root);
            while(!a.empty())
            {
                pn++;
                resultP.push(a.top());
                a.pop();
            }
            pn++;     
        }
        if (root == q)
        {
            qn++;
            stack<TreeNode*> a = tmp;
            resultQ.push(root);
            while(!a.empty())
            {
                qn++;
                resultQ.push(a.top());
                a.pop();
            }
        }
        tmp.push(root);
        searchTree(root->left, p, q, resultP, resultQ, tmp);
        searchTree(root->right, p, q, resultP, resultQ, tmp);
        tmp.pop();
        return;
    }
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        // 用栈记录遍历路径,并把有两个根节点的路径保存起来
        stack<TreeNode*> resultP, resultQ, tmp;
        searchTree(root, p, q, resultP, resultQ, tmp);
        TreeNode* pre = NULL;
        TreeNode* result = NULL;
        while(!resultP.empty() && !resultQ.empty())
        {
            // cout << "q: " << resultQ.top()->val << " " << endl;
            // cout << "p: " << resultP.top()->val << " " << endl;
            if(resultP.top() != resultQ.top())
            {
                break;
            }
            result = resultP.top();
            resultQ.pop();
            resultP.pop();
        }
        return result;
    }
};

leetcode上分别用思路2和思路3提交的计算时间,很神奇,思路三更慢,感觉是因为递归次数很多?
思路二和思路三
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,458评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,454评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,171评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,062评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,440评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,661评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,906评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,609评论 0 200
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,379评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,600评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,085评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,409评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,072评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,088评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,860评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,704评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,608评论 2 270