剑指Offer刷题笔记

数组中重复的数字

题目描述:
找出数组中重复的数字

在一个长度为 n n n的数字里的所有数字都在 0 − n − 1 0-n-1 0n1的范围内。数组中某些数字是重复的,但不直到有哪几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。

举例:

输入:长度为7的数组{230253}
输出:2或者3

解题思路一:

  • 对输入的数组进行排序。从排序的数组中找出重复的数字是一件很容易的事情,只需要从头到尾扫描排序后的数组就可以了。
  • 排序一个长度为 n n n的数组需要 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)

解题思路二:

  • 利用哈希表
  • 从头到尾扫描数组中的每一个数字,每扫描到一个数字的的时候,都可以利用 O ( 1 ) O(1) O(1)的时间来判断哈希表里是否已经包含了该数字。
  • 如果哈希表里话还没有这个数字,就把它加入到哈希表。
  • 如果哈希表里已经存在该数字,就找到一个重复的数字。
  • 这个算法的时间复杂度是 O ( n ) O(n) O(n),空间复杂度是 O ( 1 ) O(1) O(1)

解题思路三:
我们注意到数组中的数字都在 0 − ( n − 1 ) 0-(n-1) 0(n1)的范围内。如果这个数组中没有重复的数字,那么当数组排序之后数字 i i i将出现在下标为 i i i的位置(一个萝卜占一个坑)。由于数组中有重复的数字,有些位置可能存在多个数字,同时瘀血位置可能没有数字。

具体实现过程:

  • 从头到尾扫描数组中的每个数字
  • 当扫描到下标为 i i i的数字时,首先比较这个数字(用 m m m表示)是不是等于 i i i
  • 如果是,则接着扫描下一个数字
  • 如果不是,则再拿它和下标为 m m m的数字进行比较。
  • 如果它和下标为 m m m的数字相等,就找到了一个重复的数字(该数字在下标为 i i i m m m的位置都出现了)
  • 如果它和下标为 m m m的数字不相等,就把它和下标为 m m m的数字交换,把 m m m放到属于它的位置
  • 接下来再重复这个比较、交换的过程,直到我们发现一个重复的数字

基于解题思路三的代码实现:

int duplicate(vector<int> numbers)
{
	int len = numbers.size();
	if (len == 0)
	{
		return -1;
	}
	for (int i = 0; i < len; ++i)
	{
		if (numbers[i]<0 || numbers[i]>len - 1)
		{
			return -1;
		}
	}
	for (int i = 0; i < len; ++i)
	{
		while (numbers[i] != i)//只要不相等,就不断进行交换,比较
		{
			//找到重复的数字了
			if (numbers[i] == numbers[numbers[i]])
			{
				return numbers[i];
			}
			//进行交换
			int tmp = numbers[i];
			numbers[i] = numbers[tmp];
			numbers[tmp] = tmp;
		}
	}
	return -1;

}

在这里插入图片描述

不修改数组找出重复的数字

题目描述:
不修改数组找出重复的数字

在一个长度为 n + 1 n+1 n+1的数组里的所有数字都在 1 − n 1-n 1n的范围内,所以数组中至少有一个数字是重复的。
请找出数组中任意一个重复的数字,但不能修改输入的数组。

例如:

输入长度为8的数组:{2,3,5,4,3,2,6,7}
输出:2或3

解题思路一:

  • 由于题目要求不能修改输入的数组,我们可以创建一个长度为 n + 1 n+1 n+1的辅助数组,然后逐一把原数组的每个数字复制到辅助数组。
  • 如果原数组中被复制的数字是 m m m,则把它复制到辅助数组中下标为 m m m的位置。这样很容易就能发现哪个数字是重复的。
  • 但是,由于需要创建一个数组,该方案需要 O ( n ) O(n) O(n)的辅助空间

解决思路二

思考:
为什么数组中会有重复的数字?
加入没有重复的数字,那么在 1 − n 1-n 1n的范围里只有 n n n个数字。
由于数组里包含超过 n n n个数字,所以一定包含了重复的数字。
因此,在某范围里数字的个数对解决这个问题很重要。

具体实现过程:

  • 我们把 1 − n 1-n 1n的数字从中间的数字m分为两部分
  • 前面一半为1-m,后面一半为(m+1)-n。
  • 如果1-m的数字的数目超过m,那么这一半的区间里一定包含重复的数字
  • 否则(m+1)-n的区间里一定包含重复的数字
  • 我们可以继续把包含重复数字的区间一分为二,直到找到一个重复的而数字。
  • 这个过程和二分查找算法很类似,只是多了一步统计区间里数字的数目

基于解决思路二的代码实现

int getDuplication(vector<int> &numbers)
{
	int len = numbers.size();
	if (len == 0)
	{
		return -1;
	}
	int start = 1;
	int end = len - 1;
	while (start <= end)
	{
		int middle = ((end - start) >> 1) + start;//找中间数字
        
        //判断前半部分的数量		
		int count = countRange(numbers, len, start, middle);
		if (end == start)
		{
			if (count > 1)//表示重复出现
			{
				return start;
			}
			else
			{
				break;
			}
		}
		if (count > (middle - start + 1))//重复数字在前半部分
		{
			end = middle ;
		}
		else
		{
			start = middle + 1;//重复数字在后半部分
		}
	}
	return -1;
 }

替换空格

题目描述:

请实现一个函数,把字符串中的每个空格替换成"%20"。

举例:

输入:“we are happy”
输出:“we%20are%20happy”

解题思路:

  • 先遍历一遍字符串,这样就能统计出字符串中空格的总数
  • 由此计算出替换之后的字符串的总长度
  • 每替换一个空格,长度增加2,因此替换后的长度等于原来的长度加上2×空格数目
  • 从字符串的后面开始进行复制和替换。
  • 首先准备两个指针: P 1 P_1 P1 P 2 P_2 P2, P 1 P_1 P1指向原始字符串的末尾, P 2 P_2 P2指向替换之后的字符串的末尾
  • 接下来我们向前移动指针 P 1 P_1 P1,逐个把它指向的字符复制到 P 2 P_2 P2指向的位置,直到碰到第一个空格为止。
  • 碰到第一个空格之后,把 P 1 P_1 P1向前移动1格,在 P 2 P_2 P2之前插入字符串"%20"。由于"%20"的长度为3,同时也要把 P 2 P_2 P2向前移动3格。
  • 接着向前复制,直到 P 1 P_1 P1 P 2 P_2 P2指向同一个位置,表明所有的空格都已经替换完毕。
  • 从上面的分析可以看出,所有的字符都只复制(移动)一次,因此这个算法的时间效率是 O ( n ) O(n) O(n)

代码实现:

void ReplaceBlank(char string[],int length)//length为字符数组string的总容量
{
	if (string == nullptr || length <= 0)
	{
		return;
	}

	int originalLen = 0;//字符串的原始长度
	int numberOfBlank = 0;//空格的数量
	int i = 0;
	while (string[i] != '\0')
	{
		++originalLen;//计算字符串的有效长度
		if (string[i] == ' ')
		{
			++numberOfBlank;//计算空格数量
		}
		++i;
	}
	int newLength = originalLen + numberOfBlank * 2;
	if (newLength > length)
	{
		return;
	}

	int  p1 = originalLen;//指向'\0' 的位置
	int  p2 = newLength;//原始字符串中'\0'的新位置、

	while (p1 >= 0 && p2 > p1)
	{
		if (string[p1] == ' ')
		{
			string[p2--] = '0';
			string[p2--] = '2';
			string[p2--] = '%';
		}
		else
		{
			string[p2--] = string[p1];
		}
		p1--;
	}
}

在这里插入图片描述

从尾到头打印链表

题目描述:

输入一个链表的头结点,从尾到头反过来打印每个节点的值

链表节点定义如下:

struct ListNode
{
	int m_nKey;
	ListNode *m_pNext;
};

解题思路一:
利用栈先进后出的特性

解题思路二:
递归

因为递归在本质上就是一个栈结构,于是很自然地就想到了用递归来实现。
要实现反过来输出链表,我们每访问到一个节点的时候,先递归输出它后面的节点,再输出该节点本身,这样链表的输出结果就反过来了。


解题思路一实现代码:

void PrintListReversingly_Iteratively(ListNode *pHead)
{
	stack<ListNode *> nodes;
	ListNode *pNode = pHead;
	while (pNode != nullptr)
	{
		nodes.push(pNode);//每遍历到一个节点,先将其入栈
		pNode = pNode->m_pNext;//遍历下一个节点
	}
	while (!nodes.empty())
	{
		pNode = nodes.top();//节点开始出栈
		printf("%d ", pNode->m_nKey);
		nodes.pop();
	}
}

解题思路二实现代码:

void PrintListReversingly_Iteratively(ListNode *pHead)
{
	if (pHead != nullptr)
	{
		if (pHead->m_pNext != nullptr)
		{
		//遍历它的下一个节点
			PrintListReversingly_Iteratively(pHead->m_pNext);
		}
		//递归结束后,打印该节点
		printf("%d ", pHead->m_nKey);
	}

}

二叉树的下一个节点

题目要求:

给定一棵二叉树和其中一个节点。如何让找出中序遍历序列的下一个节点?
树中的节点除了有两个分别指向左右子节点的指针,还有一个指向父节点的指针

数据结构:

struct TreeNode
{
	char val;
	TreeNode *parent;//指向父节点的指针
	TreeNode *left;//指向左孩子的指针
	TreeNode *right;//指向右孩子的指针
};

解题思路:

依据下图进行分析:
在这里插入图片描述

  • 如果一个节点有右子树,那么它的下一个节点就是它的右子树中最左子节点。
  • 也就是说,从右子节点出发一直沿着指向左子节点的指针,我们就能找到它的下一个节点。例如 : b :b :b的下一个节点就是 h h h
  • 如果一个节点没有右子树且该节点是它父节点的左子节点,那么它的下一个节点就是它的父节点。例如 : d :d :d的下一个节点就是 b b b
  • 如果一个节点既没有右子树,并且它还是它父节点的右子节点,那么这种情形就比较复杂。
  • 我们可以沿着指向父节点的指针一直向上遍历,直到找到一个是它父节点的左子节点的节点。
  • 如果这样的节点存在,那么这个节点的父节点就是我们要找的下一个节点
  • 例如:为了找到节点 i i i的下一个节点,我们沿着指向父节点的指针向上遍历,先到达节点 e e e。由于节点 e e e是它父节点 b b b的右节点,我们继续向上遍历到达节点 b b b。 节点 b b b是它父节点 a a a的左子节点,因此节点 b b b的父亲节点 a a a就是节点 i i i的下一个节点

代码实现:



TreeNode *GetNext(TreeNode *node)
{
	if (node == nullptr)
	{
		return nullptr;
	}
	TreeNode *next = nullptr;
	if (node->right != nullptr)//有右子树,next就是右子树中最左子树节点
	{
		TreeNode *pright = node->right;
		while (pright->left != nullptr)
		{
			pright = pright->left;
		}
		//此时pright就是最左边的节点
		next = pright;
		return next;
	}
	else if (node->parent != nullptr)
	{
		TreeNode *cur = node;//当前节点
		TreeNode *pparent = node->parent;//当前节点的父节点
		
		/*
		* 该节点是它父节点的右子节点
		* 不断向上去寻找一个是其父节点的左子节点的节点
		* 该节点的父节点就是我们要找的next节点
		*/
		while (pparent != nullptr&&cur == pparent->right)
		{
			cur = pparent;
			pparent = pparent->parent;
		}
		next = pparent;
	}
	return next;
}

用两个栈实现队列

题目要求:

用两个栈实现一个队列
队列的声明如下,请实现它的两个函数appendTail和deleteHead,
分别完成在队列尾部插入节点和在队列头部删除节点的功能。

数据结构:

template<typename T> class CQueue
{
public:
	CQueue();
	~CQueue();

	void appendTail(const T &node);
	T deleteHead();

private:
	stack<T> stack1;
	stack<T> satck2;
};

解题思路:

插入元素的步骤:

  • 插入的元素直接压入 s t a c k 1 stack1 stack1

删除元素的步骤:

  • s t a c k 2 stack2 stack2不为空时。在 s t a c k 2 stack2 stack2的栈顶元素是最先进入队列的元素,可以弹出。
  • s t a c k 2 stack2 stack2为空时,我们把 s t a c k 1 stack1 stack1中的元素逐个弹出并压入 s t a c k 2 stack2 stack2
  • 由于先进入队列的元素被压到 s t a c k 1 stack1 stack1的底端,经过弹出和压入操作之后就处于 s t a c k 2 stack2 stack2的顶端,又可以直接弹出。
    在这里插入图片描述

代码实现:

插入元素:

template<typename T> 
void  CQueue<T>::appendTail(const T &element)
{
	stack1.push(element);
}

删除队头元素:

template<typename T>
T CQueue<T>::deleteHead()
{
	if (stack2.size() <= 0)
	{
		if (stack1.size() > 0)//将元素转入stack2
		{
			T & data = stack1.top();
			stack1.pop();
			stack2.push(data);
		}
	}
	if (stack2.size() == 0)
	{
		throw new execption("queue is empty");
	}
	 
	T head = stack2.top();//取出stack2的栈顶元素就是要删除的队头元素
	stack2.pop();
	return head;
}

用两个队列实现一个栈

题目要求:

用两个队列实现一个栈
栈的声明如下,请实现它的两个函数appendTail和deleteHead,
完成栈的后进先出的功能

数据结构:

template<typename T> class CStack
{
public:
	CStack(void);
	~CStack(void);
 
	void appendTail(const T& node);
	T deleteHead();
 
private:
	queue<T> q1;
	queue<T> q2;
};
 

问题分析:

  • 用两个队列模拟实现栈的时候就需要两个队列的元素“互相转移”,从而实现栈的这一特性

  • 栈的性质是后进先出

  • 数据的插入:保持一个队列为空,一个队列不为空,往不为空的队列中插入元素

  • 数据的删除:要拿到队列中最后压入的数据,只能每次将队列中数据pop到只剩一个

  • 此时这个数据为最后压入队列的数据

  • 在每次pop时,将数据压入到另一个队列中,直到该队列中只剩下一个元素,将其进行删除

在这里插入图片描述


代码实现:

插入元素


template<typename T>
void CStack<T>::appendTail(const T& node)//实现栈元素的插入
{
	//数据的插入原则:保持一个队列为空,一个队列不为空,往不为空的队列中插入元素
	if (!q1.empty())
	{
		q1.push(node);
	}
	else
	{
		q2.push(node);
	}
}
 

元素的删除:

template<typename T>
T CStack<T>::deleteHead()//实现栈元素的删除
{
	int ret = 0;
	if (!q1.empty())//q1不为空,将q1的元素转移到q2
	{
		int num = q1.size();
		while (num > 1)
		{
			q2.push(q1.front());
			q1.pop();
			--num;
		}
		ret = q1.front();// 要删除的元素
		q1.pop();//删除该元素
	}
	else //q2不为空,将q2的元素转移到q1
	{
		int num = q2.size();
		while (num > 1)
		{
			q1.push(q2.front());
			q2.pop();
			--num;
		}
		ret = q2.front();
		q2.pop();
	}
	return ret;
}

矩形覆盖

问题描述:

我们可以用 2 × 1 2×1 2×1的小矩形横着或者竖着去覆盖更大的矩形。请问用n个 2 × 1 2×1 2×1的小矩形无重叠地覆盖一个 2 × n 2×n 2×n的大矩形,总共有多少种方法?

在这里插入图片描述


解题思路:

  • 小矩形覆盖的方式无非就两种,横着和立着。
  • 当第一块选择竖着放,那么接下来的 2 × ( n − 1 ) 2×(n-1) 2×(n1)的矩形仍然需要用 2 × 1 2×1 2×1的矩形来覆盖,那么就是 F ( n − 1 ) F(n-1) F(n1)
  • 当第一块选择横着放,那么第二块也必须横着放才能填满下面的空缺。接下来的 2 × ( n − 2 ) 2×(n-2) 2×(n2)的矩形面临同样的问题。

递推公式如下:

n=0 F(0)=0
n=1 F(1)=1 //只有一种放置方式。
n=2 F(2)=2 //横着和竖着两种。
n>=3 F(n)=F(n-1)+F(n-2)

代码实现:

C++递归实现

class Solution {
public:
    int rectCover(int number) {
        if(number<=0)return 0;
        if(number==1)return 1;
        if(number==2)return 2;
        return rectCover(number-1)+rectCover(number-2);
    }
};

C++迭代实现

class Solution {
public:
    int rectCover(int number) {
        if(number<=0)return 0;
        if(number==1)return 1;
        if(number==2)return 2;
        
        int f1=1;
        int f2=2;
        int result=0;
        for(int i=3;i<=number;i++){
            result=f1+f2;
            f1=f2;
            f2=result;
        }
        return result;
    }
};

矩阵中的路径

问题描述:

请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一格开始,每一步可以从矩阵中的任意一格开始,每一步可以在矩阵中向左、右、上、下移动一格。如果一条路径经过了矩阵中的某一格,那么该路径不能再次进入该格子。

例如:
在下面的3×4的矩阵中包含一条字符串 &quot; b f c e &quot; &quot;bfce&quot; "bfce"的路径(路径中的字母用下画线标出)。但矩阵中不包含字符串 &quot; a b f b &quot; &quot;abfb&quot; "abfb"的路径,因为字符串的第一个字符b占据了矩阵中的第一行第二个格子之后,路径不能再次进入这个格子。


解题思路:

回溯法

  • 首先在矩阵中任选一个格子作为路径的起点。
  • 假设矩阵中某个格子的字符为 c h ch ch,并且这个格子将对应于路径上的第 i i i个字符
  • 如果路径上的第i个字符不是 c h ch ch,那么这个格子不可能处在路径上的第 i i i个位置
  • 如果路径上的第i个字符正好是 c h ch ch,那么到相邻的格子寻找路径上的第 i + 1 i+1 i+1个字符
  • 除矩阵边界上的格子之外,其他格子都有四个相邻的格子
  • 重复这个过程,直到路径上的所有字符都在矩阵中找到相应的位置

由于回溯法的递归特性,路径可以被看成一个栈。当在矩阵中定位了路径中前 n n n个字符的位置之后,在与第 n n n个字符对应的格子的周围都没有找到第 n + 1 n+1 n+1个字符,这时候只好在路径上回到第 n − 1 n-1 n1个字符,重新定位第 n n n个字符


代码实现

bool hasPathCore(const char *matrix, int rows, int cols,
	             int row, int col, const char * str, int & pathLength, 
	             bool * visited)
 {
	if (str[pathLength] == '\0')
	{
		return true;
	}

	bool hasPath = false;
	if (row >= 0 && row < rows&&col >= 0 && col < cols
		&&matrix[row*cols + col] == str[pathLength]
		&& !visited[row*cols + col])
	{
		++pathLength;
		visited[row*cols + col] = true;//如果匹配就将该字符标志为访问过
      
        //去该字符的上下左右继续进行下一个字符的匹配
		hasPath = hasPathCore(matrix, rows, cols, row, col - 1, str, pathLength, visited)//左
			|| hasPathCore(matrix, rows, cols, row - 1, col, str, pathLength, visited)//上
			|| hasPathCore(matrix, rows, cols, row, col + 1, str, pathLength, visited)//右
			|| hasPathCore(matrix, rows, cols, row + 1, col, str, pathLength, visited);//下
		if (!hasPath)//四个方向都没有找到匹配的下一个
		{
			--pathLength;
			visited[row*cols + col] = false;//回上一个字符继续进行匹配
		}
	}
	return hasPath;
	
}


bool hasPath(char *matrix, int rows, int cols, char *str)
{
	if (matrix == nullptr || rows < 1 || cols < 1 || str == nullptr)
	{
		return false;
	}
	
	bool *visited = new bool[rows*cols];//用来标志该字符是否被访问过
	memset(visited, 0,rows*cols);

	int pathLength = 0;
	for (int row = 0; row < rows; ++row)
	{
		for (int col = 0; col < cols; ++col)
		{
			if (hasPathCore(matrix, rows, cols, row, col, str, pathLength, visited))
			{
				return true;
			}
		}
	}
	delete[] visited;
	return false;
}

机器人的运动范围

题目描述:

地上有一个m行n列的方格。一个机器人从坐标(0,0)的格子开始移动,它每次可以左、右、上、下移动一格,但不能进入行坐标和列坐标的数位之和大于K的格子。

例如:
k k k 18 18 18时,机器人能够进入方格 ( 35 , 37 ) (35,37) 35,37,因为 3 + 5 + 3 + 7 = 18 3+5+3+7 = 18 3+5+3+7=18。但是,它不能进入方格 ( 35 , 38 ) (35,38) 35,38,因为 3 + 5 + 3 + 8 = 19 3+5+3+8 = 19 3+5+3+8=19。请问该机器人能够达到多少个格子?


解题思路:

回溯法

  • 首先需要计算给定整数上的各个位上数之和
  • 使用一个访问数组记录是否已经经过该格子
  • 机器人从 ( 0 , 0 ) (0,0) (0,0)开始移动,当它准备进入 ( i , j ) (i,j) (i,j)的格子时,通过检查坐标的数位之和来判断机器人是否能够进入
  • 如果机器人能进入 ( i , j ) (i,j) (i,j)的格子,接着在判断它是否能进入四个相邻的格子 ( i , j − 1 ) (i,j-1) (i,j1), ( i , j + 1 ) (i,j+1) (i,j+1), ( i − 1 , j ) (i-1,j) (i1,j), ( i + 1 , j ) (i+1,j) (i+1,j)

代码实现:

计算一个整数各个位数之和

int getDigitSum(int number)
{
	int sum = 0;
	while (number > 0)
	{
		sum += number % 10;
		number /= 10;
	}
	return sum;
}

检查机器人能否进入该格子

bool check(int k, int rows, int cols, int row, int col, vector<bool> &visited)
{
	if (row >= 0 && row < rows&&col >= 0 && col < cols
		&&getDigitSum(row) + getDigitSum(col) <= k
		&& !visited[row*cols + col])
	{
	//数字位数之和小于等于k,并且该格子没有被访问过,即可进入该格子
		return true;
	}
	return false;
}

核心代码:

int movingCountCore(int k, int rows, int  cols, int row, int col, vector<bool> &visited)
{
	int count = 0;
	if (check(k, rows, cols, row, col, visited))
	{
		visited[row*cols + col] = true;
		count=1+
		movingCountCore(k, rows, cols, row-1, col, visited)+
		movingCountCore(k, rows, cols, row+1, col, visited)+
		movingCountCore(k, rows, cols, row, col-1, visited)+
		movingCountCore(k, rows, cols, row, col+1, visited);
	}
	return  count;
}

原函数:


int movingCount(int k, int rows, int cols)
{
	if (k < 0 || rows <= 0 || cols <= 0)
	{
		return 0;
	}
	vector<bool>  visited(rows*cols,false);//标志该位置是否可以到达
	int count = movingCountCore(k, rows, cols, 0, 0, visited);
	return count;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值