剑指offer练习
写一个程序要注意:
1. 功能性测试(完成基本功能)
2. 边界值测试(单独处理边界)
3. 负面测试(处理无效数据)
其余的还有:
1. 变量命名规范化,可读化
2. 释放一个内存时,要记得把引用这块内存的指针置为NULL
通知函数的调用者函数出错有三种方式,比如第16题涉及到如果对0求负数幂,由于不能对0求倒数,应该提示调用出错:
1. 返回值
2. 全局变量
3. 抛出异常
基本概念:
1. 鲁棒性(健壮性)指程序能够判断输入是否合乎规范要求,并对不符合要求的输入予以合理的处理。
1. 赋值运算符
class CMyString
{
char* m_pData;
int size;
}
}
写一个CMyString类的赋值运算符。
CMyString& operator = (const CMyString& s)
{
if( this == &s) return * this;
if(m_pData)
delete[] m_pDate; // 或者delete m_pData; 也可以
m_pData = nullptr;
m_pData = new char[s.size + 1];
strcpy(m_pData, s.m_pData);
size = s.size;
return * this;
}
这种写法在内存不足无法分配时会抛出new char异常,m_pData成为一个空指针。改进的方法是先分配内存,成功之后再修改自身的数据,否则不修改。
CMyString& operator = (const CMyString& s)
{
if(this == &s) return this;
char* temp = new char[s.size + 1];
if(temp){ // 如果分配成功了
delete m_pData;
m_pData = temp;
size = s.size;
}
return *this;
}
另一种方法是使用一个临时对象,我们尽量不要在构造函数里面删除自身数据,而应该统一在析构函数里完成,通过临时对象可以完成这一点:
CMyString& operator = (const CMyString& s)
{
if(this != &s)
{
CMyString temp = CMyString(s);
// 交换自身的数据指针和临时对象的数据指针
char* tp = s.m_pData;
s.m_pData = m_pData;
m_pData = tp;
} // 出了if块之后,会调用temp的析构,释放它的内存
return *this;
}
CMyString& operator = (const CMyString& s)
{
if(this != &s)
{
CMyString temp = CMyString(s);
// 交换自身的数据指针和临时对象的数据指针
char* tp = s.m_pData;
s.m_pData = m_pData;
m_pData = tp;
} // 出了if块之后,会调用temp的析构,释放它的内存
return *this;
}
2. 单例模式
单例模式有懒汉和饿汉两种,其中饿汉方式是线程安全的,一旦类被加载就会去创建实例,而且只会创建一次,所以在饿汉模式下的单利模式getInstance()里不需要判断static对象是否为空,是必非空的。
而懒汉模式在getInstance里实例化static对象,会发生线程安全性问题。如果两个线程先后实例化(都执行构造函数),会出现构造了两次的问题。懒汉模式的getInstance在多线程环境下需要加锁,否则就是线程不安全的。
懒汉模式
| -- 没加锁的懒汉模式(线程不安全)
| -- 加锁的懒汉模式
| -- 单次判断(线程安全但效率低)
| -- 双判空(线程安全且高效)
2.1 加一个锁,一次判断。每次只有一个线程进入临界区,每次调用getInstance必须排队。
CMyClass* getInstance()
{
lock();
if(m_instance == NULL)
{
m_instance = new CMyClass();
}
unlock();
return m_instance;
}
2.2 加一个锁,两次判断。因为只在第一次实例化的时候会有多线程问题,所以只需要在if判断快里加锁就可以了。
CMyClass* getInstance()
{
if(m_instance == NULL)
{
lock();
if(m_instance == NULL) // 第二次判断是防止排队的第二个线程在第一个线程已经实例化之后,再次实例化
{
m_instance = new CMyClass();
}
unlock();
}
}
3. 找数组中重复的数字
问题描述:长度为n的数组里,数字大小在 0~ n-1之间,其中某些数字重复,但不知道哪些重复了,也不知道重复了几次。请找出任意一个重复的数字。
解法:
1. 给数组先排序,O(nlogn),然后以O(n)扫描一下,有相邻两个一样就找到了重复的。
2. 用哈希表,只需要一次遍历,时间复杂度O(n), 空间复杂度O(n),用来保存一个哈希表。
#include <iostream>
using namespace std;
#define SIZE 10
int hashTable[SIZE] = {0};
int main()
{
int num[SIZE] = {1, 2, 2, 3, 4, 5, 5 ,6, 7};
int repNum = -1;
for (int i = 0; i < SIZE; i++)
{
if (hashTable[num[i]] != 0)
{
repNum = num[i];
break;
}
hashTable[num[i]] ++;
}
cout << repNum;
}
using namespace std;
#define SIZE 10
int hashTable[SIZE] = {0};
int main()
{
int num[SIZE] = {1, 2, 2, 3, 4, 5, 5 ,6, 7};
int repNum = -1;
for (int i = 0; i < SIZE; i++)
{
if (hashTable[num[i]] != 0)
{
repNum = num[i];
break;
}
hashTable[num[i]] ++;
}
cout << repNum;
}
3. 看题,容量为n的数组,元素内容也在0~n-1之间,说明如果元素无重复的话,应该是每个位置的索引和元素值一样,也就是说,数字i在排序之后应该出现在第i个位置上。而现在出现了重复,说明有个位置被占了。我们可以遍历数组,尝试把每个元素放到它理应在的位置上(数字i在第i个位置)。如果发现那个位置i被另一个i占了,说明i就是一个重复的数字。时间复杂度为O(n)。
#include <iostream>
using namespace std;
#define SIZE 10
int main()
{
int n[SIZE] = { 1, 2, 3, 4, 2, 5, 5 ,6, 7 };
#define swap(x, y)\
do {\
int t = x;\
x = y;\
y = t;\
}while(0)
int res = -1;
for (int i = 0; i < SIZE; )
{
if (n[i] != i)
{
if (n[n[i]] == n[i])
{
res = n[i];
break;
}
// 把数字放到它应在的位置
int v = n[i];
// 注意这里不能n[n[i]],一旦n[i]改了,结果就不对了
swap(n[i], n[v]);
}
}
cout << res;
}
using namespace std;
#define SIZE 10
int main()
{
int n[SIZE] = { 1, 2, 3, 4, 2, 5, 5 ,6, 7 };
#define swap(x, y)\
do {\
int t = x;\
x = y;\
y = t;\
}while(0)
int res = -1;
for (int i = 0; i < SIZE; )
{
if (n[i] != i)
{
if (n[n[i]] == n[i])
{
res = n[i];
break;
}
// 把数字放到它应在的位置
int v = n[i];
// 注意这里不能n[n[i]],一旦n[i]改了,结果就不对了
swap(n[i], n[v]);
}
}
cout << res;
}
4. 二维数组中的查找
问题描述:有一个二维数组,每行都按照从左到右递增,每一列按照从上到下递增。写一个函数,接受一个二维数组和一个整数,判断二维数组中是否有该整数。
比如有一个二维数组:
1 2 8 9
2 4 9 12
4 7 10 13
6 8 11 15
查找7, 5...
解决方案:从右上角(比如9)开始缩小查找区域,如果右上角的值大于目标值,则该列不可能有目标值,剔除一列,接着判断左边一列。如果某列第一个值小于目标值,则目标值有可能在此列,且不可能在此行,剔除此行(因为更左边更不可能有了,而右边的已经剔除了),规模立马缩小了。接着就是重复这个步骤,知道找到目标值,或者列到底,或者行到头,表示没有这个值。也就是说,每次我们只会去那右上角和目标值比较,不会去遍历整个表,或者某行某列。
#include <iostream>
using namespace std;
#define SIZE 4
using namespace std;
#define SIZE 4
// 以(i, j)为右上角的矩形
bool find(int n[][SIZE], int num, int i, int j)
{
if (i > SIZE || j < 0) // 递归终点
return false;
if (n[i][j] > num)
return find(n, num, i, j - 1);
else if (n[i][j] == num)
return true;
else
return find(n, num, i + 1, j);
}
int main()
{
int n[][SIZE] = {
{1, 2, 8, 9},
{2, 4, 9, 12},
{4, 7, 10, 13},
{6, 8, 11, 15},
};
if (find(n, 14, 0, 3))
{
cout << "find number " << endl;
}
else
{
cout << "didnt find number" << endl;
}
}
bool find(int n[][SIZE], int num, int i, int j)
{
if (i > SIZE || j < 0) // 递归终点
return false;
if (n[i][j] > num)
return find(n, num, i, j - 1);
else if (n[i][j] == num)
return true;
else
return find(n, num, i + 1, j);
}
int main()
{
int n[][SIZE] = {
{1, 2, 8, 9},
{2, 4, 9, 12},
{4, 7, 10, 13},
{6, 8, 11, 15},
};
if (find(n, 14, 0, 3))
{
cout << "find number " << endl;
}
else
{
cout << "didnt find number" << endl;
}
}
5. 替换空格
问题描述:实现一个函数,把字符串中的每个空格替换成"%20",例如,输入“We are happy.” ,则输出"We%20are%20happy."。比如给定一个string buffer,容量足够大,替换空格。
解决方案:我们可以先遍历一边字符O(n),统计字符串中空格的个数,以得到需要的字符串长度,新长度=原来长度+空格数x2。然后从新长度的末尾开始修改原buffer,从后往前复制是一个技巧,可以减少移动的次数,写法也很方便。
#include <iostream>
using namespace std;
#define SIZE 1024 // buffer size
int main()
{
char strbuf[SIZE] = "We are happy.";
//cout << sizeof(strbuf); // 1024
int originalLen = strlen(strbuf) + 1; // 13 + 1, 包含结尾0
int newlen = originalLen;
for (int i = 0; strbuf[i] != 0; i++)
{
if (strbuf[i] == ' ')
newlen += 2;
}
if (newlen > SIZE)
return 0;
int pre = originalLen - 1;
int beh = newlen - 1;
while (pre >= 0)
{
if (strbuf[pre] == ' ')
{// 替换
strbuf[beh--] = '0';
strbuf[beh--] = '2';
strbuf[beh--] = '%';
}
else
{// 直接覆盖
strbuf[beh--] = strbuf[pre];
}
--pre;
}
cout << strbuf;
return 0;
}
using namespace std;
#define SIZE 1024 // buffer size
int main()
{
char strbuf[SIZE] = "We are happy.";
//cout << sizeof(strbuf); // 1024
int originalLen = strlen(strbuf) + 1; // 13 + 1, 包含结尾0
int newlen = originalLen;
for (int i = 0; strbuf[i] != 0; i++)
{
if (strbuf[i] == ' ')
newlen += 2;
}
if (newlen > SIZE)
return 0;
int pre = originalLen - 1;
int beh = newlen - 1;
while (pre >= 0)
{
if (strbuf[pre] == ' ')
{// 替换
strbuf[beh--] = '0';
strbuf[beh--] = '2';
strbuf[beh--] = '%';
}
else
{// 直接覆盖
strbuf[beh--] = strbuf[pre];
}
--pre;
}
cout << strbuf;
return 0;
}
6. 从尾到头打印链表
问题描述:链表是面试中常考的内容,链表的各种操作都要能默写出来,并且要保证robust。要从尾到头打印链表,也就是先打印本节点的下一个节点,再打印本节点。很简单的递归。
解决方案:
void PrintListReversingly(ListNode* root)
{
if(root == NULL) return ;
PrintListReversingly(root->m_pNext);
cout << root->m_pData << endl;
}
{
if(root == NULL) return ;
PrintListReversingly(root->m_pNext);
cout << root->m_pData << endl;
}
递归都可以通过栈来改写成非递归:
void PrintListReversingly(ListNode* root)
{
if(root == NULL) return ;
stack<ListNode*> nodes;
while(root)
{
nodes.push(root);
root = root->next;
}
while(!nodes.empty())
{
ListNode* p = nodes.top();
nodes.pop();
cout << p->m_pData;
}
}
{
if(root == NULL) return ;
stack<ListNode*> nodes;
while(root)
{
nodes.push(root);
root = root->next;
}
while(!nodes.empty())
{
ListNode* p = nodes.top();
nodes.pop();
cout << p->m_pData;
}
}
7. 重建二叉树
问题描述:二叉树也是面试中常考的内容,二叉树的各种操作也需要清楚。典型的平凡二叉树,结点的结构是:
struct Node
{
Node* left;
Node* left;
Node* right;
int value;
}
重建二叉树是指,给定了前序遍历和中序遍历结果,构造出一颗二叉树。并能给出它的后序遍历结果。比如给定了前序结果1, 2 , 4, 7, 3, 5, 6, 8, 和中序结果4, 7, 2, 1, 5, 3, 8, 6,要求用上面的Node结点结构重建二叉树,并返回根结点。
解决方案:看书的第63页的图。
#include <iostream>
using namespace std;
struct Node
{
int value;
Node* right;
Node* left;
};
//
Node* rebuild(int dlr[], int dlr_p, int s,// 前序遍历和区间, dir_p表示前序遍历中的子序列索引,s表示中序和前序序列的大小
int ldr[], int ldr_l, int ldr_r) // 中序遍历和区间, ldr_l,ldr_r分别表示中序遍历中子树的范围
{
// 边界
if (dlr_p < 0 || dlr_p >= s) return NULL;
if (ldr_l > ldr_r) return NULL;
// 判断
Node* node = new Node;
node->value = dlr[dlr_p]; // 前序遍历的第一个点就是根结点
// 从中序遍历中寻找根结点的位置
int pos = -1;
for (int i = ldr_l; i <= ldr_r; i++)
{
if (ldr[i] == node->value)
{
pos = i;
break;
}
}
int lsize = pos - ldr_l; // 左子树宽度
int rsize = ldr_r - pos; // 右子树宽度
node->left = rebuild(dlr, dlr_p + 1, s, ldr, ldr_l, pos - 1);
node->right = rebuild(dlr, dlr_p + lsize + 1, s, ldr, pos + 1, ldr_r);
return node;
}
// 后续遍历
void lrd(Node* root)
{
if (root == NULL)return;
lrd(root->left);
lrd(root->right);
cout << root->value << endl;
}
int main()
{
const int size = 8;
int dlr[size] = {1, 2, 4, 7, 3, 5, 6, 8};
int ldr[size] = {4, 7, 2, 1, 5, 3, 8, 6};
Node* root = rebuild(dlr, 0, size, ldr, 0, size - 1);
lrd(root);
}
using namespace std;
struct Node
{
int value;
Node* right;
Node* left;
};
//
Node* rebuild(int dlr[], int dlr_p, int s,// 前序遍历和区间, dir_p表示前序遍历中的子序列索引,s表示中序和前序序列的大小
int ldr[], int ldr_l, int ldr_r) // 中序遍历和区间, ldr_l,ldr_r分别表示中序遍历中子树的范围
{
// 边界
if (dlr_p < 0 || dlr_p >= s) return NULL;
if (ldr_l > ldr_r) return NULL;
// 判断
Node* node = new Node;
node->value = dlr[dlr_p]; // 前序遍历的第一个点就是根结点
// 从中序遍历中寻找根结点的位置
int pos = -1;
for (int i = ldr_l; i <= ldr_r; i++)
{
if (ldr[i] == node->value)
{
pos = i;
break;
}
}
int lsize = pos - ldr_l; // 左子树宽度
int rsize = ldr_r - pos; // 右子树宽度
node->left = rebuild(dlr, dlr_p + 1, s, ldr, ldr_l, pos - 1);
node->right = rebuild(dlr, dlr_p + lsize + 1, s, ldr, pos + 1, ldr_r);
return node;
}
// 后续遍历
void lrd(Node* root)
{
if (root == NULL)return;
lrd(root->left);
lrd(root->right);
cout << root->value << endl;
}
int main()
{
const int size = 8;
int dlr[size] = {1, 2, 4, 7, 3, 5, 6, 8};
int ldr[size] = {4, 7, 2, 1, 5, 3, 8, 6};
Node* root = rebuild(dlr, 0, size, ldr, 0, size - 1);
lrd(root);
}
8. 二叉树的下一个节点
问题描述:给定一颗二叉树和其中一个结点,如何找出中序遍历的下一个节点?树的节点中,除了左右指针,还有一个指向父结点的指针。
解决方案:
1. 如果这个节点是父结点的左节点,则下一个节点就是父结点
2. 如果这个节点是父结点的右节点,且有右节点,则下一个结点就是它的右子树的最左节点
3. 如果这个节点是父结点的右节点,且无右节点,则下一个结点就以它的父结点为基准的下一个节点,回到问题1.2,直到找到这样一个节点,满足1或2
Node* findNext(Node* node)
{
if(node == NULL) return NULL;
Node* parent = node->parent;
if(parent->left == node) return parent;
else
{// 是右节点
Node* next = node->right ;
if(next)
{//,从右子树中找最左节点,一直遍历left,知道->left为空
while(next->left)
next = next->left;
}
else
{
next = findNext(node->parent);
}
return next;
}
}
{
if(node == NULL) return NULL;
Node* parent = node->parent;
if(parent->left == node) return parent;
else
{// 是右节点
Node* next = node->right ;
if(next)
{//,从右子树中找最左节点,一直遍历left,知道->left为空
while(next->left)
next = next->left;
}
else
{
next = findNext(node->parent);
}
return next;
}
}
9. 用两个栈实现一个队列、用两个队列实现一个栈
问题描述:这是一个典型的适配器模式,STL中的stack 和queue也是适配器,默认是由deque实现的。栈是一种只能在一端操作的数据结构,而且满足先进后出;队列是一种两端操作的结构,一端进,一端出(是不是很像动物
),满足先进先出。
解决方案:为了方便,我直接用STL中的stack实现一个queue,queue相当于一个适配器。要实现的接口有:
void push(v_type value) ;
void pop();
v_type front();
bool empty();
这三个,我们使用两个stack来保存状态,队列中的元素,要么在stack1中,要么在stack2中。stack1用来保存push的对尾数据,stack2用来保存pop的队首数据。push时,始终往stack1push,无论它是不是空的,pop时:
1. 如果stack2为空,则把stack1中的所有元素pop出来,依次放入stack2,使得它们在stack2中的顺序和在stack1中的恰好相反,这样,先入队的酒会在stack2的顶上,然后pop 出来就好了。
2. 如果stack2非空,则直接pop出stack2顶部那个元素,肯定是最早进入队列的元素。
#include <iostream>
#include <stack>
using namespace std;
// MyQueue适配器
class MyQueue
{
public:
MyQueue():s1(), s2()
{
}
stack<int> s1;
stack<int>s2;
void push(int n)
{
s1.push(n);
}
void pop()
{
if (!s2.empty())
{
s2.pop();
return;
}
while (!s1.empty())
{
int n = s1.top();
s2.push(n);
s1.pop();
}
s2.pop();
}
int front()
{
if (!s2.empty())
{
return s2.top();
}
while (!s1.empty())
{
int n = s1.top();
s2.push(n);
s1.pop();
}
return s2.top();
}
bool empty()
{
return s1.empty() && s2.empty();
}
};
int main()
{
MyQueue q;
q.push(1);
q.push(3);
q.push(2);
q.push(5);
q.push(4);
cout << q.front() << endl;
q.pop();
cout << q.front() << endl;
while (!q.empty())
{
cout << q.front();
q.pop();
}
}
#include <stack>
using namespace std;
// MyQueue适配器
class MyQueue
{
public:
MyQueue():s1(), s2()
{
}
stack<int> s1;
stack<int>s2;
void push(int n)
{
s1.push(n);
}
void pop()
{
if (!s2.empty())
{
s2.pop();
return;
}
while (!s1.empty())
{
int n = s1.top();
s2.push(n);
s1.pop();
}
s2.pop();
}
int front()
{
if (!s2.empty())
{
return s2.top();
}
while (!s1.empty())
{
int n = s1.top();
s2.push(n);
s1.pop();
}
return s2.top();
}
bool empty()
{
return s1.empty() && s2.empty();
}
};
int main()
{
MyQueue q;
q.push(1);
q.push(3);
q.push(2);
q.push(5);
q.push(4);
cout << q.front() << endl;
q.pop();
cout << q.front() << endl;
while (!q.empty())
{
cout << q.front();
q.pop();
}
}
还有一个问题就是用两个队列实现一个栈,也是一个适配器,要实现的操作也还是:
void push(v_type value) ;
void pop();
v_type top();
bool empty();
这四个,由于栈有后入先出的特点,只能在一端操作。用两个队列实现一个栈的中心思想是
每次只有一个操纵队列(每次只有一个队列非空,书72页),而且在每次执行pop和top的时候另一个队列成为操纵队列。
void push(v_type value) ;
void pop();
v_type front();
bool empty();
#include <iostream>
#include <queue>
using namespace std;
class MyStack
{
public:
MyStack() :q1(), q2()
{
}
// 每次只会有一个队列非空
queue<int> q1;
queue<int> q2;
void push(int n)
{
// 当前操纵队列
queue<int>* cur = &q1;
if (!q2.empty()) cur = &q2;
cur->push(n);
}
void pop()
{
if (empty()) return;
queue<int>* cur = &q1;
queue<int>* another = &q2;
if (!q2.empty())
{
cur = &q2;
another = &q1;
}
// 将前面的那些转移到另一个队列中
while (cur->size() > 1)
{
int n = cur->front();
another->push(n);
cur->pop();
}
if(!cur->empty())
cur->pop();
}
int top()
{
if (empty()) return 0;
queue<int>& cur = q1;
queue<int>& another = q2;
if (!q2.empty())
{
another = q1;
cur = q2;
}
// 将前面的那些转移到另一个队列中
int n ;
while (!cur.empty())
{
n = cur.front();
another.push(n);
cur.pop();
}
return n;
}
bool empty()
{
return q1.empty() && q2.empty();
}
};
int main()
{
MyStack s;
s.push(1);
s.push(2);
s.push(3);
s.push(4);
cout << s.top();
s.pop();
s.push(5);
while (!s.empty())
{
cout << s.top();
s.pop();
}
}
10. 斐波那契数列
f(n) = > (
0, n = 0
1, n = 1
f(n -1) +f(n - 2), n > 1
)
问题描述:输入n,求斐波那契数列的第n项。
解决方案:斐波那契数列的递归公式是 ,但是这样会有很多重复计算的问题(递归几乎都有)。可以通过一个备忘录来优化。或者直接从f(0)开始算起,逐步递推到f(n)
#include <iostream>
using namespace std;
int main()
{
for (;;)
{
int n;
cin >> n;
if (n == 1)
{
cout << 1 << endl;
continue;
}
int fibMinusOne = 1;
int fibMinusTow = 0;
int fibN = 0;
for (int i = 2; i <= n; i++)
{
fibN = fibMinusTow + fibMinusOne;
fibMinusTow = fibMinusOne;
fibMinusOne = fibN;
}
cout << fibN << endl;
}
return 0;
}
using namespace std;
int main()
{
for (;;)
{
int n;
cin >> n;
if (n == 1)
{
cout << 1 << endl;
continue;
}
int fibMinusOne = 1;
int fibMinusTow = 0;
int fibN = 0;
for (int i = 2; i <= n; i++)
{
fibN = fibMinusTow + fibMinusOne;
fibMinusTow = fibMinusOne;
fibMinusOne = fibN;
}
cout << fibN << endl;
}
return 0;
}
10.2 青蛙跳台阶问题
有只青蛙一次可以跳1级台阶,2级台阶,或3级台阶。问跳上n级台阶有多少种跳法。
问题分析:这是一个斐波那契数列的应用。f(0) = 0 , f(1) = 1, f(2) = 2, f(3) = 3, f(4) = f(3) + f(2) + f(1),对于n阶台阶,第一步可以走3阶,2阶或1阶。所以f(n) = f(n - 3) +f(n-2) + f(n-1)。可以用递归+备忘录优化,也可以递推。略过。
11. 旋转数组的最小数字
问题描述:把一个数组最开始的若干个元素搬到数组的末尾,称为数组的旋转。输入一个递增排序的数组的一个旋转,输出旋转数组的最小元素。例如,数组3,4,5,1,2为1,2,3,4,5的一个旋转,该旋转数组的最小值为1。
解决方案:采用二分法,充分利用旋转数组的特性,将旋转数组分为两部分。左子序列的第一个值一定大于等于右子序列的最后一个值。采用两个指针,一个从最左边开始,一个从最右边开始。每次检测 (l+r)/2位置上的值,将其与l和r上的比较,如果大于等于l上的值,一定在左子序列,最小值一定在此值右边;如果小于右等于r的值,一定在此值左边。如此就将问题缩小了一半。时间复杂度O(logn)。
#include <iostream>
using namespace std;
// l指针一定在左子序列
// r指针一定在右子序列
int find(int *n, int l, int r)
{
if (n[l] < n[r]) return n[l]; // 处理 自身有序这种情况,可以直接返回
if (r - l == 1) return n[r]; // 左右两指针相遇,只差一个,则最小值是右子序列的第一个数字
int mid = (l + r) / 2.f;
if (n[mid] >= n[l]) return find(n, mid, r);
if (n[mid] <= n[r]) return find(n, l, mid);
}
int main()
{
int n[] = {3, 4, 5, 1, 2};
cout << find(n, 0, sizeof(n)/ sizeof(int) - 1);
}
using namespace std;
// l指针一定在左子序列
// r指针一定在右子序列
int find(int *n, int l, int r)
{
if (n[l] < n[r]) return n[l]; // 处理 自身有序这种情况,可以直接返回
if (r - l == 1) return n[r]; // 左右两指针相遇,只差一个,则最小值是右子序列的第一个数字
int mid = (l + r) / 2.f;
if (n[mid] >= n[l]) return find(n, mid, r);
if (n[mid] <= n[r]) return find(n, l, mid);
}
int main()
{
int n[] = {3, 4, 5, 1, 2};
cout << find(n, 0, sizeof(n)/ sizeof(int) - 1);
}
12. 矩阵中的路径
问题描述:设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任何一格开始,每一步可以在矩阵中向左,右,上,下移动一格。如果一条路径经过了矩阵中的某一格,那么该路径不能再次进入该格子。例如,在下面的3x4矩阵中包含一条“bfce”的路径,但不包含"abfb"的路径,因为字符串的第一个字符b在占据了矩阵的第一行第二个格子之后,就不能再进入这个格子。
a b t g
c f c s
j d e h
解决方案:采用回溯法。详见书89页。很好理解,不过要注意的是对于“不能再进入这个格子”的处理,用一个同样维度的bool矩阵来标记。
#include <iostream>
using namespace std;
#define SIZE 4
// i, j 是当前点的位置
bool traceback(bool mask[][SIZE], char n[][SIZE], int r, int c, int i, int j, const char* s)
{
if (i < 0 || i >= r || j < 0 || j >= c)
return false;
if (mask[i][j]) return false; // 不能重复访问一个节点
if (n[i][j] == s[0])
{// 找下一部分
mask[i][j] = true; // 标记这个节点已经被访问过
if (strlen(s) == 1) return true; // 已经相等
if (traceback(mask, n, r, c, i, j - 1, s + 1) // 向左走
|| traceback(mask, n, r, c, i, j + 1, s + 1) // 向右走
|| traceback(mask, n, r, c, i - 1, j, s + 1) // 向上走
|| traceback(mask, n, r, c, i + 1, j, s + 1) // 向下走
)
return true;
else
{// 没有此路径
mask[i][j] = false;
return false;
}
}
else
return false;
}
bool find(bool mask[][SIZE], char n[][SIZE], int r, int c, const char* s)
{
for (int i = 0; i < r; i++)
{
for (int j = 0; j < c; j++)
{
if (n[i][j] == s[0])
if (traceback(mask, n, r, c, i, j, s))
return true;
}
}
return false;
}
int main()
{
char n[][SIZE] = {
{'a', 'b', 't', 'g'},
{'c', 'f', 'c', 's'},
{'j', 'd', 'e', 'h' },
};
bool mask[][SIZE] = {
{0},
{ 0 },
{ 0 },
};
const char* s = "bfce";
//const char* s2 = "abfb";
if (find(mask, n, 3, 4, s))
cout << "the string is found" << endl;
else
cout << "can't find string" << endl;
}
using namespace std;
#define SIZE 4
// i, j 是当前点的位置
bool traceback(bool mask[][SIZE], char n[][SIZE], int r, int c, int i, int j, const char* s)
{
if (i < 0 || i >= r || j < 0 || j >= c)
return false;
if (mask[i][j]) return false; // 不能重复访问一个节点
if (n[i][j] == s[0])
{// 找下一部分
mask[i][j] = true; // 标记这个节点已经被访问过
if (strlen(s) == 1) return true; // 已经相等
if (traceback(mask, n, r, c, i, j - 1, s + 1) // 向左走
|| traceback(mask, n, r, c, i, j + 1, s + 1) // 向右走
|| traceback(mask, n, r, c, i - 1, j, s + 1) // 向上走
|| traceback(mask, n, r, c, i + 1, j, s + 1) // 向下走
)
return true;
else
{// 没有此路径
mask[i][j] = false;
return false;
}
}
else
return false;
}
bool find(bool mask[][SIZE], char n[][SIZE], int r, int c, const char* s)
{
for (int i = 0; i < r; i++)
{
for (int j = 0; j < c; j++)
{
if (n[i][j] == s[0])
if (traceback(mask, n, r, c, i, j, s))
return true;
}
}
return false;
}
int main()
{
char n[][SIZE] = {
{'a', 'b', 't', 'g'},
{'c', 'f', 'c', 's'},
{'j', 'd', 'e', 'h' },
};
bool mask[][SIZE] = {
{0},
{ 0 },
{ 0 },
};
const char* s = "bfce";
//const char* s2 = "abfb";
if (find(mask, n, 3, 4, s))
cout << "the string is found" << endl;
else
cout << "can't find string" << endl;
}
13. 机器人的运动范围
问题描述:地上有m行n列的方格,一个机器人从坐标(0,0)的格子开始移动,每次可以向左、右、上、下移动一格,但不能进入行坐标和列坐标的位数之和大于k的格子。例如,当k为18时,机器人能够进入方格(35,37),因为3+5+3+7=18。但它不能进入方格(35, 38),因为3+5+3+8=19,请问机器人能够到达多少个格子?
解决方案:使用回溯法。
解决方案:
#include <iostream>
using namespace std;
int sum(int i, int j)
{
int res = 0;
while (i > 0)
{
res += i % 10;
i /= 10;
}
while (j > 0)
{
res += j % 10;
j /= 10;
}
return res;
}
void find(int m, int n,int i, int j, int k, int& count, bool* visited)
{
if (i < 0 || i >= m || j < 0 || j >= n) return;
if (visited[n * i + j]) return;
if (sum(i, j) > k) return ;
if (!visited[n * i + j])
{
visited[n * i + j] = true;
count += 1;
}
find(m, n, i - 1, j, k, count, visited); // 向上走
find(m, n, i + 1, j, k, count, visited); // 向下走
find(m, n, i, j - 1, k, count, visited); // 向左走
find(m, n, i, j + 1, k, count, visited); // 向右走
}
int main()
{
for (;;)
{
int m, n, k;
m = 5;
n = 5;
k = 3;
int count = 0;
using namespace std;
int sum(int i, int j)
{
int res = 0;
while (i > 0)
{
res += i % 10;
i /= 10;
}
while (j > 0)
{
res += j % 10;
j /= 10;
}
return res;
}
void find(int m, int n,int i, int j, int k, int& count, bool* visited)
{
if (i < 0 || i >= m || j < 0 || j >= n) return;
if (visited[n * i + j]) return;
if (sum(i, j) > k) return ;
if (!visited[n * i + j])
{
visited[n * i + j] = true;
count += 1;
}
find(m, n, i - 1, j, k, count, visited); // 向上走
find(m, n, i + 1, j, k, count, visited); // 向下走
find(m, n, i, j - 1, k, count, visited); // 向左走
find(m, n, i, j + 1, k, count, visited); // 向右走
}
int main()
{
for (;;)
{
int m, n, k;
m = 5;
n = 5;
k = 3;
int count = 0;
if(m <= 0 || n <= 0 || k < 0)
{
cout << count;
continue;
}
bool* visited = new bool[m *n];
for (int i = 0; i < m*n; i++)
visited[i] = 0;
find(m, n, 0, 0, k, count, visited);
cout << count;
}
}
bool* visited = new bool[m *n];
for (int i = 0; i < m*n; i++)
visited[i] = 0;
find(m, n, 0, 0, k, count, visited);
cout << count;
}
}
14. 剪绳子
问题描述:一根长度为n的绳子,把绳子剪成m段(m,n都是整数, n >1且m>1),每段绳子长度为k[0], k[1],... k[m],请问k[0]xk[1]x ... k[m]可能的最大乘积是多少?例如,当绳子长度是8时,我们把它剪成2,3,3,此时得到最大乘积18.注意,只有n是给定的,m是未给定的。
解决方法:用动态规划的方法。剪第一刀的时候有 n-1 种可能的选择,也就是剪出来的绳子的长度可能为1, 2, ... n-1。有递归公式 f(n) = max( f(i) * f(n - 1)), 0 < i < n 注意,第一剪剪下来的这一段也有自己的最优解,而不是简单的 i 。
#include <iostream>
using namespace std;
int main()
{
int n = 8;
int *product = new int[n + 1];
// 边界
product[0] = 0;
product[1] = 1;
product[2] = 2;
product[3] = 3;
int max = 0;
for (int i = 4; i <= n; i++)
{
max = 0;
for (int j = 0; j <= i/2; j++)
{
int p = product[j] * product[i - j];
if (p > max) max = p;
product[i] = max;
}
}
max = product[n];
delete[] product;
cout << max;
}
using namespace std;
int main()
{
int n = 8;
int *product = new int[n + 1];
// 边界
product[0] = 0;
product[1] = 1;
product[2] = 2;
product[3] = 3;
int max = 0;
for (int i = 4; i <= n; i++)
{
max = 0;
for (int j = 0; j <= i/2; j++)
{
int p = product[j] * product[i - j];
if (p > max) max = p;
product[i] = max;
}
}
max = product[n];
delete[] product;
cout << max;
}
另一种解法是贪婪算法,当n>=5时,尽可能多剪成长度为3的绳子;当剩下的绳子长度为4时,把绳子剪成两段长度为2的绳子。具体的证明和算法在97页。
15. 位运算
二进制的位运算有
与 &
或 |
异或 ^
左移 <<
右移 >>
其中左右移的优先级最低,计算的时候经常要加括号。位运算的优先级总体来说比较低(比比较运算符低)。
15.1 微软Excel中,用A表示第一列,B表示第二列,。。。Z表示第26列,AA表示第27列,AB表示第28列...要求写一个函数,输入一个列号编码,输出它是第几列。
解决方案:是一个进制转换问题,把编码从26进制转换成十进制即可
#include <iostream>
#include <string>
#include <math.h>
using namespace std;
int f(const string s)
{
int n = s.size();
int res = 0;
for (int i = 0; i < s.size(); i++)
{
char c = s[i];
int num = c - 'A' + 1;
res += num * pow((double) 26, (double)(--n));
}
return res;
}
int main()
{
for (;;)
{
string s;
cin >> s;
cout << f(s);
}
}
#include <string>
#include <math.h>
using namespace std;
int f(const string s)
{
int n = s.size();
int res = 0;
for (int i = 0; i < s.size(); i++)
{
char c = s[i];
int num = c - 'A' + 1;
res += num * pow((double) 26, (double)(--n));
}
return res;
}
int main()
{
for (;;)
{
string s;
cin >> s;
cout << f(s);
}
}
15.2 统计二进制数中的1的个数
解决方案:
15.2.1 会在输入数字是负数时造成死循环(最高位是1)。
#include <iostream>
using namespace std;
int countOne(int n)
{
int res = 0;
while (n > 0)
{
res += (n & 1);
n = n >> 1;
}
return res;
}
int main()
{
for (;;)
{
int n;
cin >> n;
cout << countOne(n) << endl;
}
}
using namespace std;
int countOne(int n)
{
int res = 0;
while (n > 0)
{
res += (n & 1);
n = n >> 1;
}
return res;
}
int main()
{
for (;;)
{
int n;
cin >> n;
cout << countOne(n) << endl;
}
}
15.2.2 为了避免死循环,可以不右移输入的数字n,可以把n与1做与运算,看最低位是不是1,然后把1左移一位得到2,在和n做与运算,就能判断n的次低位是不是1。。。反复左移1,每次都可以判断,无论是不是最高位的符号位。
#include <iostream>
using namespace std;
int countOne(int n)
{
int count = 0;
unsigned int flag = 1;
while (flag)
{
if ((n & flag) != 0) count++;
flag = flag << 1;
}
return count;
}
int main()
{
for (;;)
{
int n;
cin >> n;
cout << countOne(n) << endl;
}
}
using namespace std;
int countOne(int n)
{
int count = 0;
unsigned int flag = 1;
while (flag)
{
if ((n & flag) != 0) count++;
flag = flag << 1;
}
return count;
}
int main()
{
for (;;)
{
int n;
cin >> n;
cout << countOne(n) << endl;
}
}
15.2.3 通过“把一个整数-1之后在和原来的数按位与,结果等于把整数的二进制表示中最右边的1变成0”。比如1100, 减一之后是1011, 再和原先的数按位与是1000, 这样就成功的只剔除了原先的最后一个1,循环往复,直到数为0即可。具体看书102
#include <iostream>
using namespace std;
int countOne(int n)
{
int count = 0;
while (n)
{
++count;
n = (n - 1) & n;
}
return count;
}
int main()
{
for (;;)
{
int n;
cin >> n;
cout << countOne(n) << endl;
}
}
using namespace std;
int countOne(int n)
{
int count = 0;
while (n)
{
++count;
n = (n - 1) & n;
}
return count;
}
int main()
{
for (;;)
{
int n;
cin >> n;
cout << countOne(n) << endl;
}
}
16. 数值的整数次方
问题描述:实现double Power(double base, int exponent), 不得使用库函数,同时不需要考虑大数问题。
解决方案:需要考虑以下问题:
1. 指数是负数
2. 不能对0求负幂,因为不能求0的倒数,应该报告调用出错
3. 0的0次方在数学上无意义,输出0或者1都可以
#include <iostream>
bool g_InvalidInput = false; // 保存错误信息
double PowerWithUnsignedExponent(double base, unsigned int exponent) // 计算正数幂
{
double result = 1.0;
for (int i = 1; i <= exponent; ++i)
result *= base;
return result;
}
double Power(double base, int exponent)
{
g_InvalidInput = false;
if (base == 0.0 && exponent < 0)
{// 不能对0求负幂,因为不能对0求倒数
g_InvalidInput = true;
return 0.0;
}
unsigned int absExponent = (unsigned int)exponent;
if (exponent < 0)
absExponent = (unsigned int)(-exponent);
double result = PowerWithUnsignedExponent(base, exponent);
if (exponent < 0)
// 如果是负数幂,就取倒数
result = 1.0 / result;
return result;
}
int main()
{
}
bool g_InvalidInput = false; // 保存错误信息
double PowerWithUnsignedExponent(double base, unsigned int exponent) // 计算正数幂
{
double result = 1.0;
for (int i = 1; i <= exponent; ++i)
result *= base;
return result;
}
double Power(double base, int exponent)
{
g_InvalidInput = false;
if (base == 0.0 && exponent < 0)
{// 不能对0求负幂,因为不能对0求倒数
g_InvalidInput = true;
return 0.0;
}
unsigned int absExponent = (unsigned int)exponent;
if (exponent < 0)
absExponent = (unsigned int)(-exponent);
double result = PowerWithUnsignedExponent(base, exponent);
if (exponent < 0)
// 如果是负数幂,就取倒数
result = 1.0 / result;
return result;
}
int main()
{
}
优化的方案是用下面的数列:
a^n = a^(n/2) * a^(n/2) , n为偶数
a^((n-1)/2) * a^((n-1)/2) * a , n为奇数
17. 打印从1到最大的n位数
问题描述:输入数字n,按顺序打印出从1到最大的n为十进制数字,比如输入3 ,打印1,2,3,一直到最大的三位数999。
解决方案:由于没有说n有多大,如果很大的话,即便用Longlong 保存也会超范围。解决方法是在字符串上模拟数字加法,因为字符串是没有长度限制的。
#include <iostream>
using namespace std;
// 给n +1
bool Increment(char* n)
{
bool isOverflow = false;
int nTakeOver = 0; // 进位
int nLength = strlen(n);
for (int i = nLength - 1; i >= 0; i--)
{
int nSum = n[i] - '0' + nTakeOver; // 加上进位
if (i == nLength - 1)
nSum++;
if (nSum >= 10) // 需要进位
{
if (i == 0) // 如果已经到最高位了,即最后一个数字
isOverflow = true;
else
{
nSum -= 10;
nTakeOver = 1;
n[i] = '0' + nSum; //刷新
}
}
else
{
n[i] = '0' + nSum;
break;
}
}
return isOverflow;
}
// 输出数字,注意别输出补位的零
void PrintNumber(const char* n)
{
int len = strlen(n);
bool isBeginningZero = true;
for (int i = 0; i < len; i++)
{
if (n[i] == '0' && isBeginningZero)
{
continue;
}
isBeginningZero = false;
cout << n[i];
}
cout << '\n';
}
//
void PrintToMaxOfNDigits(int n)
{
if (n <= 0)
return;
char* number = new char[n + 1];
memset(number, '0', n);
number[n] = '\0';// 此时 字符串应该是 0000...000\0, n个0, 结尾一个\0
while (! Increment(number)) // 输出,直到位数已满
{
PrintNumber(number);
}
delete[] number;
}
int main()
{
for (;;)
{
int n;
cin >> n;
PrintToMaxOfNDigits(n);
}
}
using namespace std;
// 给n +1
bool Increment(char* n)
{
bool isOverflow = false;
int nTakeOver = 0; // 进位
int nLength = strlen(n);
for (int i = nLength - 1; i >= 0; i--)
{
int nSum = n[i] - '0' + nTakeOver; // 加上进位
if (i == nLength - 1)
nSum++;
if (nSum >= 10) // 需要进位
{
if (i == 0) // 如果已经到最高位了,即最后一个数字
isOverflow = true;
else
{
nSum -= 10;
nTakeOver = 1;
n[i] = '0' + nSum; //刷新
}
}
else
{
n[i] = '0' + nSum;
break;
}
}
return isOverflow;
}
// 输出数字,注意别输出补位的零
void PrintNumber(const char* n)
{
int len = strlen(n);
bool isBeginningZero = true;
for (int i = 0; i < len; i++)
{
if (n[i] == '0' && isBeginningZero)
{
continue;
}
isBeginningZero = false;
cout << n[i];
}
cout << '\n';
}
//
void PrintToMaxOfNDigits(int n)
{
if (n <= 0)
return;
char* number = new char[n + 1];
memset(number, '0', n);
number[n] = '\0';// 此时 字符串应该是 0000...000\0, n个0, 结尾一个\0
while (! Increment(number)) // 输出,直到位数已满
{
PrintNumber(number);
}
delete[] number;
}
int main()
{
for (;;)
{
int n;
cin >> n;
PrintToMaxOfNDigits(n);
}
}
这样的话还是比较复杂的,在短时间内要写出来几乎不可能,可以用递归来实现更简单的方法。如果我们在所有数字前面补0,就会发现,n位左右十进制数其实就是n个从0到9的全排列。也就是说,只要把每一位从0到9全排列一遍,就得到了所有十进制数字。在打印的时候,忽略前导0就好了。
#include <iostream>
using namespace std;
void PrintToMaxOfNDigits(int n)
{
if (n <= 0) return;
char* number = new char[n + 1];
number[n] = '\0';
for (int i = 0; i <= 9; i++)
{
number[0] = i + '0';
PrintToMaxOfNDigitsRecursively(number, n, 0);
}
}
// 输出数字,注意别输出补位的零
void PrintNumber(const char* n)
{
int len = strlen(n);
bool isBeginningZero = true;
for (int i = 0; i < len; i++)
{
if (n[i] == '0' && isBeginningZero)
{
continue;
}
isBeginningZero = false;
cout << n[i];
}
cout << '\n';
}
void PrintToMaxOfNDigitsRecursively(char* number, int length, int index)
{
if (index == length - 1)
{
PrintNumber(number);
}
for (int i = 0; i < 10; i++)
{
number[index + 1] = i + '0';
PrintToMaxOfNDigitsRecursively(number, length, index + 1);
}
}
int main()
{
for (;;)
{
int n;
cin >> n;
PrintToMaxOfNDigits(n);
}
}
using namespace std;
void PrintToMaxOfNDigits(int n)
{
if (n <= 0) return;
char* number = new char[n + 1];
number[n] = '\0';
for (int i = 0; i <= 9; i++)
{
number[0] = i + '0';
PrintToMaxOfNDigitsRecursively(number, n, 0);
}
}
// 输出数字,注意别输出补位的零
void PrintNumber(const char* n)
{
int len = strlen(n);
bool isBeginningZero = true;
for (int i = 0; i < len; i++)
{
if (n[i] == '0' && isBeginningZero)
{
continue;
}
isBeginningZero = false;
cout << n[i];
}
cout << '\n';
}
void PrintToMaxOfNDigitsRecursively(char* number, int length, int index)
{
if (index == length - 1)
{
PrintNumber(number);
}
for (int i = 0; i < 10; i++)
{
number[index + 1] = i + '0';
PrintToMaxOfNDigitsRecursively(number, length, index + 1);
}
}
int main()
{
for (;;)
{
int n;
cin >> n;
PrintToMaxOfNDigits(n);
}
}
18. 删除链表的结点
问题描述:在O(1)时间内删除单链表的某个结点。给定一个表头指针和一个结点指针,要求定义一个函数,在O(1)时间内删除该结点。链表的数据结构如下:
struct ListNode
{
int m_pData;
ListNode* m_pNext;
}
解决方法:通常我们要删除单链表中一个结点,要从表头遍历找到此节点的上一个结点,然后进行一系列指针转移操作,时间复杂度是O(n)。此题要求时间复杂度O(1),那只能从此结点指针着手了,因为访问这个节点的时间是O(1)。可以用结点的下一个结点覆盖此结点,然后删除下一个结点即可。因为结点结构很简单,这样是可行的。
void RemoveNode(ListNode** head, ListNode* node)
{
if(*head == NULL || node == NULL) return ;
// 如果要删除的不是尾结点
if(node->next != NULL)
{
ListNode* next= node->next;
node->m_pDate = next->m_pData;
node->next = next->next;
delete next;
next = NULL;
}
// 如果只有一个结点,删除这个节点
else if(*head == node)
{
delete node;
node = NULL;
*head= NULL:
}
else
{// 链表有多个节点,且删除尾结点,则尾结点的上一个节点的next应该是0,
// 这时候只能老老实实遍历了
ListNode* n = *head;
while(n->next != node)
{
n = n->next;
}
n->next = NULL;
delete node;
node = NULL;
}
}
{
if(*head == NULL || node == NULL) return ;
// 如果要删除的不是尾结点
if(node->next != NULL)
{
ListNode* next= node->next;
node->m_pDate = next->m_pData;
node->next = next->next;
delete next;
next = NULL;
}
// 如果只有一个结点,删除这个节点
else if(*head == node)
{
delete node;
node = NULL;
*head= NULL:
}
else
{// 链表有多个节点,且删除尾结点,则尾结点的上一个节点的next应该是0,
// 这时候只能老老实实遍历了
ListNode* n = *head;
while(n->next != node)
{
n = n->next;
}
n->next = NULL;
delete node;
node = NULL;
}
}
18.2 删除已排序链表中重复的结点。比较简单,说一下思路。如果当前结点和下一个结点的值相同,那么就是重复的。
19. 正则表达式匹配
问题描述:实现一个函数,用来匹配包含'.'和'*'的正则表达式。模式中的字符 . 代表任意一个字符,而 * 代表它前面的字符可以出现任意多次(含0次)。匹配指该字符串的所有字符都匹配整个模式。例如,字符串“aaa”与模式"a.a", "ab*ac*a"匹配,但与"aa.a" . "ab*a"均不匹配。
解决方案:
#include <iostream>
using namespace std;
bool matchCore(char* str, char* pattern)
{
// 递归终点
if (*str == '\0' && *pattern == '\0') // 当两者都走完时,匹配
return true;
if (*str != '\0' && * pattern == '\0') // 当模式已经走完时,字符串还没走完,不匹配。(题目要求字符串中的所有字符匹配整个模式);翻过来的情况在下面的代码中判断,即*str = '\0’返回false。
return false;
if (*(pattern + 1) == '*')
{// 如果模式的第二个字符是* 或者模式允许任意字符出现任意多次即".*"模式
if (*str == *pattern || *pattern == '.' && *str != '\0')
{// 如果str的第一个字符和pattern的第一个匹配,则str跳过一个,或者pattern跳过这个*匹配(即匹配0次),或者两者都跳过
return matchCore(str + 1, pattern)
|| matchCore(str, pattern + 2)
|| matchCore(str + 1, pattern + 2);
}
else
{ // 如果不匹配,则跳过这个* 匹配(因为*允许出现0次前面的字符)
return matchCore(str, pattern + 2);
}
}
if (*str == *pattern || (*pattern == '.' && *str != '\0'))
return matchCore(str + 1, pattern + 1);
}
bool match(char* str, char* pattern)
{
if (str == NULL || pattern == NULL) return false;
return matchCore(str, pattern);
}
using namespace std;
bool matchCore(char* str, char* pattern)
{
// 递归终点
if (*str == '\0' && *pattern == '\0') // 当两者都走完时,匹配
return true;
if (*str != '\0' && * pattern == '\0') // 当模式已经走完时,字符串还没走完,不匹配。(题目要求字符串中的所有字符匹配整个模式);翻过来的情况在下面的代码中判断,即*str = '\0’返回false。
return false;
if (*(pattern + 1) == '*')
{// 如果模式的第二个字符是* 或者模式允许任意字符出现任意多次即".*"模式
if (*str == *pattern || *pattern == '.' && *str != '\0')
{// 如果str的第一个字符和pattern的第一个匹配,则str跳过一个,或者pattern跳过这个*匹配(即匹配0次),或者两者都跳过
return matchCore(str + 1, pattern)
|| matchCore(str, pattern + 2)
|| matchCore(str + 1, pattern + 2);
}
else
{ // 如果不匹配,则跳过这个* 匹配(因为*允许出现0次前面的字符)
return matchCore(str, pattern + 2);
}
}
if (*str == *pattern || (*pattern == '.' && *str != '\0'))
return matchCore(str + 1, pattern + 1);
}
bool match(char* str, char* pattern)
{
if (str == NULL || pattern == NULL) return false;
return matchCore(str, pattern);
}
20. 表示数值的字符串
跳过
问题描述:实现一个函数,判断字符串是否表示数值(包括整数、小数)比如,字符串"+100","5e2", "-123", "3.1416"以及"-1E-16"都表示数值,但"12e", "1a3.14",“1.2.3”,"+-5","12e+5.4"都不是数值。
解决方案:表示数值的字符串遵循模式 A[.[B]][e|EC]或者.B[e|EC], 其中A为数值的整数部分,B为小数部分,C为指数部分,C只能是整数,不会是小数。
21. 调整数组顺序使得奇数位于偶数前面
问题描述:输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有奇数位于数组的前半部分,所有偶数位于数组的后半部分。
解决方案:利用快速排序一样的思想,一个左指针,一个右指针,左指针碰到偶数, 右指针碰到奇数,就交换两者,知道两个指针相遇。
#include <iostream>
using namespace std;
int main()
{
int n[] = {0, 1,2,3,4,5,6,7,8};
int size = sizeof(n) / sizeof(int);
int l = 0;
int r = size - 1;
while (l < r)
{
while ((n[r] & 1) == 0) --r; // 位运算符优先级低于比较运算符。位运算优先级一般很低,移位最低
while ((n[l] & 1) == 1) ++l;
if (l >= r) break;
n[r] ^= n[l];
n[l] ^= n[r];
n[r] ^= n[l];
l++;
r--;
}
for (int i = 0; i < size; i++)
cout << n[i] << " " << endl;
}
using namespace std;
int main()
{
int n[] = {0, 1,2,3,4,5,6,7,8};
int size = sizeof(n) / sizeof(int);
int l = 0;
int r = size - 1;
while (l < r)
{
while ((n[r] & 1) == 0) --r; // 位运算符优先级低于比较运算符。位运算优先级一般很低,移位最低
while ((n[l] & 1) == 1) ++l;
if (l >= r) break;
n[r] ^= n[l];
n[l] ^= n[r];
n[r] ^= n[l];
l++;
r--;
}
for (int i = 0; i < size; i++)
cout << n[i] << " " << endl;
}
22. 获得链表中倒数第k个节点
问题描述:输入一个链表,输出该链表的倒数第N个节点。从1开始计数,链尾是倒数第一个节点。
解决方案:两个指针,一个快指针,一个慢指针,快指针走n-1步,然后一起走,等块指针走到尾部,慢指针的位置就是倒数第n个结点
++++++------------------------------ 快指针先走N步,到了最后一个加号处
-----------------------------+++++++然后一起走,最后慢指针到了最左边那个加号处,两个指针走的构成镜像
证明:假设链长为L(未知),快指针走到正数第N个结点时(走了N-1步),剩余L-N个没走,需要走L-N步。此时两者一起走,等快指针到了链尾,慢指针走了L-N步,到了正数第L-N+1个,对应于倒数第N个(正数倒数的编号和是L+1)。
ListNode * RGetKthNode(ListNode * pHead, unsigned int k) // 函数名前面的R代表反向
{
if (k == 0 || pHead == NULL) // 这里k的计数是从1开始的,若k为0或链表为空返回NULL
return NULL;
ListNode * pAhead = pHead;
ListNode * pBehind = pHead;
while (k > 1 && pAhead != NULL) // 前面的指针先走到正向第k个结点, 走了k-1步
{
k--;
pAhead = pAhead->m_pNext;
}
if (k > 1 || pAhead == NULL) // 结点个数小于k,返回NULL
return NULL;
while (pAhead->m_pNext != NULL) // 前后两个指针一起向前走,直到前面的指针指向最后一个结点
{
pBehind = pBehind->m_pNext;
pAhead = pAhead->m_pNext;
}
return pBehind; // 后面的指针所指结点就是倒数第k个结点
}
{
if (k == 0 || pHead == NULL) // 这里k的计数是从1开始的,若k为0或链表为空返回NULL
return NULL;
ListNode * pAhead = pHead;
ListNode * pBehind = pHead;
while (k > 1 && pAhead != NULL) // 前面的指针先走到正向第k个结点, 走了k-1步
{
k--;
pAhead = pAhead->m_pNext;
}
if (k > 1 || pAhead == NULL) // 结点个数小于k,返回NULL
return NULL;
while (pAhead->m_pNext != NULL) // 前后两个指针一起向前走,直到前面的指针指向最后一个结点
{
pBehind = pBehind->m_pNext;
pAhead = pAhead->m_pNext;
}
return pBehind; // 后面的指针所指结点就是倒数第k个结点
}
23. 链表中环的入口结点
略过,见另一篇有关链表的博客
24. 反转链表
略过,同上
25. 合并两个已排序的链表
基本思路是每次从两个链表头部选小的(如果是递增链表的话),然后把next指针指向这个结点。
略过,同上
26. 树的子结构
问题描述:输入两个二叉树A和B,判断B是不是A的子结构。
解决方案:
1. 现在A中查找与B根结点一样的值,等价于树的遍历。
2. 以匹配的根为根,递归的判断结构是否一致。
分为两个函数,一个用来找到那个根,另一个以那个根为根,递归的判断。
详见书150页。
27. 翻转二叉树
略
* 28. 对称的二叉树
问题描述:实现一个函数,用来判断一个二叉树是不是对称的(关于根结点对称),即左右翻转之后,还是它。
解决方案:我们定义一种遍历方法,类前序遍历,只不过现在是先遍历右子树,再遍历左子树。用两个指针,一个往左走,按照前序遍历,先左子树,再右子树;一个往右走,先右子树,再左子树。
bool isSymmetraical(BinaryNode* pRoot)
{
return isSymmetraical(pRoot, pRoot);
}
bool isSymmetraical(BinaryNode* pRoot1, BinaryNode* pRoot2) // root1往左走,root2往右走
{
if(pRoot1 == NULL && pRoot2 == NULL)
return true;
if(pRoot1 == NULL || pRoot2 == NULL) // 有一个不为空
return false;
if(pRoot1->m_Value != pRoot2->m_Value)
return false;
return isSymmetraical(pRoot1->m_left, pRoot2->m_right)
&& isSymmetraical(pRoot1->m_right, pRoot2->m_left);
}
{
return isSymmetraical(pRoot, pRoot);
}
bool isSymmetraical(BinaryNode* pRoot1, BinaryNode* pRoot2) // root1往左走,root2往右走
{
if(pRoot1 == NULL && pRoot2 == NULL)
return true;
if(pRoot1 == NULL || pRoot2 == NULL) // 有一个不为空
return false;
if(pRoot1->m_Value != pRoot2->m_Value)
return false;
return isSymmetraical(pRoot1->m_left, pRoot2->m_right)
&& isSymmetraical(pRoot1->m_right, pRoot2->m_left);
}
29. 顺时针打印矩阵
问题描述:输入一个矩阵,按照从外向内以顺时针打印出每一个数字。如
1 2 3 4
5 6 7 8
9 10 11 12
13 14 15 16
依次打印出1,2,3,4,8,12,16,15,14,13,9,5,6,7,11,10
画图分析,可以把矩阵看成是好几圈构成的结构,每次打印外面的一圈,然后回到再里面一圈的左上角。
略
30. 包含min函数的栈
问题描述:定义栈的数据结构,在该类型中实现一个能得到栈最小值的min函数。在此栈中,调用min,push,pop的时间都是O(1)。
解决方案:用另一个辅助栈存贮最小值,不能用一个数字保存(因为如果那个最小值被弹出了,栈的最小是什么?)。
每次往栈中压入元素,检查此元素和辅助栈栈顶元素(最小值)的大小关系,如果比最小值小,则把此值压入辅助栈,否则(大于或等于),再压入一个最小值。
从栈中pop数据的时候,辅助栈也pop一个。这样就可以解决栈中有多个最小值的问题了。每次使用min获得最小值,一定是辅助栈栈顶元素。
31. 栈的压入、弹出序列
问题描述:定义两个序列,一个代表栈的入栈顺序,一个代表出栈顺序,判断出栈顺序是否能实现。假设压入的数字都不相等。
解决方案:
以弹出序列为基准,参考入栈顺序,入栈和出栈:
如果下一个弹出的数字刚好是栈顶数字,则直接弹出;
如果下一个弹出的数字不是栈顶数字,则把压栈序列中还没有入栈的序列压入,直到从中找到了下一个弹出数字,压入栈;
如果所有数字都压入栈了,还没有从中(入栈序列中)找到下一个出栈数字,则该弹出序列不可能是一个弹出序列。
32. 从上到下打印二叉树(二叉树层次遍历/广度优先遍历)
依靠队列实现,略。
33. 二叉搜索树的后序遍历序列
问题描述:输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历结果。假设数组内容无重复。
解决方案:二叉搜索树相比平凡二叉树的特点就是
1. 中序有序性,即中序遍历是有序的。
2. 给定了前序遍历结果或者后序遍历结果,就可以确定BST的结构,而平凡二叉树不行。
比如5, 7, 6, 9, 11, 10, 8这个后序遍历,8一定是BST的根,8左边那些是左子树和右子树,由于二叉树中左子树中所有值均<根<右子树中所有值,所以在这些树中,很容易发现5,7,6是左子树,9,11,10是右子树,这些数字一定是连着的。然后左右子树又是BST,重复递归,知道叶节点。
注意如何判断假,比如 7, 4, 6, 5这个序列,5是根,从左往右遍历这个序列,第一个数字7>5,所以7在右子树,且此树应该没有左子树,那么接下来的值应该都在右子树,即7,4,6在右子树,但我们发现右子树中有一个节点4小于5,这违背了BST的定义,所以,这个序列不是一个可行的BST后序遍历序列。
代码参考书上181.
34. 二叉树中和为某一值的路径
跳过
问题描述:输入一颗二叉树和一个整数,打印出二叉树节点值的和为输入整数的所有路径。必须到达叶节点。
35. 复杂链表的复制
跳过
36. 二叉搜索树转化为双向链表
看另一个博客
利用二叉搜索树的中序有序性。分类:
37. 序列化二叉树
写两个函数,分别实现二叉树的序列化和反序列化
解决方案:
1. 序列化为前序和中序结果,然后重建。缺点是不能有重复的节点
2. 采用前序遍历,因为前序遍历的第一个节点是根结点。遇到一个空指针,就保存为一个特殊字符比如$。尝试反序列化1,2,4,$,$,$,3,5,$,$,6,$,$时,1时根结点,第二个值2非$,说明1的左子结点是2(如果2是$的话,说明1的左子树为空)。
具体看书195
* 38. 字符串的排列
问题描述:输入一个字符串,打印该字符串的所有排列。
解决方案:不要去尝试排列组合,统计所有字符在排列。而是从现有字符串开始。交换两个字符就会获得一个新的字符串。
#include <cstdio>
void Permutation(char* pStr, char* pBegin);\
void Permutation(char* pStr)
{
if(pStr == nullptr)
return;
Permutation(pStr, pStr);
}
void Permutation(char* pStr, char* pBegin)
{
if(*pBegin == '\0')
{
printf("%s\n", pStr);
}
else
{
for(char* pCh = pBegin; *pCh != '\0'; ++ pCh)
{
char temp = *pCh;
*pCh = *pBegin;
*pBegin = temp;
Permutation(pStr, pBegin + 1); // 处理后面的
temp = *pCh;
*pCh = *pBegin;
*pBegin = temp;
}
}
}
// ====================测试代码====================
void Test(char* pStr)
{
if(pStr == nullptr)
printf("Test for nullptr begins:\n");
else
printf("Test for %s begins:\n", pStr);
Permutation(pStr);
printf("\n");
}
int main(int argc, char* argv[])
{
Test(nullptr);
char string1[] = "";
Test(string1);
char string2[] = "a";
Test(string2);
char string3[] = "ab";
Test(string3);
char string4[] = "abc";
Test(string4);
return 0;
}
void Permutation(char* pStr, char* pBegin);\
void Permutation(char* pStr)
{
if(pStr == nullptr)
return;
Permutation(pStr, pStr);
}
void Permutation(char* pStr, char* pBegin)
{
if(*pBegin == '\0')
{
printf("%s\n", pStr);
}
else
{
for(char* pCh = pBegin; *pCh != '\0'; ++ pCh)
{
char temp = *pCh;
*pCh = *pBegin;
*pBegin = temp;
Permutation(pStr, pBegin + 1); // 处理后面的
temp = *pCh;
*pCh = *pBegin;
*pBegin = temp;
}
}
}
// ====================测试代码====================
void Test(char* pStr)
{
if(pStr == nullptr)
printf("Test for nullptr begins:\n");
else
printf("Test for %s begins:\n", pStr);
Permutation(pStr);
printf("\n");
}
int main(int argc, char* argv[])
{
Test(nullptr);
char string1[] = "";
Test(string1);
char string2[] = "a";
Test(string2);
char string3[] = "ab";
Test(string3);
char string4[] = "abc";
Test(string4);
return 0;
}
* 39. 数组中出现次数超过一半的数字
问题描述:数组中有一个数字的出现次数超过了数组长度的一半,找出这个数字。
解决方案:O(n)根据数组的特点。一个次数出现次数超过数组长度的一半,说明它出现的次数比其它数字出现的次数的和还要多。
要注意的是,题目已经保证了数组中必定有一个数字出现次数超过数组的一半。下面的算法只在这种情况下有效,
用两个值,一个保存数字,一个保存次数。当我们遍历到下一个数字的时候,如果下一个数字和我们之前保存的数字相同,则次数加1; 如果下一个数字和我们之前保存的数字不同,则次数减一,此时如果次数为0,则保存下一个数字,并把次数设为1。由于我们要找的数字出现的次数很多,比其他数字出现次数加起来还要多,那么要找的那个数字,一定会活到最后(最后一次把次数设为1的那个)。
* 40. 最小的k个数(TOP K问题)
问题描述:输入n个整数,找出其中最小的k个数。例如,输入4,5,1,6,2,7,3,8,这8个数字,则最小的4个数字是1,2,3,4
解决方案:
1. 最简单的就是先拍好,然后选出前k个,复杂度是O(nlogn)
2. 基于partition(快排)的算法, 复杂度为O(n)
在使用快排过程中,记下来每次排序选择的
Quicksort相关原理就不多说了,STL中Quicksort算法的实现我在<stl sort源码剖析>中一文也详细说明过。所以这里就只说说怎么样用Quicksort来求topk。
其实很简单,大家都知道Quicksort每次分割后即把比轴小的值和比轴大的值给cut开了。那么,假如是想求前k小(以下源码即是),我就只关心比轴小的一边,继续分割这一边,直到如果分割后的元素没有k个了,就结束循环。我们用源码说话:
其实很简单,大家都知道Quicksort每次分割后即把比轴小的值和比轴大的值给cut开了。那么,假如是想求前k小(以下源码即是),我就只关心比轴小的一边,继续分割这一边,直到如果分割后的元素没有k个了,就结束循环。我们用源码说话:
TopK问题即求序列中最大或最小的K个数。这里以求最小K个数为例。
快速排序的思想是使用一个基准元素将数组划分成两部分,左侧都比基准数小,右侧都比基准数大。
给定数组array[low…high],一趟快排划分后的结果有三种:
1)如果基准数左侧元素个数Q刚好是K-1,那么在基准数左侧(包含基准数本身),即为TopK的所有元素。
2)如果基准数左侧元素个数Q小于K-1,那么说明基准数左侧的Q个数都是TopK里的元素,只需要在基准数的右侧找出剩下的K-Q个元素即可。问题转化成了以基准数下标为起点,高位(high)为终点的Top(K-Q)。递归下去即可。
3)如果基准数左侧元素个数Q大于K-1,说明第K个位置,在基准数的左侧,需要缩小搜索范围,在低位(low)至基准数位置重复递归即可,最终问题会转化成上面两种情况。
快速排序的思想是使用一个基准元素将数组划分成两部分,左侧都比基准数小,右侧都比基准数大。
给定数组array[low…high],一趟快排划分后的结果有三种:
1)如果基准数左侧元素个数Q刚好是K-1,那么在基准数左侧(包含基准数本身),即为TopK的所有元素。
2)如果基准数左侧元素个数Q小于K-1,那么说明基准数左侧的Q个数都是TopK里的元素,只需要在基准数的右侧找出剩下的K-Q个元素即可。问题转化成了以基准数下标为起点,高位(high)为终点的Top(K-Q)。递归下去即可。
3)如果基准数左侧元素个数Q大于K-1,说明第K个位置,在基准数的左侧,需要缩小搜索范围,在低位(low)至基准数位置重复递归即可,最终问题会转化成上面两种情况。