第三章 高质量代码
1.代码的完整性
剑指 Offer 16. 数值的整数次方
实现 pow(x, n) ,即计算 x 的 n 次幂函数(即,xn)。不得使用库函数,同时不需要考虑大数问题。
输入:x = 2.00000, n = 10
输出:1024.00000
输入:x = 2.00000, n = -2
输出:0.25000
解释:2-2 = 1/22 = 1/4 = 0.25
- 初始解法:x*x重复n-1次。但这种情况下没有考虑n为负数和零。
- 完整解法:应该对负数进行-操作,对于零应该抛出异常。但这种方法需要在循环中n-1次乘法。
- 优化:考虑利用递归可以求出x的n次方,即先求出xn/2,而xn/2可以由xn/4求出。即如果n是奇数,相当于多乘一次本身,而偶数可以直接递归平方求出。
- **利用右移运算代替/2运算,用位与运算代替求余运算判断奇偶性。**位运算的效率比乘除以及求余效率要高。
考验思维的全面性
double myPow(double x, int n) {
if(x == 1|| n == 0) return 1;
if(n == 1) return x;
long exp = (long)n;
if(exp > 0){
double res = myPow(x, exp>>1);
res *= res;
if(exp&1) res *= x;
return res;
}
if(n < 0){
double res = myPow(x, (-exp)>>1);
res *= res;
if(exp&1) res *= x;
return 1/res;
}
return 0;
}
剑指 Offer 17. 打印从1到最大的n位数
输入数字 n
,按顺序打印出从 1 到最大的 n 位十进制数。比如输入 3,则打印出 1、2、3 一直到最大的 3 位数 999。
输入: n = 1
输出: [1,2,3,4,5,6,7,8,9]
方法一:用字符串模拟数字加法。
首先要考虑到当n很大的时候(比如100),打印出来的数很有可能是超过了INT_MAX的范围的,所以我们用字符串来表示每个数。
当然,在这一题中,由于返回的是一个 int 型的数组,所以是不可能超过INT_MAX的,但是一般大数问题都不会要求返回 int 数组来保存每一位数,而是循环输出每一位数。
我们的思路就是,假设 n = 3,就定义一个字符串,初始化为 “000”,然后用它来循环模拟从1到最大的n位数,并循环保存到 int 数组中(在真实情况下则是循环输出)。
意识到是大数问题。
利用在加1时第一个字符产生了进位,则已经是最大的n位。
按照阅读习惯,字符串“00980”需要输出为980而不是00980。
vector<int> output;
vector<int> printNumbers(int n) {
if(n <= 0) return vector<int>(0);
string s(n,'0');//用字符串来表示大数,例如n=3
while( !increment(s) ) outputNumbers(s);//increment实现字符串上+1,如果返回true,表示已经到达最大值999
return output;
}
bool increment(string & s){//模拟数字的累加过程,并判断是否越界(即 999 + 1 = 1000,就是越界情况
bool isOverFlow = false;//默认情况下没有到达最大值
int carry = 0;//carry表示进位
for(int i = s.size()-1; i >= 0; --i){
int cur = s[i] - '0' + carry;//cur表示当前这次操作
if(i == s.size()-1) ++cur;//如果此时i在个位,cur进行+1,相当于对字符串s中表示的数字进行累加过程
if(cur >= 10){//如果当前操作cur大于10,表示可能要进行进位
if(i == 0) isOverFlow = true;//假如已经在最大位,而current++之后>=10,说明循环到头了,即999 + 1 = 1000
else{//只是普通进位
carry = 1;
s[i] = cur - 10 + '0';//去除进位10以后,转换为字符的形式
}
}else{//如果没有进位,更新s[i]的值,然后跳出循环,表示本次累加结束,去将本次累加结果存到输出,等待下次累加操作
s[i] = cur + '0';
break;
}
}
return isOverFlow;
}
void outputNumbers(string s){
// 本函数用于循环往output中添加符合传统阅读习惯的元素。比如001,我们会添加1而不是001。
bool isFirtNonZero = false;// 判断是否经过第一个非‘0’,比如0010前面的两个0
string temp = "";
for(int i=0; i<s.length(); ++i){
if(!isFirtNonZero && s[i] != '0') isFirtNonZero = true;
if(isFirtNonZero) temp += s[i];//如果已经经过第一个非零字符,后面的‘0’需要保留
}
output.push_back(stoi(temp));
}
方法二:递归全排列解法。
假设 n = 3,要输出的数其实就是三位数的全排列(000,001,002,…,999,当然 000 不能输出),我们用递归来表示出这个过程即可。注意000,以及前缀0不能打印出来。
没想到stoi这个函数这么强大,
stoi(str);
如果 s = “000”,则temp会是空,那么不进行输出;而且stoi会自动把“009”这类字符串转化成数字9,自动把前缀0去掉。
不过在实际面试中,大概率最后要求输出的是string,就不能使用stoi函数了,那么最后还是需要借助方法1的outputNumbers函数,去掉前缀0,只不过为了防止输出“000”,需要把37行修改成
if(temp != "") output.push_back(stoi(temp));
vector<int> printNumbers(int n) {
if(n <= 0) return vector<int>(0);
vector<int> output;
string s(n,'0');
allArrange(s, output, 0);
return output;
}
void allArrange(string &s, vector<int> &output, int pos){
if(pos == s.size()){//递归停止条件是已经修改了字符串的最后一位
int temp = stoi(s);//如果 s = "000",则temp会是空,那么不进行输出;而且stoi会自动把“009”这类字符串转化成数字9,自动把前缀0去掉
if(temp) output.push_back(temp);
return;
}
for(int i = 0; i < 10; ++i){
s[pos] = i + '0';
allArrange(s, output, pos + 1);
}
}
剑指 Offer 18. 删除链表的节点
给定单向链表的头指针和一个要删除的节点的值,定义一个函数删除该节点。返回删除后的链表的头节点。
输入: head = [4,5,1,9], val = 5
输出: [4,1,9]
解释: 给定你链表中值为 5 的第二个节点,那么在调用了你的函数之后,该链表应变为 4 -> 1 -> 9.
从头结点开始顺序查找,通过检查head->next->val == val
来找到待删除结点的前一个结点,head ->next = head->next->next
。
剑指offer和此题有区别:剑指上面直接给出了待删的结点ListNode,所以它和它的下一个节点可以直接找到,不需要从头遍历,直接把该节点的下一个节点值复制到待删结点,再把下一个结点删除,达到了删除这个结点的效果。但如果next为空是尾节点,就需要从头遍历,把待删结点的前一个结点next指向空。
ListNode* deleteNode(ListNode* head, int val) {
if(!head) return nullptr;
if(head->val == val) return head->next;//如果要删除的点是头结,直接令head指向next,这样如果链表只有一个节点,则为空,如果不是一个节点,则相当于把头结点删掉。
ListNode* dummy = head;//dummy保存头结点
while(head->next){//这样找到的head是待删除结点的前一个结点
if(head->next->val == val){
head->next = head->next->next;
break;
}else{
head = head->next;
}
}
return dummy;
}
剑指 Offer 20. 表示数值的字符串
请实现一个函数用来判断字符串是否表示数值(包括整数和小数)。
数值(按顺序)可以分成以下几个部分:
- 若干空格
- 一个 小数 或者 整数
- (可选)一个 ‘e’ 或 ‘E’ ,后面跟着一个 整数
- 若干空格
小数(按顺序)可以分成以下几个部分:
- (可选)一个符号字符(’+’ 或 ‘-’)
- 下述格式之一:
- 至少一位数字,后面跟着一个点 ‘.’
- 至少一位数字,后面跟着一个点 ‘.’ ,后面再跟着至少一位数字
- 一个点 ‘.’ ,后面跟着至少一位数字
整数(按顺序)可以分成以下几个部分:
- (可选)一个符号字符(’+’ 或 ‘-’)
- 至少一位数字
//状态机
bool isNumber(string s) {
if(s.empty()) return false;
int n = s.size();
int index = -1;
bool hasDot = false,hasE = false, hasOp = false,hasNum = false;
while(index < n && s[++index] == ' ');//把字符串前面的空格去掉
while(index < n){
if('0' <= s[index] && s[index] <= '9') hasNum = true;
else if(s[index] == 'E' || s[index] == 'e'){
if(hasE || !hasNum) return false;//如果已经有E了,或者前面没有数字,则直接返回false
hasE = true;
hasOp = false; hasDot = false; hasNum = false;//后面就要重置,因为e后面这些都可以跟
}else if(s[index] == '+' || s[index] == '-'){
if(hasOp || hasNum || hasDot) return false;//加号前面不能有加减号、数字(否则就是表达式)和小数点
hasOp = true;
}else if(s[index] == '.'){
if(hasDot || hasE) return false;
hasDot = true;
}else if(s[index] == ' ') break;//有空格直接退出
else return false;
++index;
}
while(index < n && s[++index] == ' ');//把后面得空格去掉
return hasNum && (index == n);
}
剑指 Offer 21. 调整数组顺序使奇数位于偶数前面
输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有奇数位于数组的前半部分,所有偶数位于数组的后半部分。
输入:nums = [1,2,3,4]
输出:[1,3,2,4]
注:[3,1,2,4] 也是正确的答案之一。
vector<int> exchange(vector<int>& nums) {
int n = nums.size();
if(n <= 1) return nums;
int left = 0,right = n-1;
while(left < right){
while( left < right && (nums[left]&1) == 1){//如果left是奇数,就一直右移,直到碰到偶数
++left;
}
while(left < right && (nums[right]&1) == 0){//如果right是偶数,就一直左移,直到碰到奇数
--right;
}
if(left < right) swap(nums[left],nums[right]);//如果能换,就交换奇偶数的位置
}
return nums;
}
这道题如果扩展成按照数组大小,能否被3整除等条件,分为前后两部分,为了使代码具有可扩展性。把整个函数解耦成两个部分:一是判断数字应该在数组前半部分还是后半部分的标准;二是拆分数组的操作。
2.代码的鲁棒性
剑指 Offer 22. 链表中倒数第k个节点
输入一个链表,输出该链表中倒数第k个节点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点。
例如,一个链表有 6 个节点,从头节点开始,它们的值依次是 1、2、3、4、5、6。这个链表的倒数第 3 个节点是值为 4 的节点。
给定一个链表: 1->2->3->4->5, 和 k = 2.
返回链表 4->5.
应注意代码鲁棒性:如果k的大小超过链表的长度,则返回空。
ListNode* getKthFromEnd(ListNode* head, int k) {
if(!head || k == 0) return nullptr;
ListNode * slow = head,* fast = head;
while(fast && k){
--k;
fast = fast->next;
}
if(k) return nullptr;//如果k的大小超过链表的长度,则返回空
while(fast){
fast = fast->next;
slow = slow->next;
}
return slow;
}
剑指 Offer 24. 反转链表
定义一个函数,输入一个链表的头节点,反转该链表并输出反转后链表的头节点。
输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL
/*非递归
ListNode* reverseList(ListNode* head) {
ListNode *pre = nullptr,*cur = nullptr;
while(head){
cur = head->next;//用cur保存修改当前节点之前的指向
head->next = pre;//将当前head节点指向上一个节点
pre = head;
head = cur;
}
return pre;
}*/
//递归
ListNode* reverseList(ListNode* head,ListNode *pre = nullptr) {
if(!head) return pre;
ListNode* cur = head->next;
head->next = pre;
return reverseList(cur,head);
}
剑指 Offer 25. 合并两个排序的链表
输入两个递增排序的链表,合并这两个链表并使新链表中的节点仍然是递增排序的。
输入:1->2->4, 1->3->4
输出:1->1->2->3->4->4
/*原地合并,不开辟新节点
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
if(!l1) return l2;
if(!l2) return l1;
if(l1->val <= l2->val){
l1->next = mergeTwoLists(l1->next,l2);
return l1;
}else{
l2->next = mergeTwoLists(l2->next,l1);
return l2;
}
}*/
//开辟新节点
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2){
if(!l1) return l2;
if(!l2) return l1;
ListNode* mergeHead = nullptr;
if(l1->val <= l2->val){
mergeHead = l1;
mergeHead->next = mergeTwoLists(l1->next,l2);
}else{
mergeHead = l2;
mergeHead->next = mergeTwoLists(l2->next,l1);
}
return mergeHead;
}
剑指 Offer 26. 树的子结构
输入两棵二叉树A和B,判断B是不是A的子结构。(约定空树不是任意一个树的子结构)。B是A的子结构, 即 A中有出现和B相同的结构和节点值。
例如:给定的树 A:
3
/ \
4 5
/ \
1 2
给定的树 B:
4
/
1
返回 true,因为 B 与 A 的一个子树拥有相同的结构和节点值。
与二叉树相关的代码,在每次使用指针的时候,都首先问自己这个指针有没有可能是nullptr,如果是nullptr是怎么处理。
bool isSubStructure(TreeNode* A, TreeNode* B) {//递归在A中寻找 与B根节点 相同的节点
if(!A || !B) return false;//
bool flag = false;
if(A->val == B->val) flag = isStructure(A, B);
if(flag) return true;
else{
return isSubStructure(A->left,B) || isSubStructure(A->right,B);
}
}
bool isStructure(TreeNode* A, TreeNode* B){//判断A和B树的结构是否完全一样
if(!B) return true;//如果B已经遍历结束,直接返回true
if(!A || A->val != B->val) return false;
return isStructure(A->left,B->left) && isStructure(A->right,B->right);//如果当前节点相同,就去看他们的左右子节点是否相同
}