读书笔记| 剑指offer

date: 2016-06-02 14:00

剑指 offer

何海涛blog: http://zhedahht.blog.163.com/
源码: http://download.csdn.net/download/cadcisdhht/3809923

读书笔记

程序的鲁棒性很重要, 要注意 边界条件, 特殊输入(null, 空字符串), 错误处理

面试相关

  • coding

思考清楚再开始编码
代码命名&缩进
能够单元测试

  • 面试

规划好路线和出行时间
穿着得体
注意面试邀请函里面的面试流程
准备几个问题

  • 项目经验(STAR)

situation: 简短的项目背景
task: 自己完成的任务
action: 为了完成任务做了哪些工作, 怎么做
result: 自己的贡献

简述 S, 突出 TAR

  • 项目相关问题

碰到的最大问题是什么? 怎么解决的?
从项目中学到什么?
什么时候会和其他成员有冲突? 什么样的冲突? 如何解决?

  • 掌握的技能

了解: 上过课或者读过书, 没有实际的项目
熟悉: 实际项目中应用
精通: 不要轻易使用

  • 为什么跳槽

避免: 老板太苛刻; 同事太难相处; 加班多; 工资低

  • 需要具备的素质

扎实的基础知识: 编程语言, 数据结构, 算法
高质量代码: 鲁棒, 稳定, 边界条件, 特殊输入(null, 空字符串), 错误处理
清晰的思路: 画图, 举例, 分治
优化效率: 时间&内存
综合能力: 编程能力, 技术功底, 沟通能力, 学习能力, 知识迁移, 抽象建模, 发散思维

  • 提问环节

不要问和职位没有关系问题(公司未来5年规划)
薪水
面试结果
职位或者项目相关, 需要提前做功课

  • 小提示

如果要修改原数据, 要提前询问
如果是在排序的数组中查找, 都可以使用二分法
通常递归实现更简洁容易实现, 如果没有特殊要求, 可以优先采用
写代码时最后使用完整的英文单词组合变量名和函数, 让人可以一眼读懂代码的意图
如果没有限定处置范围, 就要考虑是否存在 大数问题
要特别注意 鲁棒性
每一次指针操作都要注意是否可能为 NULL
摆放数字 -> 排列 + 组合
有关字符的问题都可以考虑是否可以使用 hash 表
面对面试问题主动提出高质量的问题

基础知识

  • c++

sizeof
<effective C++>: 面试之前突击c++
<C++ primer>: 全面了解 c++ 语法
<inside C++ object model>
<The C++ Programming Language>: 全面深入掌握 C++

  • c#

<Professional C#>: 有几章专门介绍 c# 和其他语言的区别
<CLR via C#>: 能够深入理解 装箱卸箱, 垃圾回收, 反射

  • 数据结构

数组: array -> STL vector -> c++11 array
字符串: char -> STL string
链表: 单向, 环形, 双向; 添加 / 删除 节点
树: 二叉树 前/中/后 序遍历, 宽度优先遍历
栈和队列: FILO / FIFO

  • 算法&数据操作

查找: 二分, 哈希表, 二叉排序树
排序: 冒泡, 插入, 快速, 归并
递归&循环
位运算: 世界上有 10 种人

代码规范

书写, 布局, 命名
代码完整性: 功能测试, 边界测试, 负面测试(错误输入)
错误处理方式: return code; 全局变量; 异常
鲁棒性: 容错性

解决问题的思路

抽象问题 -> 形象化, 画图
抽象问题 -> 具体化, 举例
复杂问题 -> 简单化(分治)

优化效率: 时间 + 空间

时间效率: 编程习惯; 循环/递归 实现; 数据结构+算法; 思维+追求完美
时间效率和空间效率平衡: 时间换空间不一定可行, 比如嵌入式设备存储有限
保存递归过程中的结果来调高时间效率

面试中的各项能力

沟通能力 / 学习能力 -> 善于提问
知识迁移能力: 稍微改变已知问题; 简单->复杂
抽象建模能力: 比如 斐波那契 f(n) = f(n-1) + f(n-2)
发散思维能力: 思维活动的多向性&变通性; 知识面的深度&广度

面试案例

案例一: 不要轻易说精通 -> 基本语法 -> 检查 空指针/空字符串 等情况 -> 错误处理方式 -> 不要主动在技术面询问薪资

案例二: 简要介绍项目(STAR) -> UML 图来分析项目 -> 树(逐渐缩小范围) -> 二叉搜索树 -> 指向根节点的指针(转化成链表) -> 使用辅助内存(转化成链表) -> 提问 项目/团队 相关的问题

problem

  • singleton(单例模式)

多线程环境 -> 锁资源 -> 减少锁资源操作(添加 if)
当然上面的做法, 都没有使用 静态构造函数 简洁

class Singleton{
    private static $instance;

    public static function getInstance(){
        if(static::$instance === null){
            static::$instance = new static();
        }
        return static::$instance;
    }
}
  • sizeof + array
int getSize(int a[]){
    return sizeof(a);
}
int a[] = {1,2,3,4,5};
int *b = a;
sizeof(a);  // 20
sizeof(b);  // 4, 指针大小都为 int
getSize(a); // 4, 作为参数传递, 会退化为指针
  • 二维数组中查找

选取右上角的数字

bool find(int arr[][], int rows, int columns, int number){
    bool found = false;
    if(arr!=NULL && rows>0 && columns>0){ // 鲁棒性
        int row=0, column=columns-1;
        while(row<rows && column>=0){
            if(arr[row][column] == number){ // 选取右上角的点
                found = true; break;
            }else(arr[row][column] < number) column--;
            else row++;
        }
    }
    return found;
}
  • 替换字符串中的空格

php: str_replace()
先遍历出空格的个数, 然后用2个指针从后往前遍历

  • 反向打印链表

  • 根据树的前序遍历和中序遍历

思路: 前序遍历第一个节点就是根节点, 根据根节点在中序遍历中获取左右子树, 然后递归

struct BinaryTreeNode
{
    int             m_nValue;
    BinaryTreeNode* m_pLeft;
    BinaryTreeNode* m_pRight;
};

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 = NULL;

    if(startPreorder==endPreorder){
        if(startInorder==endInorder && *startPreorder==*startInorder) return root;
        else throw exception("invalid input");
    }

    // 在 中序遍历 中查找根节点
    int* rootInorder = startInorder;
    while(rootInorder<=endInorder && *rootInorder!=rootValue) rootInorder++;
    if(rootInorder==endInorder && *rootInorder!=rootValue) throw 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;
}

BinaryTreeNode* Construct(int* preorder, int* inorder, int length){
    if(preorder==NULL || inorder==NULL || length<=0) return NULL;

    return ConstructCore(preorder, preorder+length-1, inorder, inorder+length-1);
}
  • 2个栈实现队列

进栈: A栈进数据
出栈: B栈出数据, 如果B栈为空, 将A栈数据进栈到B, 然后出栈

  • O(n) 实现排序

数据在较小范围内(比如 age), 使用辅助空间即可

  • 旋转数组的最小值

二分查找
特殊情况: 有重复数字

  • 斐波那契

直接递归 -> 循环(O(N)) -> 矩阵(O(logN))

类似: 青蛙一次跳1-2级; 青蛙一次跳1-n级(2^n-1); 1*2 矩阵拼图

  • 一个数二进制的 1 的个数

右移 -> 负数右移会死循环
使用 1 和数字进行 & 运算, 然后 左移 1, 重复 & 运算
自减 1 后再和原数进行 & 运算, 就是把最右边的 1 置为 0

类似: 一个数是不是 2 的整数次方; 2个数需要变换几次才能二进制相同(异或)

  • 实现 pow() 函数

指数为负数
使用全局变量 标识 基数指数都是 0 的情况
二分实现 n 次方

  • 打印 1 到最大的 n 位数

大数问题
转化为排列问题

  • O(1) 删除节点

实现当前节点替换当前节点的下一个节点
特殊情况: 只有一个节点

  • 调整数组顺序, 使奇数在偶数前面

2个指针, 一个在表头, 一个在表尾
将数组分为2部分: 抽象出比较方法

  • 链表中倒数第 k 个节点

2个指针, 一个先走 k-1 步
特殊情况: 链表为空, k=0, 链表是否有 k 个元素

类似: 链表中间节点; 判断链表是否有环

  • 反转链表

递归, 3个指针

  • 合并有序链表

  • 判断 子树

先遍历查找子树的根节点, 然后遍历看是否是子树

  • 数的镜像

递归, 交换左右子树

  • 顺时针打印矩阵

按照 4 个方向 打印即可
特殊情况: 最后可能退化成只有 1 行/列/点

  • 带 min 的栈

使用辅助栈
入栈: 压入最小元素到栈顶
出栈: 如果当前元素是最小元素, 辅助栈出栈

  • 栈的 压入 / 弹出 序列

按照入栈序列入栈, 遍历弹出序列, 如果元素在 栈和辅助栈栈顶, 弹出, 继续下一个;
如果不等, 压入 栈 -> 辅助栈, 如果栈为空还没有找到, 则不是

  • 层次遍历二叉树

使用 队列
类似: 图的 bfs

  • 后序遍历 判断是不是 二叉搜索树

二叉搜索树: 左子树小于根节点, 右子树大于根节点
后序遍历: 左子树 -> 右子树 -> 根节点
递归

  • 二叉树中和某值的路径

在遍历树的同时, 使用 vector 实现栈来保存路径

void FindPath(BinaryTreeNode* pRoot, int expectedSum, vector<int>& path, int& currentSum);
void FindPath(BinaryTreeNode* pRoot, int expectedSum){
    if(pRoot==NULL) return;
    vector<int> path;
    int currentSum = 0;
    FindPath(printArr, expectedSum, path, currentSum);
}

void FindPath(BinaryTreeNode* pRoot, int expectedSum, vector<int>& path, int& currentSum){
    currentSum += pRoot->m_nValue;
    path.push_back(pRoot->m_nValue);

    // 满足条件: 叶节点 + currentSum
    bool isLeaf = pRoot->m_pLeft==NUll && pRoot->m_pRight==NULL;
    if(currentSum==expectedSum && isLeaf){
        vector<int>::iterator iter = path.begin();
        for(;iter!=path.end(); iter++){
            cout<<*iter;
        }
        cout<<endl;
    }

    // 如果不是 叶节点
    if(pRoot->m_pLeft!=NULL) FindPath(pRoot->m_pLeft, expectedSum, path, currentSum);
    if(pRoot->m_pRight!=NULL) FindPath(pRoot->m_pRight, expectedSum, path, currentSum);

    // 返回 父节点
    currentSum -= printArr->m_nValue;
    path.pop_back();
}
  • 复制复杂链表

在原节点后复制一个当前节点 -> 复制节点的复杂关系 -> 根据奇偶分离 2 个链表

  • 二叉搜索树 -> 排序双向链表(不能增加节点)

二叉搜索树: 中序遍历就是有序序列(太多指针操作)

  • 字符串的所有排列

这里字符都是唯一的
递归: 将待排序字符串分为第一个字符和后面的字符, 将第一个字符依次和后面的字符交换

void permutation(char* pStr, char* pBegin){
    if(*pBegin == '\0') cout<<pStr<<endl;
    else{
        for(char* pCh=pBegin; *pCh!='\0'; pCh++){
            char tmp = *pCh; 
            *pCh = *pBegin;
            *pBegin = tmp;

            permutation(pStr, pBegin+1);

            tmp = *pCh; 
            *pCh = *pBegin;
            *pBegin = tmp;
        }
    }
}

void permutation(char* pStr){
    if(pStr == NULL) return;
    permutation(pStr,pStr);
}

类似:

n个字符的所有组合
从n个字符中取m个的组合 = 从n-1个字符中取m-1个字符(m包含第一个字符) + 从n-1个字符中取m个字符

8个数字放在正方形8个顶点上, 使3对面上的顶点相等
求全排列, 判断是否存在符合条件的情况

8皇后问题

  • 数组中超过一半的数字

保存当前数, 遍历数组, 再次出现, 计数+1, 不等, 计数-1

int moreThanHalfNum(int* a, int n){
    int res, cnt=0;
    for(int i=0;i<n;i++){
        if(!cnt) res = a[i];
        if(a[i]==res) cnt++;
        else cnt--;
    }
    return res;
}
  • 最小的k个数

基于快排的 partition 算法, 将比a[k]小的数字移动到数组左边(需要改变原数据)
维护一个最大堆, 依次比较插入的数据(特别适合大数据场景)

  • 连续子数组最大和

动态规划

  • 1-n整数中1出现的次数

去掉最高位递归(和求全排列的思路一样的)

  • 把正整数数组拼接成一个最小的数

规律: 2个数n,m, 只要确定 mn 和 nm 的大小, 就可以确定 m,n 的先后顺序
隐藏的大数问题: 将 m,n 转换成字符串, 然后进行比较

  • 丑数

原始方法: 一次判断是不是丑数, 知道找到第1500个
空间换时间: 维护一个排序好的丑数序列, 以及3个位置(这个位置乘以2刚好比最大的丑数大)

  • 第一次只出现一次的字符

通过 (int)char 维护一个hash表, 遍历一次计数, 然后再次遍历
类似:
第一个字符串中删除第二个字符串(为第二个字符串维护hash表)
删除字符串中的重复字符(为一个bool类型的hash表)
判断2个字符串是否含有相同的字符: 扫描str1增加每个字符的cnt, 扫描第二次减每个字符的cnt

  • 数组中的逆序对

原数组的逆序对 = 左数组的逆序对 + 右数组的逆序对 + 求左右数组的逆序对

  • 链表的公共节点

如果有公共节点, 那么肯定之后的节点一定都相同, 遍历获取2个链表的长度, 然后用2个指针遍历, 一个先走 m-n 步

  • 排序数组中连续数字的第一次&最后一次出现位置

二分查找的变种

// 数第一次出现的位置
if(a[mid] == num){
    if(mid==0 || (mid>0 && a[mid-1]!=num)) return mid;
    else end = mid-1;
}else if(a[mid] > num) end = mid-1;
else start = mid+1;
  • 二叉树的深度
int treeDepth(BinaryTreeNode* pRoot){
    if(pRoot==NULL) return 0;
    int nLeft = treeDepth(pRoot->m_pLeft);
    int nRight = treeDepth(pRoot->m_pRight);

    return (nLeft > nRight) ? (nLeft+1) : (nRight+1);
}

类似:
判断是否为平衡二叉树: 可以套用上面的方法, 但是会重复遍历, 采用后序遍历, 每次记录树的深度即可只遍历一次

bool isBalanced(BinaryTreeNode* pRoot, int* pDepth){
    if(pRoot==NULL){
        *pDepth = 0; return true;
    }

    int left, right;
    if(isBalanced(pRoot->m_pLeft, &left) && isBalanced(pRoot->m_pRight, &right)){
        if((left>right) && (left-right)<=1){
            *pDepth = 1 + left; return true;
        }
        if((left<right) && (right-left)<=1){
            *pDepth = 1 + right; return true;
        }
    }
    return true;
}
  • 数组中只出现一次的数字

一个数字只出现一次, 其他数字都出现2次: 异或
二个数字只出现一次, 其他数字都出现2次: 通过第一次异或的结果分组, 将问题转化为上面的类型
一个数字只出现一次, 其他数字都出现3次: 使用hash表存每位上面 1 的个数, 不能整除就说明这个1属于这个数

  • 和为 s 的2个数字 vs 和为s的序列

2个指针, 一个数组投, 一个数组尾
2个指针, 一个在第一个元素, 一个在第二个元素

  • 反转单词 vs 左旋转字符串

先反转整个字符串, 然后依次反转单词
先反转整个字符串, 然后反转 左边/右边 的字符串

  • n个骰子的点数

当前骰子点数和上一次骰子的点数的关系: f(n) = f(n-1) + ... + f(n-6)

int n, num,i,j,k;
cin>>n;
int arrSize = 6*n+1;
int total = pow(6,n);
int a1[arrSize]={0}, a2[arrSize]={0};
for(i=1;i<=6;i++) a1[i] = 1;
for(i=2;i<=n;i++){
    // 如果 i 是奇数, 通过 a2 计算 a1
    if(i&1){
        for(j=1;j<=6*i;j++){
            a1[j] = 0;
            for(k=1;k<=6 && (j-k)>=0;k++){
                a1[j] += a2[j-k];
            }
        }
    }else{
        for(j=1;j<=6*i;j++){
            a2[j] = 0;
            for(k=1;k<=6 && (j-k)>=0;k++){
                a2[j] += a1[j-k];
            }
        }
    }
}
  • 扑克牌的顺子

牌抽象成数字, 大小王抽象成 0(指代任意数字) -> 数值数组排序 -> 计算不连续 -> 和 0 来比较

  • 圆圈中最后剩余的数字

可以推导出一个数学公式

  • 位运算实现加法
int add(int num1, int num2){
    int sum, carry;
    do{
        sum = num1 ^ num2;
        carry = (num1 & num2) <<1;
        num1 = sum;
        num2 = carry;
    }while(num2!=0);

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

推荐阅读更多精彩内容

  • 本文是我自己在秋招复习时的读书笔记,整理的知识点,也是为了防止忘记,尊重劳动成果,转载注明出处哦!如果你也喜欢,那...
    波波波先森阅读 3,968评论 0 23
  • 前言 2. 实现 Singleton 3. 数组中重复的数字 4. 二维数组中的查找 5. 替换空格 6. 从尾到...
    Observer_____阅读 2,831评论 0 1
  • 算法思想贪心思想双指针排序快速选择堆排序桶排序荷兰国旗问题二分查找搜索BFSDFSBacktracking分治动态...
    第六象限阅读 2,551评论 0 0
  • 昨日半夜 噢,不!应该是今日凌晨 牙痛到被疼醒,辗转难眠 强逼着自己入眠,因为白天还要上班 坐在下班回家的公交车上...
    木槿花_阅读 116评论 0 0
  • 回达州杂诗 ☆田秀 又别温岭去达州一衣携梦乘火车 着别温岭海涛声清风 送别我离去的新娘低吟浅唱 ...
    兴安居士阅读 351评论 0 2