数组中重复的数字
题目描述:
找出数组中重复的数字
在一个长度为 n n n的数字里的所有数字都在 0 − n − 1 0-n-1 0−n−1的范围内。数组中某些数字是重复的,但不直到有哪几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。
举例:
输入:长度为7的数组{2,3,0,2,5,3}
输出: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−(n−1)的范围内。如果这个数组中没有重复的数字,那么当数组排序之后数字
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 1−n的范围内,所以数组中至少有一个数字是重复的。
请找出数组中任意一个重复的数字,但不能修改输入的数组。
例如:
输入长度为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
1−n的范围里只有
n
n
n个数字。
由于数组里包含超过
n
n
n个数字,所以一定包含了重复的数字。
因此,在某范围里数字的个数对解决这个问题很重要。
具体实现过程:
- 我们把 1 − n 1-n 1−n的数字从中间的数字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×(n−1)的矩形仍然需要用 2 × 1 2×1 2×1的矩形来覆盖,那么就是 F ( n − 1 ) F(n-1) F(n−1)
- 当第一块选择横着放,那么第二块也必须横着放才能填满下面的空缺。接下来的 2 × ( n − 2 ) 2×(n-2) 2×(n−2)的矩形面临同样的问题。
递推公式如下:
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的矩阵中包含一条字符串
"
b
f
c
e
"
"bfce"
"bfce"的路径(路径中的字母用下画线标出)。但矩阵中不包含字符串
"
a
b
f
b
"
"abfb"
"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 n−1个字符,重新定位第 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,j−1), ( i , j + 1 ) (i,j+1) (i,j+1), ( i − 1 , j ) (i-1,j) (i−1,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;
}