文章目录
剪绳子
题目描述:
给你一根长度为 n n n绳子,请把绳子剪成 m m m段( m m m、 n n n都是整数, n > 1 n>1 n>1并且 m ≥ 1 m≥1 m≥1)。每段的绳子的长度记为 k [ 0 ] 、 k [ 1 ] 、 … … 、 k [ m ] k[0]、k[1]、……、k[m] k[0]、k[1]、……、k[m]。 k [ 0 ] ∗ k [ 1 ] ∗ … ∗ k [ m ] k[0]*k[1]*…*k[m] k[0]∗k[1]∗…∗k[m]可能的最大乘积是多少?
举例:
当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18
解题思路一:
动态规划
- 首先定义函数 f ( n ) f(n) f(n)为把长度为n的绳子剪成若干段后各段长度乘积的最大值
- 在剪第一刀的时候,我们有 n − 1 n-1 n−1种可能的选择(也就是剪出来的第一段绳子的可能长度分别为 1 , 2 , . . . . , n − 1 1,2,....,n-1 1,2,....,n−1)。
- 因此, f ( n ) = m a x ( f ( i ) × f ( n − i ) ) f(n)=max(f(i)×f(n-i)) f(n)=max(f(i)×f(n−i)),其中 0 < i < n 0<i<n 0<i<n。
代码实现:
//输入绳子的长度,输出最大乘积
int maxProductAfterCutting_solution1(int length) {
if(length < 2)//因为要求长度n>1,所以这里返回0表示输入非法
return 0;
if(length == 2)//长度为2时,因为要求剪下段数m>1,所以最大是1x1=1
return 1;
if(length == 3)//长度为3时,因为要求剪下段数m>1,所以最大是1x2=2
return 2;
//运行至此,说明绳子的长度是>3的,这之后0/1/2/3这种子问题最大就是其自身长度
//而不再需要考虑剪一刀的问题,因为它们剪一刀没有不剪来的收益高
//而在当下这么长的绳子上剪过才可能生成0/1/2/3这种长度的绳子,它们不需要再减
//所以下面的表中可以看到它们作为子问题的值和上面实际返回的是不一样的
//在表中先打上子绳子的最大乘积
int* products = new int[length + 1];
products[0] = 0;
products[1] = 1;
products[2] = 2;
products[3] = 3;//到3为止都是不剪最好
int max = 0;//用于记录最大乘积
//对于4以上的情况,每次循环要求f(i)
for(int i = 4; i <= length; ++i) {
max = 0;//每次将最大乘积清空
//因为要计算f(j)乘以f(i-j)的最大值,j超过i的一半时是重复计算
//所以只需要考虑j不超过i的一半的情况
for(int j = 1; j <= i / 2; ++j) {
//计算f(j)乘以f(i-j)
int product = products[j] * products[i - j];
//如果比当前找到的还大
if(max < product)
max = product;//就把最大值更新
}
//这里是循环无关的,书上在for里面,我把它提出来
products[i] = max;//最终,更新表中的f(i)=max(f(j)·f(i-j))
}
//循环结束后,所求的f(length)也就求出来了
max = products[length];//将其记录下来以在销毁后能返回
delete[] products;//销毁打表的辅助空间
return max;
}
解题思路二:$
- 当 n > = 5 n>=5 n>=5时, 3 ( n − 3 ) > = 2 ( n − 2 ) 3(n−3)>=2(n−2) 3(n−3)>=2(n−2)且只在 n n n取 5 5 5时取等号,且它们都大于 n n n,所以应把绳子剪成尽量多的 3 3 3
- 当剩下的绳子长度为 4 4 4时,把绳子剪成两段长度为 2 2 2的绳子
代码实现:
int maxProductAfterCutting_solution2(int length) {
if(length < 2)//因为要求长度n>1,所以这里返回0表示输入非法
return 0;
if(length == 2)//长度为2时,因为要求剪下段数m>1,所以最大是1x1=1
return 1;
if(length == 3)//长度为3时,因为要求剪下段数m>1,所以最大是1x2=2
return 2;
//尽可能多地减去长度为3的绳子段,这里是计算一下能减出多少个3
int timesOf3 = length / 3;
//当绳子最后剩下的长度为4的时候,不能再剪去长度为3的绳子段。
//此时更好的方法是把绳子剪成长度为2的两段,因为2*2>3*1。
if(length - timesOf3 * 3 == 1)//如果最后只剩一个1
timesOf3 -= 1;//就要留下一个3
//保证剩下的一定是4或者2或者0,计算一下能剪出几个2来(只有2/1/0三种情况)
int timesOf2 = (length - timesOf3 * 3) / 2;
//3的多少个3次幂,再乘以2的多少个2次幂,就是贪心选择的最优解
return (int) (pow(3, timesOf3)) * (int) (pow(2, timesOf2));
}
二进制中1的个数
题目描述:
输入一个整数,输出该数二进制表示中1的个数
举例:
把
9
9
9表示成二进制是
1001
1001
1001,有
2
2
2位是
1
1
1。
输入:9
输出:2
解题思路:
- 把一个整数减去 1 1 1,再和原整数做与运算
- 会把该整数最右边的 1 1 1变成 0 0 0
- 那么,一个整数的二进制表示中有多少个 1 1 1,就可以进行多少次这样的操作
代码实现:
int NumberOf1(int n)
{
int count = 0;
while (n != 0) {
count++;
n = (n - 1) & n;
}
return count;
}
题目描述:
实现函数 double Power(double base, int exponent),求base得exponent次方。不得使用库函数,同时不需要考虑大数问题
解题思路:
- 假如我们的目标是求一个数得 32 32 32次方,如果我们已经知道了它的 16 16 16次方,那么只要在 16 16 16次方的基础上再平方一次就可以了。
- 而 16 16 16次方是 8 8 8次方的平方,这样以此类推,我们求 32 32 32次方只需要做 5 5 5次乘方
先求平方,在平方的基础上求4次方
在4次方的基础上求8次方
在8次方的基础上求16次方
最后,在16次方的基础上求32次方
也就是说,我们可以用如下的公式求
a
a
a的
n
n
n次方
代码实现:
double PowerWithUnsignedExponent(double base, unsigned int exponent)
{
if (exponent == 0)
{
return 1;
}
if (exponent == 1)
{
return base;
}
double result = PowerWithUnsignedExponent(base, exponent >> 1);
result *= result;
if (exponent & 0x1 == 1)//exponent为奇数
{
result *= base;
}
return result;
}
打印从1到n的最大n位数
题目描述:
输入数字n,按顺序打印从1到最大的n位的十进制数。
举例:
比如输入3,就打出1,2,3一直到最大的3位数999
解题思路一
- n n n位所有十进制数其实就是 n n n个从 0 − 9 0-9 0−9的全排列
- 也就是说,我们把数字的每一位都从 0 − 9 0-9 0−9排列一遍,就得到了所有的十进制数
- 只是在打印的时候,排在前面的 0 0 0不打印出来罢了。
- 全排列用递归很容易实现,数字的每一位都可能是 0 − 9 0-9 0−9中的一个数
- 然后设置下一位
- 递归结束的条件就是我们已经设置了数字的最后一位
代码实现:
打印数字
void PrintNumber(char *number)
{
int nLen = strlen(number);
bool bIsBegin0 = true;
for(int i=0; i<nLen; i++){
if( bIsBegin0 && '0' != number[i] ){
bIsBegin0 = false;
}
if( !bIsBegin0 ){
printf("%c", number[i]);
}
}
printf("\t");
}
打印1到最大的n位数
void Print1toMaxNDigits(int n)
{
if( n < 0 ){
return;
}
char *number = new char[n+1];
number[n] = '\0';
for(int i=0; i<10; i++){
number[0] = i + '0';
Print1toMaxNDigitsRecursively(number, n, 0);
}
delete[] number;
}
数字排列代码:
void Print1toMaxNDigitsRecursively(char* number, int nLength, int nIdx)
{
if( nIdx == nLength - 1 )
{
PrintNumber(number);
return;
}
for(int i=0; i<10; i++){
number[nIdx+1] = i + '0';
Print1toMaxNDigitsRecursively(number, nLength, nIdx+1);
}
}
反转链表
ListNode* reverseList(ListNode* head) {
if(head==nullptr)
{
return nullptr;
}
ListNode *newList=nullptr;
ListNode *cur=head;
ListNode *pre=nullptr;
while(cur->next!=nullptr)
{
ListNode *tmp=cur->next;//保存当前元素的下一个
cur->next=pre;//让当前元素的前一个元素作为当前元素的下一个元素
pre=cur;//前一个元素移动到当前元素的位置
cur=tmp;//当前元素后移
}
cur->next=pre;//让最后一个节点指向它的前驱节点
newList=cur;//让最后一个节点作为新的头结点
return newList;//返回新链表的头节点
}
删除链表的节点
题目描述:
在 O ( 1 ) O(1) O(1)时间删除链表节点。给定单向链表的头指针和一个节点指针,定义一个函数在 O ( 1 ) O(1) O(1)时间删除该节点。
节点定义如下:
struct ListNode
{
int m_nValue;
ListNode* m_pNext;
};
解题思路:
- 因为是单链表,正常情况下,删除一个 a a a节点,需要知道 a a a节点的前驱节点,但是这样时间复杂度就是 O ( n ) O(n) O(n)了,不符合要求。
- 那么只有把 a a a节点的后继节点的值复制过来,再删除掉 a a a节点的后继节点。
代码实现:
void DeleteNode(ListNode **pListHead, ListNode * pToBeDeleted)
{
if (!pListHead || !pToBeDeleted)
{
return;
}
//要删除的节点不是尾节点
if (pToBeDeleted->m_pNext != nullptr)
{
ListNode *pNext = pToBeDeleted->m_pNext;//待删除节点的后继节点
pToBeDeleted->m_nValue = pNext->m_nValue;// 将待删除节点的后继节点的值赋给待删除节点
pToBeDeleted->m_pNext = pNext->m_pNext;//断开当前节点和其后继节点
delete pNext;// 删除后继节点
pNext = nullptr;
}
//链表只有一个节点,删除头节点
else if (*pListHead == pToBeDeleted)
{
delete pToBeDeleted;
pToBeDeleted = nullptr;
*pListHead = nullptr;
}
else //待删除的节点在尾部
{
ListNode *pNode = *pListHead;
while (pNode->m_pNext != pToBeDeleted)
{
pNode = pNode->m_pNext;
}
pNode->m_pNext = nullptr;
delete pToBeDeleted;
pToBeDeleted = nullptr;
}
}
删除链表中重复的节点
题目描述:
在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。
举例:
输入:1->2->3->3->4->4->5
输出:1->2->5
节点结构:
struct ListNode
{
int val;
struct ListNode *next;
ListNode(int x) :val(x), next(NULL) {}
};
解题思路:
三个指针和两层循环实现删除链表中重复的节点。
- 首先,检查边界条件(链表有0个节点或链表有1个节点)时,返回头结点
- 其次,避免由于第一个节点是重复节点而被删除,新建一个指向头结点的节点
- 再次,建立三个指针pre/p/next,分别指向当前节点的前序节点、当前节点、当前节点的后续节点
- 最后循环遍历整个链表,如果节点p的值和节点next的值相同,则删除节点p和节点next,pre和下一个没有重复的节点连接。
- 如果节点p的值和节点next的值不同,则三个指针向后移动一个指针。
代码实现:
class Solution {
public:
ListNode* deleteDuplication(ListNode* pHead)
{
// 链表有0个/1个节点,返回第一个节点
if(pHead==NULL||pHead->next==NULL)
return pHead;
else
{
// 新建一个头节点,防止第一个结点被删除
ListNode* newHead=new ListNode(-1);
newHead->next=pHead;
// 建立索引指针
ListNode* p=pHead; // 当前节点
ListNode* pre=newHead; // 当前节点的前序节点
ListNode* next=p->next; // 当前节点的后序节点
// 从头到尾遍历编标
while(p!=NULL && p->next!=NULL)
{
if(p->val==next->val)//如果当前节点的值和下一个节点的值相等
{
// 循环查找,找到与当前节点不同的节点
while(next!=NULL && next->val==p->val)
{
ListNode* temp=next;
next=next->next;
// 删除内存中的重复节点
delete temp;
temp = nullptr;
}
pre->next=next;
p=next;
}
else//如果当前节点和下一个节点值不等,则向后移动一位
{
pre=p;
p=p->next;
}
next=p->next;
}
return newHead->next;//返回头结点的下一个节点
}
}
};
正则表达式的匹配
题目描述:
请实现一个函数用来匹配包括’.’和’‘的正则表达式。模式中的字符’.’表示任意一个字符,而’‘表示它前面的字符可以出现任意次(包含0次)。 在本题中,匹配是指字符串的所有字符匹配整个模式。
举例:
字符串”aaa”与模式”a.a”和”ab*ac*a”匹配,但是与”aa.a”和”ab*a”均不匹配
解题思路:
- 每次从p中拿出一个字符来与s中的字符进行匹配
- 如果该字符后续的字符不是 ∗ * ∗那么直接与s中对应字符进行匹配判断即可,,如果匹配上了,那么就将两个游标都往后移动一位。
- 如果匹配过程中遇到不相等的情况,则直接返回false。
- 如果后续字符是 ∗ * ∗,那么就如上面所分析的,分成两种情况
- 一种是匹配0个,那么只需要跳过p中的这两个字符,继续与s中的字符进行比较即可
- 如果是匹配多个,那么将s中的游标往后移动一个,继续进行判断。这两个条件只要其中一个能满足即可。
代码实现:
bool match(char *s,char *p)
{
if(*s=='\0'&&*p=='\0')
{
return true;
}
if(*s!='\0'&&*p=='\0')
{
return false;
}
if(*(p+1)=='*')
{
//分三种情况
//1.第一个字符匹配
if(*p==*s||*p=='.'&&*s!='\0')
{
return match(s+1,p)||match(s,p+2);
}
else
{
return match(s,p+2);
}
}
else
{
if(*s==*p||*p=='.'&&*s!='\0')
{
return match(s+1,p+1);
}
else
{
return false;
}
}
}
bool isMatch(char * s, char * p){
if(s==NULL||p==NULL)
{
return false;
}
return match(s,p);
}
表示数值的字符串
题目描述:
请实现一个函数用来判断字符串是否表示数值(包括整数和小数)
举例:
字符串"+100",“5e2”,"-123",“3.1416"和”-1E-16"都表示数值。
但是"12e",“1a3.14”,“1.2.3”,"±5"和"12e+4.3"都不是。
- 表示数值的字符串遵循模式A[.[B]][e|EC]或者.B[e|EC],其中A为数值的整数部分,
B紧跟着小数点为数值的小数部分,C紧跟着’e’或者’E’为数值的指数部分。 - 在小数里可能没有数值的整数部分(.123==0.123)
- A和C都是可能以’+‘或者’-'开头的0-9的数位串
- B也是0-9的数位串,但前面不能有正负号
解题思路:
- 首先尽可能多地扫描0-9的数位(有可能在起始处有’+‘或者’-’),也就是前面模式中表示数组整数的A部分
- 如果遇到小数点’.’,则开始扫描数值小数部分的B部分
- 如果遇到’e’或’E’,则开始扫描数值指数的C部分
代码实现:
class Solution {
public:
bool isNumeric(char* string)
{
if(string == NULL) return false;
bool sign = false, dot = false, hasE = false;
for(int i = 0; i < strlen(string); i++){
if(string[i] == '+' || string[i] == '-'){//正负号只能出现在第一个或e后面
//第一次出现 必须是第一位或在e后面
if( !sign && i != 0 && string[i-1] != 'e' && string[i-1] != 'E')
return false;
//第二次出现 必须是在e后面
if( sign && string[i-1] != 'e' && string[i-1] != 'E') return false;
sign = true;
}
else if(string[i] == 'e' || string[i] == 'E'){
//e不能出现两次,也不能在最后一个
if(hasE || i == strlen(string) - 1) return false;
hasE = true;
}
else if(string[i] == '.'){
//小数点不能出现两次 不能跟在e后面 也不能出现在最后一位
if(hasE || dot || i == strlen(string) - 1) return false;
dot = true;
}
else if(string[i] < '0' || string[i] > '9'){//其他非数字的 均错误 注意是字符不是数字
return false;
}
}
return true;
}
};
调整数组顺序使奇数位于偶数前面
题目描述:
输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有的奇数位于数组的前半部分,所有的偶数位于数组的后半部分
解题思路:
维护两个指针
- 第一个指针初始化为指向数组的第一个数字,它只向后移动
- 第二个指针初始化为指向数组的最后一个数字,它只向前移动
- 在两个指针相遇前,第一个指针总是位于第二个指针的前面
- 如果第一个指针指向的数字是偶数,并且第二个指针指向的数字是奇数,则交换这两个数字
代码实现:
class Solution_no_stable {
public:
void ReArray(int *array2, int length)
{
// 特殊输入
if (array2 == nullptr || length == 0)
return;
// 指针遍历数组
int *pLeft = array2;
int *pRight = array2 + length - 1;
while (pLeft < pRight)
{
// 向后移动pLeft,直到指向偶数
while (pLeft < pRight && (*pLeft &0x1)!=0)
pLeft++;
// 向前移动pRight,直到指向奇数
while (pLeft < pRight && (*pRight & 0x1) == 0)
pRight--;
// 奇偶数交换
if (pLeft < pRight)
swap(*pLeft, *pRight);
}
}
};
链表中倒数第K个节点
题目描述:
输入一个链表,输出该链表中倒数第k个结点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点。
例如:
一个链表有6个节点,从头节点开始,它们的值依次是1、2、3、4、5、6
这个链表的倒数第3个节点是值为4的节点
链表节点定义如下:
struct ListNode
{
int val;
struct ListNode *next;
}
解题思路:
两个指针遍历链表:
- 第一个指针从链表的头节点走 k − 1 k-1 k−1步,第二个指针不动
- 从第 k k k步开始,第二个指针也开始从链表的头节点开始遍历,直到第一个指针指向链表结尾。
- 由于两个指针的距离保持 k − 1 k-1 k−1,当第一个指针到达链表的尾节点时,第二个指针指向倒数第k个节点。
代码实现:
class Solution {
public:
ListNode* FindKthToTail(ListNode* pListHead, unsigned int k) {
// 查找倒数第0个节点和输入空指针
if(pListHead == nullptr || k == 0){
return NULL;
}
// 两个指针遍历链表
ListNode *pAhead = pListHead;
ListNode *pBehind = pListHead;
// 第一个指针从链表的头结点走K-1步
for(unsigned int i = 0; i < k - 1; i++){
if(pAhead->next != NULL){
pAhead = pAhead->next;
}
else{
return nullptr;
}
}
// 第k个节点开始,两个指针同时遍历
while(pAhead->next != NULL){
pAhead = pAhead->next;
pBehind = pBehind->next;
}
return pBehind;
}
};
链表中环的入口节点
题目描述:
如果一个链表中包含环,如何让找出环的入口节点?
例如,在下图的链表中,环的入口节点就是节点3
解题思路:
首先如何确定一个链表中有环?
- 用两个指针来解决上述问题。
- 定义两个指针,同时从链表的头节点出发
- 一个指针依次走一步,一个指针依次走两步
- 如果走的快的指针追上了走的慢的指针,那么链表就包含环
- 如果走的快的指针走到了链表的末尾(m_pNext指向NULL)都没有追上走的慢的指针,那么链表就不包含环
第二步是如何让找到环的入口?
- 依然使用两个指针来解决问题
- 先定义两个指针P1和P2指向链表的头节点
- 如果链表中的环有n个节点
- 则指针P1先在链表上向前移动n步
- 然后两个指针以相同的速度向前移动
- 当第二个指针指向环的入口节点时,第一个指针已经围绕环走了一圈,又回到了入口节点
代码实现:
判断链表是否存在环
ListNode *MeetingNode(ListNode *pHead)
{
if (pHead == nullptr)
{
return nullptr;
}
ListNode *pSlow = pHead->next;//慢指针
if (pSlow == nullptr)
{
return nullptr;
}
ListNode *pFast = pSlow->next;//快指针
while (pFast != nullptr&&pSlow != nullptr)
{
if (pFast == pSlow)//两个相遇,返回相遇点(环中一个节点)
{
return pFast;
}
pSlow = pSlow->next;
if (pFast != nullptr)
{
pFast = pFast->next;
}
}
return nullptr;
}
找环的入口点
ListNode *EntryNodeOfLoop(ListNode *pHead)
{
ListNode *meetingNode = MeetingNode(pHead);
if (meetingNode == nullptr)//不存在环
{
return nullptr;
}
int nodeInLoop = 1;//得到环中节点的数目
ListNode *pNode1 = meetingNode;
while (pNode1->next != meetingNode)
{
pNode1 = pNode1->next;
++nodeInLoop;
}
// 先移动pNode1,次数为环中节点的数目
pNode1 = pHead;//从头结点开始
for (int i = 0; i < nodeInLoop; ++i)
{
pNode1 = pNode1->next;
}
//同时移动pNode1和pNode2;
ListNode *pNode2 = pHead;
while (pNode1 != pNode2)
{
pNode1 = pNode1->next;//两个一起走
pNode2 = pNode2->next;
}
return pNode1;// 当两个相等时,就是环的入口点
}
反转链表
题目描述:
输入一个链表的头结点,反转该链表并输出反转后链表的头节点
链表的节点定义如下:
struct ListNode
{
int val;
struct ListNode *next;
}
解题思路:
- 定义三个用于反转链表的辅助指针和一个用于表示反转链表头结点的指针,
- n o d e node node指向当前节点、 l e f t left left指向当前节点的前一个节点、 r i g h t right right指向当前节点的下一个节点、 R e v e r s e H e a d ReverseHead ReverseHead指向反转链表的头结点。
- 循环反转链表,每次循环反转一个结点。
- 判断 n o d e node node是否是最后一个结点,如果是最后一个节点,则 r e v e r s e H e a d reverseHead reverseHead指向node(确定翻转链表表头节点),然后 n o d e node node指向 l e f t left left,退出循环
- 如果不是最后一个节点,则 n o d e node node指向 l e f t left left,移动 l e f t left left和 n o d e node node指针。
代码实现:
ListNode *ReverseList(ListNode *pHead)
{
ListNode *ReverseHead = nullptr;
ListNode *pNode = pHead;
ListNode *left = nullptr;
while (pNode != nullptr)
{
ListNode *right = pNode->next;// 保存当前节点的后继节点
if (right == nullptr)//当前节点为尾节点
{
ReverseHead = pNode;
}
pNode->next = left;
left = pNode;
pNode = right;
}
return ReverseHead;
}
合并两个排序的链表
题目描述:
将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
解题思路:
- 从合并两个链表的头节点开始
- 头结点值小的链表的头结点将是合并后新链表的头节点
- 当我们得到两个链表中值较小的头结点并把它链接到已经合并的链表之后,两个来年表的剩余节点依然是有序的
- 因此,合并的步骤和之前的步骤是一样的
代码实现:
class Solution {
public:
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
ListNode * newList=new ListNode(0);
ListNode * newhead=newList;
while(l1&&l2)
{
if(l1->val<l2->val)
{
newList->next=l1;//值小的作为新的节点
l1=l1->next;
}
else
{
newList->next=l2;
l2=l2->next;
}
newList=newList->next;
}
if(l1!=nullptr)//其中一个为处理完,直接将另外一个进行链接
{
newList->next=l1;
}
if(l2!=nullptr)
{
newList->next=l2;
}
return newhead->next;
}
};