LeetCode刷题笔记_20210926

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;
    }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值