剑指Offer—算法和数据结构

内容会持续更新,有错误的地方欢迎指正,谢谢!

递归和迭代

递归的代码简洁但性能不如迭代,面试时,可根据题目的特点和面试官的要求选择如何实现。深入理解递归(一) 深入理解递归(二)

排序和查找

重点掌握 二分查找、归并排序、快速排序

旋转数组的最小数字

把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个递增排序的数组的一个旋转,输出旋转数组的最小元素。例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1。

法一:顺序查找,即从头到尾遍历数组,时间复杂度为O(n),但没利用旋转数组的特性,所以达不到要求。
法二:二分查找的应用(难度较大)。先判断原数组是不是一个递增的数组,如果不是,则设三个变量,P1指向第一个元素,P2指向最后一个元素,Mid是中间数。如下图,Mid会指向5,由于5大于3,则令P1指向Mid所指的元素5,P2不变,Mid指向1,此时,1小于5,1又小于2,则令P2指向Mid所指的元素1。此时,P1和P2指向相邻的两个元素,P2指向的数字就是最小值。
这里写图片描述
但还未考虑完全,如下图,此特殊情况:P1、P2、Mid指向的三个数字都相等,则只能用法一来解决之。
这里写图片描述
最终整合之后的代码:

    int minNumberInRotateArray(vector<int> rotateArray) 
    {
        if(rotateArray.empty())
            return 0;
        int frontPoint=0,backPoint=rotateArray.size()-1,midPoint=0;
        while(rotateArray[frontPoint]>=rotateArray[backPoint])
        {
            if(backPoint-frontPoint==1)
            {
                midPoint=backPoint;
                break;
            }
            midPoint=(backPoint+frontPoint)/2;

            //特殊情况时(即所指的三个数字都相等时),用法一,即顺序查找
            if(rotateArray[midPoint]==rotateArray[frontPoint]&&
            rotateArray[frontPoint]==rotateArray[backPoint])
                return MidInOrder(rotateArray,frontPoint,backPoint);

            //正常情况时,用法二
            if(rotateArray[midPoint]>=rotateArray[frontPoint])
            {
                frontPoint=midPoint;
            }else if(rotateArray[midPoint]<=rotateArray[backPoint])
            {
                backPoint=midPoint;
            }
        }
        return rotateArray[midPoint];
    }
海量数据排序

实现一个排序算法,要求时间复杂度为O(n),你有什么方法吗?

分析:多和面试官交流,比如问清楚对什么数据排序,面试官会告诉你:打算对公司所有员工的年龄排序,公司总共几万名员工。你再问:也就是说这些数据是在一个较小的范围内的,可以使用辅助空间吗?面试官回你:可以使用常量大小的辅助空间。那么问题已经很清楚了。

解答:由于年龄是在一定范围内的,即0~99,故申请一个长度为100的内存空间,每个元素用于记录其对应年龄出现的次数,也就是22岁出现一次,a[22]就加1。

回溯法

若在二维数组(通常表现为迷宫或棋盘)上搜索路径,我们可以用回溯法,回溯法很适合用递归的代码来实现。
通过递归这种代码结构来实现回溯这个目的。回溯法可以被认为是一个有过剪枝的DFS过程。DFS和BFS算法

动态规划和贪婪算法

动态规划:若求最优解,并且该问题可分解为多个有互相联系的子问题,我们就可以用DP。先推出递推式,便可用自上而下的递归思路去实现;但为了避免不必要的重复计算,我们用自上而下的迭代思路去实现,也就是把子问题的最优解先求出来并用数组(一维或二维)保存下来,接下来利用这些子问题的解来求解大问题的解。动态规划

贪婪算法:步步为赢

动态规划之剪绳子

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

分析:求最优解,且子问题之间互相有依赖,即考察DP。
为避免子问题的重复计算,我们从下往上依次存储子问题的最优解。即:从上往下分析问题,从下往上求解问题。

对于第一刀,有n-1种选择,故有递推式:f(n)=max{f(n-i)*i},其中i=[1,n-1]

注意:

  1. 绳子剪成 i x (n-i) 和 (n-i) x i 是相同的,故可省掉一半的该运算;
  2. 不符合切割条件的输入n(即n<2),以及输入n为2、3时的结果,因为题中规定m>1。

代码:

    int maxProduct(int length)
    {
        if (length < 2) return 0;
        if (length == 2) return 1;
        if (length == 3) return 2;

        //子问题的最优解存储在products数组里,索引为1、2、3时对应的值为1、2、3
        int* products = new int[length + 1];//多申请一位,方便使用
        products[0] = 0;
        products[1] = 1;
        products[2] = 2;
        products[3] = 3;
        int max = 0;
        for (int i = 4; i <= length; ++i) 
        {
            max = 0;
            for (int j = 1; j <= i / 2; ++j) 
            {
                int product = products[j] * products[i - j];
                if (max < product)
                    max = product;
                products[i] = max;//从4往length依次存储子问题的最优解
            }
        }
        max = products[length];
        delete[] products;
        return max;
    }
贪婪算法之剪绳子

题目同上,只是解法不同。
贪婪决策:当n>=5时,应尽可能多地剪长度为3的绳子;当剩下长度为4的绳子时,则把其剪成两段长度为2的绳子。当剩下长度为小于3的绳子,直接将其乘上去即可。

    int maxProduct(int length)
    {
        if (length < 2) return 0;
        if (length == 2) return 1;
        if (length == 3) return 2;

        int timesOf3 = length / 3;//长度为3的绳子的段数
        if (length - timesOf3 * 3 == 1)//结果为1就表明,肯定会剩长度为4的绳子
            timesOf3--;
        int timesOf2 = (length - timesOf3 * 3) / 2;//长度为2的绳子的段数,timesOf2为0或2
        int result = pow(3, timesOf3) * pow(2, timesOf2);//最终结果
        return result;
    }

通常,面试官会让面试者证明该策略的正确性:
当n>=5时,3(n-3)>=2(n-2)
当n==4时,2*2>1*3
即证!

位运算

把数字表示为0和1后的操作,即对二进制数据的操作,总共只有:与&、或|、异或^、左移<<、右移>>这五种。其中,异或的话,只有0^1或1^0为1;左移的话,n=n<<1表示将n的二进制数左移一位,可用于代替乘法。

另外,一个整数减去1后 再和 原来的整数 做与运算,得到的结果相当于把原来的整数的最右边的1变成0。举例:1100。1100减去1得到1011,1100和1011做与运算,得到1000,也就是把原整数最右边的1变成了0。比如:统计1100中有多少个1,就可以用这种方法,每做一次与运算,count就+1,直到原整数变为0000。

很多的二进制问题,都可以用这种思路来解决。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值