剑指offer相关代码整理2-4
第二部分——面试需要的基础知识
01 赋值运算符函数
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
/*01赋值运算符函数 */ class CMyString { public: CMyString(const char* pData = NULL); CMyString(const CMyString& str); CMyString& operator=(const CMyString& cms); ~CMyString(); private: char* m_pData; }; /*要格外注意一下条件判断*/ CMyString::CMyString(const char* pData) { if (pData == NULL) { m_pData = new char[1]; *m_pData = '\0'; } else { m_pData = new char[strlen(pData) + 1]; strcpy(m_pData, pData); } } CMyString::CMyString(const CMyString& str) { m_pData = new char[strlen(str.m_pData) + 1]; strcpy(m_pData, str.m_pData); } /*第一个版本——普通要求*/ CMyString& CMyString::operator=(const CMyString& cms) { if (this == &cms) return *this; delete[] m_pData; m_pData = NULL; m_pData = new char[strlen(cms.m_pData) + 1]; strcpy(m_pData, cms.m_pData); return *this; } CMyString::~CMyString() { delete[] m_pData; } /* /*第二个版本——考虑异常安全性的解法 关键在于用一个临时对象拷贝源对象内容,再交换目标和临时对象的成员变量值,以保证万一中间出错实例不会被修改 CMyString& CMyString::operator=(const CMyString& cms) { if (this != &cms) { CMyString tmp(cms); char* tmpChar = tmp.m_pData; tmp.m_pData = m_pData; m_pData = tmpChar; } return *this; } */
02 实现Singleton模式
实现单例模式有以下共同点:
1) 有一个私有的无参构造函数,防止其他类实例化它。而且单例类也不应该被继承。
2) 用一个静态的变量来保存单实例的引用。
3) 用一个共有的静态方法来获取单实例的引用。如果实例为null则立即创建一个。
(用c++实现实际上要麻烦一些?)
使用单例模式的类禁止拷贝复制,可以将拷贝构造函数和=操作符重载私有化来实现禁止拷贝。因此可以构造一个不可拷贝的类来让单例类去继承。boost库里已经包含了一个实现boost::noncopyable
只要继承下面的UnCopyable,那么该类就无法进行拷贝了。主要思想是把构造函数和析构函数设置为protected,这样子类可以调用,但是外面的类不能调用。另外关键在于noncopyable把拷贝构造函数和赋值函数设置为private,外面的调用者不能通过赋值、拷贝等手段来产生一个新的子类对象。boost::nocopyable是现成的可以用来继承的库类。
这里的UnCopyble写错掉啦!!!改不了了~~~~
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
/*02实现single模式 单例模式:保证一个类仅有一个实例,并提供一个访问它的全局访问点 */ class UnCopyable { public: protected: //允许derived对象构造和析构 UnCopyable(){} ~UnCopyable(){} private: //阻止拷贝 UnCopyable& operator=(const UnCopyable& uc){} UnCopyable(const UnCopyable&){} }; /*线程不安全的单例模式 实现方法是定义一个单例类,使用类的私有静态指针变量指向类的唯一实例,并用一个共有的静态方法获取该实例*/ class Singleton_unsafe:UnCopyable { private: static Singleton_unsafe* p; public: static Singleton_unsafe* instance() { if (p == NULL) p = new Singleton_unsafe(); return p; } }; Singleton_unsafe* Singleton_unsafe::p = NULL; /*线程安全的单例模式——饿汉方法 在单例类定义时就实例化,非常简单!*/ class Singleton_hungry:UnCopyable { private: static Singleton_hungry* p; public: static Singleton_hungry* instance() { return p; } }; Singleton_hungry* Singleton_hungry::p = new Singleton_hungry();
java中单例模式的线程安全有两种方法——第一种是通过使用静态的局部变量来产生实例;第二种是通过线程互斥的方法在多线程中产生单例,也就是在getInstance的方法前加上synchronized关键字
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
/*线程安全的单例模式——懒汉方法 不到万不得已不去实例化类,第一次用到类实例时才去进行实例化 第一种,利用内部静态变量的方法 在instance函数里定义一个静态的实例,也可以保证拥有唯一的实例,返回时只要返回其指针即可*/ class Singleton_lazy1:UnCopyable { public: static Singleton_lazy1* instance() { lock(); //在支持c++0x的编译器中可以不需要,因为可以保证静态变量的线程安全 static Singleton_lazy1 obj; unlock(); return &obj; } }; /*第二种,利用加锁的方式 也就是在之前线程不安全的情况下加锁以保证线程安全*/ class Singleton_lazy2 :UnCopyable { private: static Singleton_lazy2* p; public: static Singleton_lazy2* instance() { if (p != NULL) { lock(); if (p != NULL) p = new Singleton_lazy2(); unlock(); } return p; } }; Singleton_lazy2* Singleton_lazy2::p = NULL;
03 旋转数组的最小数字
其实挺麻烦的。。。关键在于条件判断的整齐全面
int minNumberInRotateArray(vector<int> rArray) { if (rArray.size() == 1) return rArray[0]; int begin = 0, end = rArray.size() - 1; while (begin < end) { if (rArray[begin] < rArray[end]) return rArray[begin]; if (end - begin == 1) return rArray[end]; int mid = begin + (end - begin) / 2; if (rArray[begin] == rArray[mid] && rArray[mid] == rArray[end]) { int res = rArray[begin]; for (int i = begin + 1;i <= end;i++) { if (res < rArray[i]) res = rArray[i]; } return res; } if (rArray[begin] <= rArray[mid]) { begin = mid; } else if (rArray[end] >= rArray[mid]) end = mid; } return rArray[begin]; }
小知识点:
int a[2][3];
int sz = sizeof(a); //4*2*3=24;
另外,在c/c++中,当数组作为函数的参数进行传递时,数组就自动退化为同类型的指针。
数组、字符串、链表;栈与递归、队列与图和树。
二叉树的非递归遍历方法。
查找和排序,时间复杂度分析。
位运算。
第三部分——高质量的代码
注意代码的容错能力——参数检查、处理错误和异常。
注意命名方式。
从三个方面测试代码的完整性:功能测试、边界测试、负面测试(也就是非法输入)。
三种错误处理的方法:返回值传递、发生错误时设置一个全局变量、异常处理。
11数值的整数次方
/*11数值的整数次方 */ double epsilon = 0.000001; bool equaldouble(double num1, double num2) { // 比较两个浮点数是否相等 if (num1 - num2 > epsilon || num2 - num1 > epsilon) return false; return true; } bool g_InvalidInput = false; // 用于指示输出是否合法 double funcForPow(double base, int exponent) { if (exponent == 0) return 1.0; if (exponent == 1) return base; double tmp = funcForPow(base, exponent / 2); // 可以用右移运算符代替除以2 return tmp*tmp*(exponent % 2 == 1 ? base : 1); } double Power(double base, int exponent) { bool minus = false, flag = false; if (equaldouble(base, 0.0)) { if (exponent > 0) { return 0.0; } else { g_InvalidInput = true; return 0.0; } } // 用位与运算代替求余运算符更好! // exponent&0x01==1 表示奇数 if (base < 0 && exponent % 2 == 1) minus = true; base = abs(base); if (exponent < 0) flag = true; exponent = abs(exponent); double res = funcForPow(base, exponent); if (minus) res = -res; if (flag) res = 1.0 / res; return res; }
12打印最大的1到最大的n位数
(一开始题目都读错了。。。如果是打印从1-n的数,显然还是用第一种方法方便。不过现在的要求是打印1-n位的数,因此第二种方法更加简洁)
有两种方法:
第一种是模拟大数的加法运算;
第二种是利用全排列。
/*第一种方法:模拟大数加法*/ string add(const string str1, const string str2) { string res; int ind1 = str1.size() - 1, ind2 = str2.size() - 1; bool flag = false; for (;ind1 >= 0 && ind2 >= 0;ind1--, ind2--) { int tmp = str1[ind1] - '0' + str2[ind2] - '0' + (flag ? 1 : 0); if (tmp >= 10) flag = true; else flag = false; res = static_cast<char>(tmp % 10 + '0') + res; } while (ind1 >= 0) { int tmp = str1[ind1--] - '0' + (flag ? 1 : 0); if (tmp >= 10) flag = true; else flag = false; res = static_cast<char>(tmp % 10 + '0') + res; } while (ind2 >= 0) { int tmp = str2[ind2--] - '0' + (flag ? 1 : 0); if (tmp >= 10) flag = true; else flag = false; res = static_cast<char>(tmp % 10 + '0') + res; } if (flag) { res = static_cast<char>(1 + '0') + res; } return res; } void print1_n(int n) { string str = "0"; string strone = "1"; int number = 0; n--; while (n) { number = number * 10 + 9; n--; } for (int i = 1; i <= number; i++) { string stmp = add(str, strone); cout << stmp << endl; str = stmp; } }
第二种方法,利用全排列
n位所有十进制数实际上就是n个从0到9的全排列。也就是说,我们可以把数字的每一位都从0到9排列一遍。
代码的关键处在于:可以用一个printNumber函数来打印所有n位的数,注意剔除所有最前面的0;另外全排列也是一种很关键的遍历手段。
/*第二种方法:利用全排列*/ void printNumber(string str) { int i = 0; while (i < str.size() && str[i] == '0') i++; while (i < str.size()) { cout << str[i++]; } cout << endl; } void funcPrint1_n(const string& num, string& str, int ind, int numsz) { if (ind == numsz) { printNumber(str); return; } for (int i = 0; i < num.size(); i++) { str[ind] = num[i]; funcPrint1_n(num, str, ind + 1, numsz); } } void print1_n2(int n) { string num; for (int i = 0; i <= 9; i++) { num.push_back(i + '0'); } string str(n, '0'); funcPrint1_n(num, str, 0, n); } int main() { print1_n2(2); return 0; }
13在O(1)时间删除链表结点
利用删除下一个节点的方法来处理。但是要注意了,还要考虑删除的节点位于链表的尾部,以及输入的链表只有一个节点这些特殊情况。
15链表中倒数第k个结点——注意判断k>链表中结点数的情形
拓展题:
求链表的中间结点:利用走一步指针和走两步的快慢指针来解决;
判断一个单向链表是否形成了环形结构:也是利用快慢指针解决。
第四部分 解决面试题的思路
21 包含min函数的栈
问题描述:定义栈的数据结构,请在该类型中实现一个能够得到栈最小元素的min函数
该问题的关键在于:保存一个变量存储当前最小值不可行,因为每次弹出一个数之后,该数有可能是当前最小数。这时,在该数据结构中已经没有办法找到当前的最小数了。
在这里实现的方法是另外设置一个额外的存储当前最小值的栈,注意,该栈的大小与数据存储栈的大小是一致的。
另外,稍微注意一下模板类的写法。
struct TreeNode { int val; struct TreeNode* left, *right; TreeNode(int x) :val(x), left(NULL), right(NULL) { } }; template<typename T> class stackWithMin { private: stack<T> m_data; stack<T> m_min; public: void push(T); void pop(); T top(); T min(); }; template<typename T> void stackWithMin<T>::push(T data) { m_data.push(data); if (m_min.size() == 0 || data < m_min.top()) { m_min.push(data); } else m_min.push(m_min.top()); } template<typename T> void stackWithMin<T>::pop() { assert(m_data.size() > 0 && m_min.size() > 0); //注意条件判断 m_data.pop(); m_min.pop(); } template<typename T> T stackWithMin<T>::top() { assert(m_data.size() > 0 && m_min.size() > 0); return m_data.top(); } template<typename T> T stackWithMin<T>::min() { assert(m_data.size() > 0 && m_min.size() > 0); return m_min.top(); }
22 栈的压入、弹出序列
注意利用stl中的stack来模拟这个过程。思路也并不是很清晰。
bool IsPopOrder(vector<int> pushV, vector<int> popV) { if (pushV.size() == 0 || (pushV.size() != popV.size())) return false; stack<int> stdata; int ind = 0; for (int i = 0; i < pushV.size(); i++) { stdata.push(pushV[i]); //关键步骤,其实是要注意这样来模拟的方式思路会很清晰 while (!stdata.empty() && stdata.top() == popV[ind]) { stdata.pop(); ind++; } } if (!stdata.empty() || ind != popV.size()) return false; return true; }
23 从上往下打印二叉树
24二叉搜索树的后序遍历序列(判断一个序列是否是二叉搜索树的后序遍历序列)
25 二叉树中和为某一值的路径
【条件判断方面错了无数次。。。】
void funcFP(TreeNode* root, int expectNumber, int sum, vector<int>& vec, vector<vector<int>>& res) { if (root == NULL) { return; } vec.push_back(root->val); sum += root->val; if (root->left == NULL&&root->right == NULL&&expectNumber==sum) { res.push_back(vec); } if(root->left) funcFP(root->left, expectNumber, sum, vec, res); if(root->right) funcFP(root->right, expectNumber, sum, vec, res); sum -= root->val; vec.pop_back(); } vector<vector<int> > FindPath(TreeNode* root, int expectNumber) { vector<int> vec; vector<vector<int>> res; if (root == NULL) return res; funcFP(root, expectNumber, 0, vec, res); return res; }
26 复杂链表的复制
一种我喜欢的简单的方法——直接用哈希表,写起来非常清晰简单。
另外有一种不用辅助空间的思想——根据原始链表的每个结点N创建对应的N’。将N’链接在N后面。设置复制出来的结点。将长链表拆分为两个链表。
27 二叉搜索树和双向链表
这题的关键一方面理清思路,注意实现二者的转换关键在于记录下前面中序遍历已经遍历到的链表的头结点和尾结点;另一方面是记录头结点和尾结点的方式。下面代码中的记录方式很方便,通过设置成全局变量,并且在主调用函数中要对这两个全部变量进行赋初值。
另外当然也可以通过参数的方法来实现,但是脑子不好使,总会混乱。因为需要修改head和pNode的指针值,作为参数调用时是指针的指针型的参数。
TreeNode* pNode, *head; void funcCV(TreeNode* root) { if (root == NULL) return; funcCV(root->left); root->left = pNode; if (pNode == NULL) { head = root; } else { pNode->right = root; } pNode = root; funcCV(root->right); } TreeNode* Convert(TreeNode* pRootOfTree) { pNode = NULL; head = NULL; if (pRootOfTree == NULL) return NULL; funcCV(pRootOfTree); return head; }
28 字符串的排列
第一种方法:我常常用的方法,用一个record来记录当前位置的是否已经被使用。非常平民简单的算法。但是要稍微注意,根据题目的不同,有时应当用set容器而不是vector容器来记录需要存储的最终的值。
该方法实际上有普适性。
/*28 字符串的排列 */ /*第一种我常用的记录法*/ void funcPMTT(const string str, string& tmpres, set<string>& res, vector<int>& record) { if (tmpres.size() == str.size()) { res.insert(tmpres); return; } for (int i = 0; i < str.size(); i++) { if (record[i] == 0) { tmpres.push_back(str[i]); record[i] = 1; funcPMTT(str, tmpres, res, record); record[i] = 0; tmpres.pop_back(); } } } vector<string> Permutation(string str) { string tmpres; vector<string> res; set<string> tmpSet; if (str.size() == 0) return res; vector<int> record(str.size(), 0); funcPMTT(str, tmpres, tmpSet, record); for (auto it = tmpSet.begin(); it != tmpSet.end();it++) { res.push_back(*it); } return res; }
第二种方法:swap方法。针对这个问题非常好使,思路也很清晰。
swap方法用的是一种思想——把一个字符串看成两个部分,第一个部分为它的第一个字符,第二个部分为后面的字符。遍寻所有可能出现在第一个位置的字符,然后固定第一个字符,求所有后面的字符的全排列。
void funcPMTT2(string& str, int ind, set<string>& res) { if (ind == str.size()) { res.insert(str); } for (int i = ind; i < str.size();i++) { swap(str[ind], str[i]); funcPMTT2(str, ind + 1, res); swap(str[ind], str[i]); } } vector<string> Permutation(string str) { set<string> tmpres; vector<string> res; if (str.size() == 0) return res; funcPMTT2(str, 0, tmpres); for (auto it = tmpres.begin(); it != tmpres.end();it++) { res.push_back(*it); } return res; }
拓展问题:
- 输入一个含有8个数字的数组,判断有没有可能把这八个数字分别放到正方体的八个定点上,使得正方体上三组相对的面上的四个顶点的和都相等。
解决思路:转化为排列的问题——相当于先得到a1-a8这八个数字的所有排列,然后判断有没有某一个的排列符合题目中给定的条件。
- 八皇后问题。