1. 判断计数/偶数写法:
相关题目:《剑指 Offer 21. 调整数组顺序使奇数位于偶数前面》
https://leetcode-cn.com/problems/diao-zheng-shu-zu-shun-xu-shi-qi-shu-wei-yu-ou-shu-qian-mian-lcof/
常规写法: 取模(对2取模)
if( i % 2 != 0 ) //奇数
if( i% 2 == 0 ) //偶数
优化写法: 位运算(用1与运算)
if( i & 1 == 1 )
if( i & 1 == 0 )
位运算的效率高于取模运算。
2. 移位操作“>>”:
相应题目:《剑指 Offer 15. 二进制中1的个数》
https://leetcode-cn.com/problems/er-jin-zhi-zhong-1de-ge-shu-lcof/solution/
2.1 关于移位运算符“>>”的一个低级错误:
想要对一个无符号整数要进行移位操作,
正确的写法是:
n = n >> 1;
或者:
n >>= 1;
错误的写法:
n >> 1;
这样的写法就相当于是一个 表达式 直接放在了这里,并没有更新n的值:
n + 1;
2.2 形参的值也是可以修改的:
void func(int n) {
cout << n; //n = 5; 打印的是传入的实参的值
n = 6;
cout << n; //n = 6; 形参的值被修改,打印的是修改后的值
}
int main() {
func(5);
return 0;
}
总结:
遇到一些求解“二进制”相关的问题(比如求某个整数中的 0 / 1 比特的个数),首先想到的方法就是 移位操作。
3. 经典问题:反转单向链表:
相关题目:《剑指 Offer 24. 反转链表》
https://leetcode-cn.com/problems/fan-zhuan-lian-biao-lcof/
方法一:递归法:
class Solution {
public:
ListNode* reverseList(ListNode* head) {
if(head == nullptr || head->next == nullptr) return head;
ListNode* newHead = reverseList(head->next); //newHead只有在最后一层递归才会赋值,并一路返回,成为最终结果
head->next->next = head;
head->next = nullptr; //这一步是为了防止表成环
return newHead;
}
};
方法二:迭代法:
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode* cur = head;
ListNode* prev = nullptr;
while(cur) {
ListNode* next = cur->next;
cur->next = prev;
prev = cur;
cur = next;
}
return prev;
}
};
4. unordered_map的count与find方法:
相关题目:《剑指 Offer 39. 数组中出现次数超过一半的数字》
https://leetcode-cn.com/problems/shu-zu-zhong-chu-xian-ci-shu-chao-guo-yi-ban-de-shu-zi-lcof/
计算map中对应某个key的元素个数,使用count方法。
map类型中就两个方法比价特殊,与其他的STL类不同:
一个是 map.find(key);, 一个是 map.count(key);。
map.find(key);
map.count(key);
另外,在算法题中,不要求元素有序的地方就用 unordered_map 代替 map。
5. 快速排序:
快速排序的基本思想是:
通过一趟排序将数组分割成独立的两部分,其中一部分的元素的值均比另一部分的元素的值小;然后再对这两个细分的区间进行排序,以达到整个数组有序。
快速排序是工程代码中使用的最多的,使用的是 分治 思想。
快速排序在这几个 O(N*log(N)) 的排序算法中也是效率最高的。
class Solution {
public:
void quickSort(vector<int>& nums, int begin, int end) {
if(begin >= end) return; //terminator
int pivot = partition(nums, begin, end); //process current layer
quickSort(nums, begin, pivot - 1); //drill down
quickSort(nums, pivot + 1, end);
}
int partition(vector<int>& nums, int begin, int end) {
int counter = begin;
int pivot = end;
for(int i = begin; i < end; ++i) {
if(nums[i] < nums[pivot]) {
swap(nums[i], nums[counter++]); //快慢指针 //counter的目标是counter以前都是比nums[pivot]小的元素
}
}
swap(nums[counter], nums[pivot]);
return counter;
}
};
6. 归并排序:
归并和快排具有相似性,但步骤顺序相反。
归并:先排序左右子序列,然后合并两个有序子数组;
快排:先调配出左右子数组,然后对于左右子数组记性排序(分治)。
class Solution {
public:
void mergeSort(vector<int>& nums, int begin, int end) {
if(begin >= end) return ; //terminator
int mid = begin + (end - begin) / 2;
mergeSort(nums, begin, mid); //drill down
mergeSort(nums, mid + 1, end);
mergeArray(nums, begin, mid, end);
}
void mergeArray(vector<int>& nums, int begin, int mid, int end) {
vector<int> temp;
temp.resize(end - begin + 1);
int i = begin, m = mid; //第一部分
int j = mid + 1, n = end; //第二部分
int k = 0;
while(i <= m && j <= n) {
temp[k++] = nums[i] < nums[j] ? nums[i++] : nums[j++];
}
while(i <= m) {
temp[k++] = nums[i++];
}
while(j <= n) {
temp[k++] = nums[j++];
}
for(int i = 0; i < k; ++i) {
nums[begin + i] = temp[i];
}
}
};
7. 冒泡排序怎么写:
void bubbleSort(vector<int>& nums) {
for(int i = 0; i < nums.size(); i++) {
for(int j = i + 1; j < nums.size(); j++) {
if(nums[j] < nums[i]) {
swap(nums[i], nums[j]);
}
}
}
}
8. 使用迭代器直接初始化vector时是尾后迭代器:
相关题目:《剑指 Offer 40. 最小的k个数》
https://leetcode-cn.com/problems/zui-xiao-de-kge-shu-lcof/
STL中的vector容器类型 支持使用另一个容器的范围迭代器的方式直接初始化,例如下面的例子:
int main() {
vector<int> array;
vector<int> ret(arr.begin(), arr.begin() + k);
}
需要注意的是: 在直接初始化的操作中,迭代器同样是 “尾后迭代器”,即初始化的真正范围是 从 ( arr.begin()) 到 (arr.begin() + k - 1)
,不包括 第k个元素。
同理,使用数组进行直接初始化时也是相同的原理:
int main() {
int array[] = {1,2,3,4};
vector<int> vec(array, array + sizeof(array)/sizeof(int));
}
9. 二叉树的遍历:
相关题目:剑指 Offer 32 - II. 从上到下打印二叉树 II
https://leetcode-cn.com/problems/cong-shang-dao-xia-da-yin-er-cha-shu-ii-lcof/
为什么要遍历二叉树?
就像是遍历数组、遍历链表一样,唯一的目的就是数据结构中 查找 某个元素。
遍历数组、链表都很简单,使用指针和迭代器从前向后遍历整个数组/链表即可,这是因为它们是 “一维” 的存储结构,或者形象的说是线性的数据结构。
而二叉树(N叉树)的结构不是线性的,因此就不可能像数组和链表那样的方式进行遍历、查找。
对于某一组数据的搜索,除非这个数据结构支持特定的查找操作(例如unordered_map的查找根据哈希公式找到对一个位置,时间复杂度是O(1)),否则就要使用 遍历 的方式进行搜索。
对于树这种数据结构的遍历,有这样几种方式:
前序遍历、中序遍历、后序遍历,这些都是 “深度优先遍历”;
层序遍历,这个是 “广度优先遍历”。
9.1 深度优先遍历 和 广度优先遍历:
根据对树中节点的访问顺序的不同,将树的遍历分为“深度优先遍历”和“广度优先遍历”,或者说叫 “深度优先搜索”(DFS,Depth First Search)和“广度优先搜索”(BFS,Breadth First Search)。
深度优先遍历的写法:“栈(迭代法)” 或 “递归法”;
广度优先遍历的写法:“队列(迭代法)”。
9.1.1 深度优先遍历的写法模板:
方法一:迭代法:(根左右)
深度优先遍历的迭代法中使用了 “回溯” 的思想。
所谓 “回溯”,就是在遍历的时候,就给自己留好退路(回溯点),当这条路走到尽头,可以直接跳转到回溯点。
在深度优先遍历中每个入栈的right右节点就是回溯点。
树节点的定义:
typedef struct TreeNode {
int val;
TreeNode* left;
TreeNode* right;
TreeNode(int n) : val(n) {}
TreeNode(int n, TreeNode* l, TreeNode* r) : val(n), left(l), right(r) {}
} TreeNode;
一个“根左右”顺序的DFS遍历:(如果需要“左右根”或者“左根右”的遍历顺序,调整一下推入vector的顺序即可)
vector<int> binaryTreeDfs(TreeNode* root) {
vector<int> result; //保存结果
stack<TreeNode*> s;
s.push(root);
while(!s.empty()) {
TreeNode* cur = s.top();
s.pop();
result.push_back(cur->val);
if(cur->right)
s.push(cur->right); //回溯点
if(cur->left)
s.push(cur->left);
}
return result;
}
方法二:递归法:
(根左右)
vector<int> result;
void binaryTreeDfs(TreeNode* root) {
travelTree(root);
return;
}
void travelTree(TreeNode* root) {
if(!root) return;
result.push_back(root->val);
travelTree(root->left);
travelTree(root->right);
}
上面的递归例子中,result.push_back(val) 放在两个递归函数的中间就是“左根右”,放在两个递归函数的后面就是“左右根”。
9.1.2 广度优先遍历的写法模板:
一个广度优先遍历的代码模板:
关键点在于使用一个 队列。
vector<int> binaryTreeBfs(TreeNode* root) {
vector<int> result;
queue<TreeNode*> q;
q.push(root);
while(!q.empty()) {
TreeNode* cur = q.front();
q.pop();
result.push_back(cur->val);
if(cur->left)
q.push(cur->left);
if(cur->right)
q.push(cur->right);
}
return result;
}
9.1.3 广度优先遍历的按层打印:
广度优先遍历的一个变种是“按层打印”,这种问题需要再额外的借助一个for循环,每次按照 queue.size()
进行循环处理一层。
有一点需要注意的是 for中的循环条件 queue.size 需要另外保存在一个局部变量中,因为在for循环体内会修改循环条件。
class Solution {
public:
vector<vector<int>> levelOrder(TreeNode* root) {
vector<vector<int>> result;
if(root == nullptr) return result;
queue<TreeNode*> q;
q.push(root);
while(!q.empty()) {
result.push_back(vector<int>()); //先推一个空的vector进去
int curQueueSize = q.size(); //切记:在for【循环体内】如果有改变【循环条件】的操作,那就应该在进入for循环前使用一个临时变量将这个循环条件保存下来。这与“迭代器失效”的原理是一样的
for(int i = 0; i < curQueueSize; ++i) {
TreeNode* cur = q.front();
q.pop();
result.back().push_back(cur->val);
if(cur->left)
q.push(cur->left);
if(cur->right)
q.push(cur->right);
}
}
return result;
}
};