剑指offer刷题笔记

 

目录

1.二维数组中的查找

2.替换空格

3.从头到尾打印链表

4.重建二叉树

5.用两个栈实现队列

6.斐波那契数列

7.旋转数组的最小数字

8.矩阵中的路径

9.机器人的运动范围

10.剪绳子


1.二维数组中的查找

在一个二维数组中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。

主要思想:由右上角或者左下角为起点,与目标值进行比较,以右上角为例,由于其是一行中最大,一列中最小,所以其每次都能通过与目标值进行比较,去掉一行或者一列,这样可以步步紧逼,直到找到目标值。但是,左上角和右下角不可行,对于左上角的元素来说,其如果与目标值不等,根本无法剔除一行或者一列,因为其右侧与下侧都有可能与目标值相等。

另外,如下述代码,对于vector里存放vector的形式,其可以直接通过类似于二维数组的方式访问每一个元素。

	vector> a;
	a.push_back({ 1,2,3,8 });
	a.push_back({ 4,5,6 });
	cout << a[0][3] << endl;

题解程序:

class Solution {
public:
    bool Find(int target, vector > array) {
        if(array.size()==0)
            return false;
        int row=array.size();
        int col=array[0].size();
        int i=0,j=col-1;
        while(i=0)
        {
            int temp=array[i][j];
            if(temp==target)
                return true;
            else if(target

2.替换空格

请实现一个函数,将一个字符串中的每个空格替换成“%20”。例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy。

主要思想:双指针。从字符串的后侧开始赋值和替换更快,前侧替换的话会导致相同元素平移多次,时间复杂度增大。

另外,对于char型数组,获取其长度用strlen(),该函数返回的长度不包括末尾的'\0'。另外注意下面的代码,length必须为常量。

const int length=100;
char str[length]="We are happy.";//length必须为常量

题解程序:

class Solution {
public:
	void replaceSpace(char *str,int length) {
        if(str==NULL||length<=0)
            return;
        int len=strlen(str);
        int spacecount=0;//空格的数量
        for(int i=0;i

3.从头到尾打印链表

输入一个链表,按链表从尾到头的顺序返回一个ArrayList。

这题太简单了,主要时注意while(head)判断指针是否为空,然后循环里不断执行head=head->next。另外,注意reverse函数,其是一个泛型算法,将容器内容翻转。

题解程序:

/**
*  struct ListNode {
*        int val;
*        struct ListNode *next;
*        ListNode(int x) :
*              val(x), next(NULL) {
*        }
*  };
*/
class Solution {
public:
    vector printListFromTailToHead(ListNode* head) {
        vector a;
        if(head==NULL)
            return a;
        while(head)
        {
            a.push_back(head->val);
            head=head->next;
        }
        reverse(a.begin(),a.end());
        return a;     
    }
};

4.重建二叉树

输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。

主要思想:递归。前序遍历的第一个数字总是数的根节点,而在中序遍历中,根节点位于序列的中间,位于序列左侧的都在左子树上,右侧的都在右子树上。利用这一特性,把一棵大树分为左右两棵子树,然后左右子树又可以进一步划分为左右子树,这是典型的递归思想,对于递归问题,特别重要的一点是要设置合适的终止条件,否则递归会一直递归下去,直至内存溢出。

另外,注意C++copy算法中的目的容器其容器大小必须能够足够容下要拷贝的数据。因为copy是泛型算法,泛型算法的特点之一就是其永远不会改变底层容器的大小,算法可能改变容器中保存的元素,也可能在容器内移动元素,但永远不会直接添加或删除元素。所以,copy算法是不会改变容器的大小的,对于下面的例子,b是一个空的容器,调用copy算法显然会发生内存溢出。我们可以像容器c那样进行操作,即事先改变容器c的大小。也可以调用iterator标准库中的插入迭代器,在边拷贝的时候边在容器内插入要拷贝的元素。例如下面的容器d,这里使用了插入迭代器inserter,假设要拷贝的元素为x,其相当于调用了d.insert(x)。当然了,也可以使用插入迭代器back_inserter,,假设要拷贝的元素为x,其相当于调用了d.push_back(x)。具体参见C++ Primer第5版P358.

例如:

vector a={1,3,4,2};
vector b;
vector c;
vector d;
copy(a.begin(),a.end(),b.begin());//错误,容器b是空的,copy不会去改变容器的大小
c.resize(4);
copy(a.begin(),a.end(),c.begin());//正确,容器c的大小足够容纳拷贝的数据
copy(a.begin(),a.end(),inserter(d,d.begin()));//正确,调用插入迭代器标拷贝边插入值

题解程序:

/**
 * Definition for binary tree
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    TreeNode* reConstructBinaryTree(vector pre,vector vin) {
        if(pre.size()==0||vin.size()==0)
            return NULL;
        TreeNode* root=new TreeNode(pre[0]);
        vector preL,vinL;
        vector preR,vinR;
        for(int i=0;ileft=reConstructBinaryTree(preL,vinL);
               copy(pre.begin()+i+1,pre.end(),inserter(preR,preR.begin()));//拷贝先序遍历序列中对应右子树的部分
               copy(vin.begin()+i+1,vin.end(),inserter(vinR,vinR.begin()));//拷贝中序遍历序列中对应右子树的部分
               root->right=reConstructBinaryTree(preR,vinR);		   			   			   
               break;
            }
        }
        return root;
    }
};

5.用两个栈实现队列

用两个栈来实现一个队列,完成队列的Push和Pop操作。 队列中的元素为int类型。

主要思想:一个栈stack1用来存放push进来的数,一个栈stack2专门用来pop元素。当需要pop元素的时候,将stack1的元素通通存入stack2,这样逐次取出stack2中的元素,即满足先进先出。当stack2不为空的时候,stack2中的栈顶元素是最先进入队列的元素。当stack2为空时,我们把stack1中的元素逐个弹出并压入stack2.由于先进入队列的元素被压到stack1的底端,经过弹出和压入操作后就处于stack2的顶端,又可以直接弹出。

需要注意的是,stack的pop操作只删除栈顶元素,不会返回栈顶元素。而top操作只返回栈顶元素,不会删除栈顶元素

题解程序:

class Solution
{
public:
    void push(int node) {
        stack1.push(node);
        
    }

    int pop() {
        int top;
        if(stack2.size()>0)
        {
            top=stack2.top();//取出栈顶元素 不删除
            stack2.pop();//删除栈顶元素 不返回
            return top;
        }
        else
        {
            while(stack1.size()>0)
            {
              stack2.push(stack1.top());
              stack1.pop();
            }
            top=stack2.top();
            stack2.pop();
            return top;
        }
        
    }

private:
    stack stack1;
    stack stack2;
};

6.斐波那契数列

大家都知道斐波那契数列,现在要求输入一个整数n,请你输出斐波那契数列的第n项(从0开始,第0项为0)。

这一题太常见了,最常见的就是递归,但是问题在于很多能用递归的算法也可以用循环去实现,递归的方法通常效率较低,所以这种简单的题目建议采用循环的方法,因为当n足够大的时候,递归的鲁棒性会很差。

另外非常重要的一点在于,很多问题可以转换为斐波那契数列求解问题,例如青蛙跳台阶问题,把总共的跳法即为n的函数f(n),对于第一次跳一共两种情况,一是第一次跳1级,剩下的则为f(n-1),另一个是第一次跳2级,剩下的为f(n-2),显然f(n)=f(n-1)+f(n-2),学习这种分析的思想。

题解程序:

class Solution {
public:
    int Fibonacci(int n) {
        if(n<=0)
            return 0;
        if(n==1)
            return 1;
        int FminusOne=1;
        int FminusTwo=0;
        int ans=0;
        for (int i=2;i<=n;++i)
        {
            ans=FminusOne+FminusTwo;
            FminusTwo=FminusOne;
            FminusOne=ans;
      
        }
        return ans;                  
    }
};

7.旋转数组的最小数字

把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。
输入一个非递减排序的数组的一个旋转,输出旋转数组的最小元素。
例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1。
NOTE:给出的所有元素都大于0,若数组大小为0,请返回0。

主要思想:二分法。这一题类似于leetcode数组篇搜索旋转排序数组这道题,我们仍然可以采用"对于这种排序好的数组从任意一点进行旋转得到的数组,无论你从在数组中间的某个点往两侧看,都是一侧是有序的,一侧是无序的"这种思想,这也是我做这道题的方法。即先判断左侧是完全有序的还是右侧是完全有序的,在有序的一侧可以得到有序侧的最小值,然后跳向无序侧,再将无序侧分为一个子有序侧和子无序侧,再得到子有序侧的最小值,一直循环执行,用一个中间变量记录最小值,最终可以得到全局的最小值。另外一种方法是别人的解法,不建议采用书上的解法,太繁琐。别人的解法利用的是"排序好的旋转数组(非递减)可以看成是两个子有序数组,并且前面的子有序数组的元素都大于等于后面子有序数组的元素"这种思想。首先,由于范围是从两侧往内收的,所以缩小范围后的子数组其实还是满足上述条件的。因此,第一步,如果左侧有序数组第一个元素比右侧有序数组最后一个元素小,必然是全局最小值。在其他情况下,最小值必然在右侧子序列中诞生。如果中间值大于左侧子序列第一个元素,说明中间值位于右侧子序列,左侧指针右移,如果中间值小于右侧子序列最后一个元素,说明中间值位于左侧子序列,右侧子序列右移,但因为最小值必然在右侧子序列中诞生,所以end=mid,而不能为end=mid-1,正因为该原因,while循环中start其他情况,start++缩小范围即可。

我的程序:

class Solution {
public:
    int minNumberInRotateArray(vector rotateArray) {
        if(rotateArray.size()==0)
            return 0;
        int ans=rotateArray[0];
        int start=0;
        int end=rotateArray.size()-1;
        while(start<=end)
        {
            int mid=(start+end)/2;
            if(rotateArray[mid]>=rotateArray[start])//左侧有序
            {
              if(ans>rotateArray[start])
                ans=rotateArray[start];
              start=mid+1;           
            }
            else//右侧有序
            {
              if (ans>rotateArray[mid])
                ans=rotateArray[mid];
               end=mid-1;               
            }
        }
        return ans;    
    }
};

别人的程序:

class Solution {
public:
    int minNumberInRotateArray(vector rotateArray) {
        if(rotateArray.size()==0)
            return 0;
        int start=0;
        int end=rotateArray.size()-1;
        while(startrotateArray[start])
                start=mid+1;
            else if(rotateArray[mid]

8.矩阵中的路径

请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一个格子开始,每一步可以在矩阵中向左,向右,向上,向下移动一个格子。如果一条路径经过了矩阵中的某一个格子,则该路径不能再进入该格子。 例如 a b c e s f c s a d e e 矩阵中包含一条字符串"bcced"的路径,但是矩阵中不包含"abcb"路径,因为字符串的第一个字符b占据了矩阵中的第一行第二个格子之后,路径不能再次进入该格子。

主要思想:回溯法(有剪枝)。用到回溯法的题目有些是需要剪枝处理,有些是不需要剪枝处理的,这道题就是需要剪枝处理的一道题,那么什么情况下需要剪枝处理,什么情况下不需要剪枝处理呢?我目前的理解是除了已访问节点终止递归的情况,还有其他情况可以终止递归,那么就去剪枝,具体来说,就是后续节点对前面的这个节点是否有依赖关系,例如本题如果当前节点的这个字符不是答案的一份子,那么它相邻的节点显然不需要再搜索了,所以相邻的节点对这个节点有依赖关系;而如果仅仅只是跳过已访问节点,其他情况下依然需要递归的情况,则不要剪枝,例如图的深度遍历以及下一题机器人的运动范围这一题,以图的深度遍历来说,没有明显的依赖关系(仅仅是我目前的理解,不一定准确)剪枝,就是在搜索过程中利用过滤条件剪去一些完全不用考虑(已经判定经过当前节点的路径得不到解)的路径,从而避免了不必要的搜索

回溯法(有剪枝)的题目的解题步骤总结如下:

1.找到满足题目要求的一个答案,返回。
2.满足递归终止的条件,结束递归   相当于剪枝操作。
3.否则,说明走经过当前节点的这条路是有可能找到答案的,递归。
4.回溯。走到了第4步,说明之前第3步的递归也最终没有找到答案,说明走这经过当前节点的这条路是不通的,因此要回溯到上一个可能行得通的节点。

具体到本题来说:当访问到字符串末尾时,说明之前的匹配都成功了,找到了题目要求的答案,返回即可。如果满足递归终止的条件,即越界、、当前矩阵值不等于字符数组中对应位置的值、已经访问过的,说明此路不通,剪枝处理,递归终止。否则的话,说明当前经过当前节点的这条路是有可能找到答案的,当然就递归下去寻找了。务必需要注意的是第四步,回溯操作。当第三步递归搜索没有找到答案时,其才会走到这一步,因为如果第三步递归找到一个满足的节点,其必然又去调用下一个DFS了。只有在DFS中下一个节点不符合要求,程序才会退回到本次DFS中,因此我们需要回溯到上一个可行节点,具体到本题来说,就是要当前节点的标志位还原为false。

另外,补充一下char *和char [],还是以具体的例子来说明吧。

char *a="abcd";
char b[]="efg";
cout<

char *是指向字符串的指针,char []是字符数组,对于上面代码的前两行,编译器在初始化的时候,都会在末尾加上'\0',char []可以通过赋值号直接转化为char *,然后注意上面访问整个字符串与单个字符串的语法区别。

题解程序:

class Solution {
public:
	bool hasPath(char* matrix, int rows, int cols, char* str)
	{
		if (matrix == NULL || rows < 1 || cols < 1|| str==NULL)
			return false;
		bool *visited = new bool[rows*cols]();//标志位 只有确实在路径上的节点visited才为true 默认初始化为false
		for (int i = 0; i= rows || col < 0 || col >= cols || matrix[row*cols + col] != *str || visited[row*cols + col])
			return false;
		visited[row*cols + col] = true;//当前节点是可能在路径当中的
		//回溯,递归寻找
		if (DFS(matrix, rows, cols, str + 1, visited, row + 1, col) || DFS(matrix, rows, cols, str + 1, visited, row - 1, col)
			|| DFS(matrix, rows, cols, str + 1, visited, row, col + 1) || DFS(matrix, rows, cols, str + 1, visited, row, col - 1))
			return true;
		//走到这边说明这条路走不通了 还原为false
		visited[row*cols + col] = false;
		return false;
	}
};

9.机器人的运动范围

地上有一个m行和n列的方格。一个机器人从坐标0,0的格子开始移动,每一次只能向左,右,上,下四个方向移动一格,但是不能进入行坐标和列坐标的数位之和大于k的格子。 例如,当k为18时,机器人能够进入方格(35,37),因为3+5+3+7 = 18。但是,它不能进入方格(35,38),因为3+5+3+8 = 19。请问该机器人能够达到多少个格子?

主要思想:回溯法(无剪枝)。这道题就只需要单纯的回溯法而不需要剪枝。如上一题所述,仔细想一下,假设我访问了当前节点,其不满足条件,也就是说这个节点必然不可能作为答案的一份子,那么我们是否可以说明它相邻的节点也不符合条件了,显然不是,因为后续节点对这个节点没有依赖关系,当前的这个节点不满足条件,不代表相邻的节点也不满足条件,所以不需要剪枝。

题解程序:

class Solution {
public:
	int movingCount(int threshold, int rows, int cols)
	{
		if(threshold<0||rows<=0||cols<=0)
                  return 0;
		bool *visited = new bool[rows*cols]();
		int count = DFS(threshold, rows, cols, 0, 0, visited);//从起点开始搜索
                delete[] visited;
		return count;
	}
        //返回值表示从行为row,列为col的这个格子出发满足条件的格子的总数
	int DFS(int threshold, int rows, int cols, int row, int col, bool *visited)
	{
		int count = 0;
		if (row >= 0 && row= 0 && col0)
		{
			sum += row % 10;
			row /= 10;
		}
		while (col>0)
		{
			sum += col % 10;
			col /= 10;
		}
		return sum;
	}
};

10.剪绳子

给你一根长度为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。

主要思想:动态规划或者贪婪算法

动态规划算法与贪婪算法的异同:

共同点:两者都具有最优子结构性质

不同点:动态规划算法中,每步所做的选择往往依赖于相关子问题的解,因而只有在解出相关子问题时才能做出选择。而贪心算法,仅在当前状态下做出最好选择,即局部最优选择,然后再去解做出这个选择后产生的相应的子问题。动态规划算法通常以自底向上的方式解各子问题,而贪心算法则通常自顶向下的方式进行。一般来说,贪婪算法能解决的问题动态规划都能解决,但是动态规划能解决的问题贪婪算法不一定能解决,但贪婪算法和动态规划都能解决的问题贪婪算法效率要比动态规划要高。

动态规划求解问题的4个一般特征:
①求一个问题的最优解; 
②整体的问题的最优解是依赖于各个子问题的最优解; 
③小问题之间还有相互重叠的更小的子问题; 
④从上往下分析问题,从下往上求解问题

如果一道题满足上述4个特征,我们可以考虑使用动态规划算法。显然,本题满足上述四个特征。分析问题的时候,我们从上往下将大问题分解为一个个小问题,但是由于子问题在分解大问题的过程中重复出现,为了避免重复求解子问题,我们要从下往上先将小问题的最优解保存下来,再以此为基础去求解大问题的最优解。注意下面题解程序中j<=i/2的地方,因为接下来没必要走下去了,另外p[i]保存长度为i的绳子剪成若干段之后长度乘积的最大值。

动态规划法题解程序:

class Solution {
public:
    int cutRope(int number) {
        if(number<2)
            return 0;
        if(number==2)
            return 1;
        if(number==3)
            return 2;
        int* p=new int[number+1];
        //这边之所以P[2]=2而不等于1是因为这是为绳子可以大于3以后使用的 长度大于3的话绳子可以剪成一段为2的
        //下边这行的p[i]表示如果允许不剪的乘积的最大值 其主要是为后续长度大于3的绳子考虑的
        p[0]=0;p[1]=1;p[2]=2;p[3]=3;
        int max;
        for(int i=4;i<=number;++i)
        {
            max=0;
            for(int j=1;j<=i/2;++j)//j<=i/2是因为没有必要走下去了 例如p[4]*p[6] p[6]*p[4]
            {
                int temp=p[j]*p[i-j];
                if(max

贪婪算法作出的每步贪心决策都无法改变,因为贪心策略是由上一步的最优解推导下一步的最优解,而上一部之前的最优解则不作保留。所以本题是需要一定的数学功底的。具体拉私活,就是假设绳子长度为n,n>=5时,可以证明3(n-3)>n且2(n-2)>n,也就是说,当绳子剩下的长度大于等于5的时候,应该把它剪成3或者2的长度的绳子段,另外,当n>=5时,还可以证明3(n-3)>=2(n-2),所以更应该尽可能的剪长度为3的绳子段。而如果n==4,显然4=2*2>1*3,所以n==4的话可以剪成两个2或者干脆不剪,但是程序要求至少剪一刀,所以可以剪成两个2。所以贪婪算法一般来说,虽然程序效率高,但是要有一定的数学功底,并不是很好想。

贪婪算法题解程序:

class Solution {
public:
    int cutRope(int number) {
        if(number<2)
            return 0;
        if(number==2)
            return 1;
        if(number==3)
            return 2;
        int CountThree=number/3;//绳子中能剪3的数量
        if(number-3*CountThree==1)//说明绳子中最后会有4,所以3的数量要减1
            CountThree-=1;
        int CountTwo=(number-3*CountThree)/2;//剪完3的剩下的就是剪2的数量
        return (int)(pow(3,CountThree)*pow(2,CountTwo));
    }
};

11.二进制中1的个数

输入一个整数,输出该数二进制表示中1的个数。其中负数用补码表示。

主要思想:位运算。关键思路在于把一个整数减去1之后再和原来的整数做位与运算,得到的结果相当于把整数的二进制表示中最右边的1变成0,其余位都没有改变。很多二进制的问题都可以用这种思路解决。

题解程序:

class Solution {
public:
     int  NumberOf1(int n) {
         int count=0;
         while(n)
         {
             ++count;
             n=n&n-1;
         }
         return count;
         
     }
};

 

你可能感兴趣的:(剑指offer刷题笔记)