美文网首页
读书笔记| 剑指offer

读书笔记| 剑指offer

作者: daydaygo | 来源:发表于2018-11-08 20:52 被阅读172次

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

相关文章

  • 全网最全剑指offer题目解答

    【剑指offer】Java版代码(完整版) 【剑指offer】1-10题 【剑指offer】11-20题 【剑指o...

  • 剑指offer 和 leetcode题目

    剑指offer leetcode

  • 单例模式

    单例模式 最近在看《剑指offer》,根据《剑指offer》的讲解,结合《effectiveJava》简单学习了一...

  • 年龄排序

    摘抄资料:《剑指offer》

  • 剑指offer读书笔记

    数据结构 数组 数组可以说是最简单的一种数据结构,它占据一块连续的内存并按照顺序存储数据,由于数组中的内存是连续的...

  • 剑指Offer——读书笔记

    @[C++|面试|数据结构|算法] 高质量的代码 代码的规范性 书写清晰 布局清晰 命名合理 代码的完整性 完成基...

  • 剑指 offer 读书笔记

    2.3 数据结构 STL 的 vector 在开始为数组开辟较小的空间,然后往数组中添加数据。当数据的数目超过了数...

  • 读书笔记| 剑指offer

    date: 2016-06-02 14:00 剑指 offer 何海涛blog: http://zhedahht....

  • 剑指offer

    第一题:在一个二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,...

  • 《剑指offer》

    4.调整数组顺序使奇数位于偶数前面 题目描述:输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有的奇...

网友评论

      本文标题:读书笔记| 剑指offer

      本文链接:https://www.haomeiwen.com/subject/qgdgxqtx.html