2022.11.26认识复杂度和排序算法
P3.复杂度和简单排序算法
2022.11.26
时间复杂度 空间复杂度
选择排序
-
时间复杂度O(n^2),空间复杂度O(1)
-
寻找[i, n)区间里的最小值,并放在i位置上,i++
-
for (int i = 0; i < n; i++) { // 寻找[i, n)区间里的最小值,并放在i位置上 int minIndex = i; for (int j = i + 1; j < n; j++) if (arr[j] < arr[minIndex]) minIndex = j; swap(arr[i], arr[minIndex]); }
冒泡排序
-
时间复杂度O(n^2),空间复杂度O(1)
-
每次比较相邻两个数的的大小,将大的放到右边,这样每一轮都将找到待排序数据中的最大值,并放到排好序的位置
-
void bubblesort(int *a,int len) //形参a取到实参a传递过来的数组首地址 //然后解引用,取到数组的值 { for (int i=0;i<len-1;i++) //i控制排序的轮数 { for (int j=0;j<len-i-1;j++) //j控制每轮需要比较的次数 { if(a[j+1]<a[j]) //不满足正序要求,交换顺序 { int temp=a[j]; a[j]=a[j+1]; a[j+1]=temp; } } }
插入排序
- 时间复杂度O(n^2),空间复杂度O(1)
- 从i = 1开始,比较与前一个,若比前一个小则交换,知道不能换为止,但算法复杂度按照算法最差情况估计
void insertionSort(int &arr){
if(arr == null || arr.size() < 2) return arr;
for(int i = 1; i < arr.size(); i++){ // i从1开始比较
for (int j = i - 1; j >= 0 && arr[j] > arr[j+1]; j--){
swap(arr[j], arr[j+1]);
}
}
}
异或运算拓展
- 法则:相同为0,不同为1
- 还可以理解为无进位相加
- 0 ^ N = N; N ^ N = 0
- 满足交换律和结合律(利用无进制相加来解释:看某位的值看该为1的个数,与顺序无关)
- 一批数的异或结果唯一
- 不用额外变量交换两个值
swap(int a, int b){ //利用异或交换a,b的值,即
a = a ^ b;
b = a ^ b;
a = a ^ b;
}
上述代码有效条件:a和b值可以一样,但a和b地址不同!不然就为0
异或面试题
题目:int [ ], 1)一种数出现奇数次,其余均为偶数次; 2)两种数出现奇数次,其余均为偶数次
找到奇数次数字,时间复杂度O(n), 空间复杂度O(1)
1)int eor = 0; 令eor异或数组内所有值,则最终eor = a;即为奇数次数字
2)int eor = 0; 令eor异或数组内所有值,则最终eor = a^b != 0; 则eor在某位上一定不为0,表示a和b在该位上不同;eor’ = 0 去异或该位上不为0的数,则可得到a或b之一,则另一数为eor ^ eor’
int eor = 0;
for (int i = 0; i < arr.size(); i++){
eor ^= arr[i];
}
// eor = a ^ b;
// eor != 0;
// eor 中必有一位为1
int rightOne = eor & (~ eor + 1); //***提取eor中最右侧的1!!!***
int onlyOne = 0;
for (int cur : arr){
if ((cur & rightOne) != 0){
onlyOne ^= cur; // 异或该位上为1的数
} //onlyOne 为a和b right One位上位1的数字
}
cout << onlyOne << " " << (eor ^ onlyOne);
二分法详解与扩展
在有序数组中找某个数是否存在
- 时间复杂度为O(log2N)->O(logN)
在有序数组中,找>=某个数最左侧的的位置
- 也可以二分,但需要记录
局部最小值问题
- 若0位置和N-1位置不是局部最小,则0至N-1处必存在局部最小(类似费马定理)
-
对数器的概念和使用(验证OG)
//产生非确定性随机数(多次次运行时每次产生的随机数不一样)
#include <iostream>
#include <random>
int main()
{
std::random_device e;
std::uniform_real_distribution<double> u(0, 1); //随机数分布对象
for (size_t i = 0; i < 10; ++i) //生成范围为0-1的随机浮点数序列
std::cout << u(e) << " ";
std::cout << std::endl;
return 0;
}
#include "stdafx.h"
#include "iostream"
#include "ctime"
#include "cstdlib"
using namespace std;
#define N 999 //精度为小数点后面3位
int main()
{
float num;
int i;
float random[10];
srand(time(NULL));//设置随机数种子,使每次产生的随机序列不同
for (int i = 0; i < 10; i++)
{
random[i] = rand() % (N + 1) / (float)(N + 1);
}
for (int i = 0; i < 10; i++)
{
cout << random[i] << endl; //输出产生的10个随机数
}
return 0;
}
P4.认识O(NlogN)的排序
2022.11.26 27
递归行为和递归行为复杂度
引子:求mid mid = L + (R - L) / 2 == L + (R - L) >> 1 //右移一位相当于除二,更快
递归逻辑图:利用压栈、出栈、树
master公式
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DotlXk5d-1671605563885)(C:\Users\MSI-NB\Desktop\左程云\图片\master.PNG)]
归并排序
时间复杂度
- O(N*logN) //T(N) = 2 * T(N / 2) + O(N) //相对于O(N^2) 排序并没有浪费比较行为,比较行为变成有序东西接下来传递
分离函数
void mergesort(int x,int y) //分离,x 和 y 分别代表要分离数列的开头和结尾
{
if (x==y) return; //如果开头 = 结尾,那么就说明数列分完了,就要返回
int mid=X + (Y - X) >> 1; //将中间数求出来,用中间数把数列分成两段
mergesort(x,mid);
mergesort(mid+1,y); //递归,继续分离
merge(x,mid,y); //分离玩之后就合并
}
合并函数
void merge(int low,int mid,int high) //归并
//low 和 mid 分别是要合并的第一个数列的开头和结尾,mid+1 和 high 分别是第二个数列的开头和结尾
{
int i=low,j=mid+1,k=low;
//i、j 分别标记第一和第二个数列的当前位置,k 是标记当前要放到整体的哪一个位置
while (i<=mid && j<=high) //如果两个数列的数都没放完,循环
{
if (a[i]<a[j])
b[k++]=a[i++];
else
b[k++]=a[j++]; //将a[i] 和 a[j] 中小的那个放入 b[k],然后将相应的标记变量增加
} // b[k++]=a[i++] 和 b[k++]=a[j++] 是先赋值,再增加
while (i<=mid)
b[k++]=a[i++];
while (j<=high)
b[k++]=a[j++]; //当有一个数列放完了,就将另一个数列剩下的数按顺序放好
for (int i=low;i<=high;i++)
a[i]=b[i]; //将 b 数组里的东西放入 a 数组,因为 b 数组还可能要继续使用
}
void mergeSort(vector<int>& arr, int l, int h) {
if (l == h) {
return;
}
int mid = l + ((h - l) >> 1) ;
mergeSort(arr, l, mid);
mergeSort(arr, mid + 1, h);
merge(arr, l, mid, h);
}
void merge(vector<int>& arr, int l, int mid, int h) {
vector<int> help(h - l + 1);
int low = l;
int high = mid + 1;
int index = 0;
while (low <= mid && high <= h) {
help[index++] = arr[low] < arr[high] ? arr[low++] : arr[high++];
}
while (low <= mid) {
help[index++] = arr[low++];
}
while (high <= h) {
help[index++] = arr[high++];
}
for (int i = 0; i < help.size(); i++) {
arr[l + i] = help[i];
}
}
归并排序拓展题目
小和问题
在一个数组中,每一个数左边比当前数小的数累加起来,叫做这个数组的小和。求一个数组 的小和。
例子:[1,3,4,2,5] 1左边比1小的数,没有; 3左边比3小的数,1; 4左边比4小的数,1、3; 2左边比2小的数,1; 5左边比5小的数,1、3、4、2; 所以小和为1+1+3+1+1+3+4+2=16
- 思路:利用归并排序。问题可以转换为查找一个数右边比他大的数出现的次数,则先二分数组,利用并规排序,改变merge函数。
#include <iostream>
#include <vector>
using namespace std;
class Solution{
private:
int merge(vector<int>&arr, int L, int mid, int R) {
int p1 = L;
int p2 = mid + 1;
vector<int> help(R - L + 1);
int res = 0;
int i = 0; //i为help数组下标
while (p1 <= mid && p2 <= R) {
res += arr[p1] < arr[p2] ? arr[p1] * (R - p2 + 1) : 0;
help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
}
while (p1 <= mid) {
help[i++] = arr[p1++];
}
while (p2 <= R) {
help[i++] = arr[p2++];
}
for (int i = 0; i < help.size(); i++) {
arr[L + i] = help[i];
}
return res;
}
int mergeSort(vector<int>&arr, int L, int R) {
if (L == R) {
return 0;
}
int mid = L + ((R - L) >> 1);
return mergeSort(arr, L, mid) + mergeSort(arr, mid + 1, R) + merge(arr, L, mid, R);
}
public:
int smallSum(vector<int>&arr) {
if (arr.size() < 2) {
return 0;
}
return mergeSort(arr, 0, arr.size()-1);
}
};
int main()
{
vector<int> a = {1,3,4,2,5};
class Solution s;
cout << s.smallSum(a);
}
逆序对
在一个数组中,左边的数如果比右边的数大,则这两个数构成一个逆序对,请打印所有逆序 对
小和问题是右边的数比左边的数大,逆序对是左边的数比右边的数大,本质是一个问题。
为了保证merge时不比较单块数组内,因此数组内要降序排列,先放p2的
#include <iostream>
#include <vector>
using namespace std;
class Solution {
private:
void merge(vector<int>& nums, int l, int mid, int r) {
int p1 = l;
int p2 = mid + 1;
vector<int> help(r - l + 1);
int i = 0;
while (p1 <= mid && p2 <= r) {
if (nums[p1] > nums[p2]) {
int p3 = p2;
while (p3 <= r) {
cout << nums[p1] << " " << nums[p3++] << endl;
}
}
help[i++] = nums[p1] > nums[p2] ? nums[p1++] : nums[p2++];
}
while (p1 <= mid) {
help[i++] = nums[p1++];
}
while (p2 <= r) {
help[i++] = nums[p2++];
}
for (int i = 0; i < help.size(); i++) {
nums[l + i] = help[i];
}
}
void mergeSort(vector<int>& nums, int l, int r) {
if (l == r) {
return;
}
int mid = l + ((r - l) >> 1);
mergeSort(nums, l, mid);
mergeSort(nums, mid + 1, r);
merge(nums, l, mid, r);
}
public:
void inversion(vector<int>& nums) {
if (nums.size() < 2) {
return;
}
else {
mergeSort(nums, 0, nums.size() - 1);
}
}
};
int main()
{
vector<int> a = { 5,4,3,2,1,0};
class Solution s;
s.inversion(a);
}
快排问题
荷兰国旗问题
问题一
给定一个数组arr,和一个数num,请把小于等于num的数放在数 组的左边,大于num的数放在数组的右边。要求额外空间复杂度O(1),时间复杂度O(N)
利用双指针
#include <iostream>
#include <vector>
using namespace std;
class Solution {
public:
vector<int> flag(vector<int>& nums, int val) {
int slow = -1;
int fast = 0;
for (int fast = 0; fast < nums.size(); fast++) {
if (nums[fast] <= val) {
swap(nums[fast], nums[++slow]);
}
}
return nums;
}
};
int main()
{
Solution solution;
vector<int> a = { 1, 4, 5, 7, 4, 5,8, 1, 9, 6, 0, 5 };
int val = 5;
vector<int> b;
b = solution.flag(a, val);
for (auto i = b.begin(); i < b.end(); i++) {
cout << *i << "";
}
}
问题二
给定一个数组arr,和一个数num,请把小于num的数放在数组的 左边,等于num的数放在数组的中间,大于num的数放在数组的右边。要求额外空间复杂度O(1),时间复杂度O(N)
#include <iostream>
#include <vector>
using namespace std;
class Solution {
public:
vector<int> Netherlands_flag2(vector<int>& nums, int val) {
vector<int> a;
int left = -1;
int i = 0;
int right = nums.size();
if (nums.size() < 1) {
return a;
}
for (int fast = 0; fast < nums.size(); fast++) {
if (fast == right) {
return nums;
}
if (nums[fast] < val) {
swap(nums[++left], nums[fast]);
}
else if (nums[fast] > val) {
swap(nums[--right], nums[fast]);
fast--;
}
}
}
};
int main()
{
vector<int> a = { 1,4,8,4,5,6,5,9,2,1,0 };
int val = 5;
Solution solution;
vector<int> b = solution.Netherlands_flag2(a, val);
for (auto iter = b.begin(); iter < b.end(); iter++) {
cout << *iter;
}
}
不改进的快排
-
把数组范围中的最后一个数作为划分值,然后把数组通过荷兰国旗问题分成三个部分:
左侧<划分值、中间==划分值、右侧>划分值
-
对左侧范围和右侧范围,递归执行
分析:
- 划分值越靠近两侧,复杂度越高;划分值越靠近中间,复杂度越低,复杂度O(n^2)
改进的快排(随机快速排序)
-
在数组范围中,等概率随机选一个数作为划分值,然后把数组通过荷兰国旗问题分成三个部分:
左侧<划分值、中间==划分值、右侧>划分值
-
对左侧范围和右侧范围,递归执行
-
*时间复杂度为O(N*logN),空间复杂度O(logN)
P5.详解桶排序以及排序内容大总结
2022.11.27 28
堆
堆概念
- 堆结构就是用数组实现的完全二叉树结构
- 完全二叉树中如果每棵子树的最大值都在顶部就是大根堆
- 完全二叉树中如果每棵子树的最小值都在顶部就是小根堆
堆结构操作函数
堆结构的heapInsert与heapify操作
- index, (index-1)/2父, 2index左孩子, 2index+1右孩子
void heapInsert(int& arr, int index) { //向上插入O(logN)
while (arr[index] > arr[(index - 1) / 2]) {
swap(arr, index, (index - 1) /2);
index = (index - 1)/2 ;
}
}
void heapify(int& arr, int index, int size) { //向下插入O(logN)
int left = index * 2 + 1; //左孩子
while (left < size) {
int largest = left + 1 < size && arr[left + 1] > arr[left] ? left + 1 : left; //当右孩子存在,比较左右孩子谁大,取大的为largest
largest = arr[largest] > arr[index] ? largest : index;
if (largest == index) {
break;
}
swap(arr, largest, index);
index = largest;
left = index * 2 + 1;
}
}
堆排序
-
先让整个数组都变成大根堆结构,建立堆的过程:
- 从上到下的方法,时间复杂度为O(N*logN)
- 从下到上的方法,时间复杂度为O(N)
-
把堆的最大值和堆末尾的值交换,然后减少堆的大小之后,再去调整堆,一直周而复始,时间复杂度为O(N*logN),空间复杂度O(1)
-
堆的大小减小成0之后,排序完成
void heapSort(int& arr) {
if (arr == null || arr.length < 2) {
return;
}
for (int i = 0; i < arr.length; i++) {//将数组变为大根堆
heapInsert(arr, i);
}
int size = arr.length;
swap(arr, 0, --size); //0位和堆最后一个交换
while (size > 0) {
heapify(arr, 0, size);
swap(arr, 0, --size);// size--相当于断联
}
}
堆排序题目
- 已知一个几乎有序的数组,几乎有序是指,如果把数组排好顺序的话,每个元素移动的距离可以不超过k,并且k相对于数组来说比较小。请选择一个合适的排序算法针对这个数据进行排序。
- 思路:若k=6,则在0-6上进行小根堆排序,则第一个为最小值,接着往后移一位再取出次小值
堆排序可以使用c++内置优先级队列,但相当于黑盒,无法像自定义堆排序任意改变堆中元素的值而是需要进行重新扫描
class Solution {
public:
void stackSort(vector<int>& nums, int val) {
std::priority_queue<int, deque<int>, greater<int>> value;
int index = 0;
for (index = 0; index <= val; index++) { //前k个进入
value.push(nums[index]);
}
int i = 0;
for (i = 0; index <= value.size(); index++, i++) {
nums[i] = value.top();
value.pop();
value.push(nums[index]);
}
while (!value.empty()) {
nums[i++] = value.top();
value.pop();
}
}
};
比较器
-
即 重载运算符
-
返回正数,第二个放前面;返回负数,第一个放前面;返回0,无所谓
桶排序思想下的排序
计数排序
- 类似Hash[]数组
基数排序
- 按照最长数的位数,补齐每个数
- 准备桶,首先按照个位进对应的桶,再从各位小的桶依次倒出数据(相当于按照个位排序)
- 再按照十位数字进桶···(相当于按照十位数字排序)
int maxbits(vector<int> arr) { // 确定arr数组中最大位数
int maxcount = INT32_MIN;
for (int i = 0; i < arr.size(); i++) {
int count = 0;
int temp = arr[i];
while (temp) {
count++;
temp = temp / 10;
}
maxcount = max(maxcount, count);
}
return maxcount;
}
void radixSort(vector<int>& arr, int maxbits) {
int size = arr.size();
int j = 0;
if (size < 2) {
return;
}
vector<int> help(size);
for (int d = 1; d <= maxbits; d++) { // maxbits相当于出桶入桶的次数
vector<int> count(10);// 前缀表数组,10进制数字
// 将第d位的数字放入前缀表数组中
for (int i = 0; i < size; i++) {
j = getDigit(arr[i], d);
count[j]++;
}
// 将count数组变成真正的前缀表
for (int i = 1; i < 10; i++) {
count[i] = count[i] + count[i - 1];
}
// 利用前缀表出桶,要从右往左遍历
for (int i = size - 1; i >= 0; i--) {
int place = count[getDigit(arr[i], d)];
help[place - 1] = arr[i];
count[getDigit(arr[i], d)]--;
}
// 改变arr值s
for (int i = 0; i < size; i++) {
arr[i] = help[i];
}
}
}
int getDigit(int number, int digit) {// 得到arr[i]的第digit位的值
return ((number / (int)(pow(10, digit - 1))) % 10);
}
-
时间复杂度
最好O(N)、最坏O(N^2)、平均O(N*K)
-
空间复杂度
O(N*K)
排序总结
稳定性
同样值的个体之间,如果不因为排序而改变相对次序,就是这个排序是有稳定性的;否则就没有
坑
基于比较的排序,时间复杂度没有小于O(N*logN)
没有时间复杂度为O(N*logN),空间复杂度O(N)的排序
工程上改进
-
充分利用O(N*logN)和O(N^2)排序各自的优势
例如先利用快排进行partition,当数据量较小时,采用插入排序
-
稳定性的考虑
例如自带的sort排序,会根据输入数据不同选择归并或者快排
P6.链表
2022.11.28 29
哈希表
- 时间复杂度认为O(1)
- 以基础数据类型,则全部拷贝一份作为key;若以自己定义的数据类型,则记录内存地址为key
有序表
- 放入有序表的东西,如果是基础类型,内部按值传递,内存占用就是这个东西的大小
- 放入有序表的东西,如果不是基础类型,必须提供比较器,内部按引用传递,内存占用是这个东西内存地址的大小
链表
翻转单双项列表
利用双指针
方法论
- 对于笔试,不用太在乎空间复杂度,一切为了时间复杂度
- 对于面试,时间复杂度依然放在第一位,但是一定要找到空间最省的方法
- 经常会利用哈希表等额外数据结构或者快慢指针
题目1 判断一个链表是否为回文结构
给定一个单链表的头节点head,请判断该链表是否为回文结构
笔试时,利用stack,不考虑空间复杂度。
class Solution {
public:
bool isPalindrome(ListNode* head) {
std::stack<int> st;
ListNode* cur = head;
ListNode* cur1 = head;
while(cur) {
st.push(cur->val);
cur = cur->next;
}
while(cur1) {
if (cur1->val == st.top()) {
st.pop();
}else {
return false;
}
cur1 = cur1->next;
}
return true;
}
};
快慢指针找中点
-
找中点策略:快慢指针
快指针一次走两步,慢指针一次走一步,快指针到头则慢指针到中点。
面试时候,时间复杂度达到O(N),额外空间复杂度达到O(1)
class Solution {
public:
bool isPalindrome(ListNode* head) {
ListNode* cur = head;
ListNode* pre = head;
ListNode* first = head;
ListNode* last = nullptr;
while (cur->next != nullptr && cur->next->next != nullptr) {
cur = cur->next->next;
pre = pre->next;
}
if (cur->next == nullptr) {
first = head;
last = reverse(pre);
while (first->next != nullptr && last->next !=nullptr) {
if (first->val != last->val) return false;
else {
first = first->next;
last = last->next;
}
}
return true;
}else {
first = head;
last = reverse(pre->next);
pre->next = nullptr;
while (first->next != nullptr && last->next !=nullptr) {
if (first->val != last->val) return false;
else {
first = first->next;
last = last->next;
}
}
if (first->val != last->val) return false;
return true;
}
}
ListNode* reverse(ListNode* head) {
ListNode* cur = head;
ListNode* temp = nullptr;
ListNode* pre = nullptr;
while (cur != nullptr) {
temp = cur->next;
cur->next = pre;
pre = cur;
cur = temp;
}
return pre;
}
};
题目2将单向链表按某值划分成左边小、中间相等、右边大的形式
题目:给定一个单链表的头节点head,节点的值类型是整型,再给定一个整数pivot。实现一个调整链表的函数,将链表调整为左部分都是值小于pivot的节点,中间部分都是值等于pivot的节点,右部分都是值大于pivot的节点
思路:创建六个变量,小等大的头和尾,不断更新直至结束,接着串起来。
- 笔试做法,利用额外数组空间,在数组内进行快排,最后再放入链表中时间复杂度O(N logN),空间复杂度O(N)
void listPartition(ListNode* head, int val) {
if (head == nullptr) {
return;
}
vector<int> arr;
ListNode* cur = head;
while (cur) {
arr.push_back(cur->val);
cur = cur->next;
}
quickSort(arr, val);
cur = head;
int i = 0;
while (cur) {
cur->val = arr[i++];
cur = cur->next;
}
}
- 面试做法
申请六个变量:小于给定值的头、尾,等于给定值的头、尾,大于给定值的头、尾;然后遍历一遍链表,将各个节点插入到相应的区域;最后将小于区域、等于区域和大于区域首尾依次相连。注意:三个区域均有可能为空,在相连的时候一定要讨论清楚边界!
ListNode* listPartition(ListNode* head, int val) {
ListNode* SH = nullptr;
ListNode* ST = nullptr;
ListNode* EH = nullptr;
ListNode* ET = nullptr;
ListNode* BH = nullptr;
ListNode* BT = nullptr;
ListNode* cur = head;
while (cur) {
// 将链表分配给六个指针
ListNode* temp = cur->next;
if (cur->val < val) {
cur->next = nullptr;
if (SH == nullptr) {
SH = cur;
ST = cur;
}
else {
ST->next = cur;
ST = cur;
}
}
else if (cur->val == val) {
cur->next = nullptr;
if (EH == nullptr) {
EH = cur;
ET = cur;
}
else {
ET->next = cur;
ET = cur;
}
}
else if (cur->val > val) {
cur->next = nullptr;
if (BH == nullptr) {
BH = cur;
BT = cur;
}
else {
BT->next = cur;
BT = cur;
}
}
cur = temp;
}
// 考虑多种情况,将六个指针串起来
if (SH == nullptr) {
if (EH == nullptr) {// 没小于没等于只有大于
return BH;
}
else {
if (BH == nullptr) {
return EH;// 没小于有等于没大于
}
else {
ET->next = BH;
return EH;// 没小于有等于有大于
}
}
}
else {
if (EH == nullptr) {
if (BH == nullptr) {// 有小于没等于没大于
return SH;
}
else {// 有小于没等于有大于
ST->next = BH;
return SH;
}
}
else {
if (BH == nullptr) {// 有小于有等于没大于
ST->next = EH;
return SH;
}
else {// 有小于有等于有大于
ST->next = EH;
ET->next = BH;
return SH;
}
}
}
}
题目3两个单链表相交的一系列问题
给定两个可能有环也可能无环的单链表,头节点head1和head2。请实现一个函数,如果两个链表相交,请返回相交的 第一个节点。如果不相交,返回null。
思路:可以用哈希表找环;或者用快慢指针找环
- 快慢指针找环入口:快指针走两步,慢指针走两步,如果有环,则必定在环内相遇,记录相遇点,快指针回到起始位置,与慢指针同时开始一次一步,则必在入口处相遇
ListNode* haveLoop1(ListNode* head) {// 利用哈希表
ListNode* cur = head;
unordered_set<ListNode*> uset;
while (cur) {
if (uset.find(cur) == uset.end()) {
uset.insert(cur);
cur = cur->next;
}
else {
return cur; // 有环
}
}
return nullptr; // 没有环
}
ListNode* haveLoop2(ListNode* head) {// 利用快慢指针找入环点,快慢指针在环里相遇,快指针来到开头,与慢指针一次一步,再相遇时为入口
if (head == nullptr || head->next->next == nullptr) {
return nullptr;
}
ListNode* fast = head->next->next;
ListNode* slow = head->next;
while (fast != slow) {
if (fast->next == nullptr || fast->next->next == nullptr) {
return nullptr;
}
fast = fast->next->next;
slow = slow->next;
}
fast = head;
while (fast != slow) {
slow = slow->next;
fast = fast->next;
}
return fast;
}
ListNode* noLoop(ListNode* head1, ListNode* head2) {
if (head1 == nullptr || head2 == nullptr) {
return nullptr;
}
int n = 0;
ListNode* cur = head1;
while (cur->next != nullptr) {
n++;
cur = cur->next;
}
cur = head2;
while (cur->next != nullptr) {
n--;
cur = cur->next;
}
ListNode* cur1 = n > 0 ? head1 : head2; // cur1为长链表
ListNode* cur2 = n > 0 ? head2 : head1; // cur2为短链表
n = abs(n);
while (n != 0) {
cur1 = cur1->next;
n--;
}
while (cur1 != nullptr && cur2 != nullptr) {
cur1 = cur1->next;
cur2 = cur2->next;
if (cur1 == cur2) {
return cur1;
}
}
return nullptr;
}
ListNode* bothLoop(ListNode* head1, ListNode* loop1, ListNode* head2, ListNode* loop2) {
if (loop1 == loop2) {
int n = 0;
ListNode* cur = head1;
while (cur->next != loop1) {
n++;
cur = cur->next;
}
cur = head2;
while (cur->next != loop1) {
n--;
cur = cur->next;
}
ListNode* cur1 = n > 0 ? head1 : head2; // cur1为长链表
ListNode* cur2 = n > 0 ? head2 : head1; // cur2为短链表
n = abs(n);
while (n != 0) {
cur1 = cur1->next;
n--;
}
while (cur1 != cur2) {
cur1 = cur1->next;
cur2 = cur2->next;
}
return cur1;
}
else {
ListNode* cur1 = head1->next;
while (cur1 != loop2) {
if (cur1 == loop1) {
return nullptr;
}
cur1 = cur1->next;
}
return loop2;
}
}
ListNode* getIntersectNode(ListNode* head1, ListNode* head2) {
if (head1 == nullptr || head2 == nullptr) {
return nullptr;
}
ListNode* cur1 = haveLoop2(head1);
ListNode* cur2 = haveLoop2(head2);
if (cur1 == nullptr && cur2 == nullptr) {
return noLoop(head1, head2);
}
if (cur1 != nullptr && cur2 != nullptr) {
return bothLoop(head1, cur1, head2, cur2);
}
return nullptr;
}
题目四 复制含有随机指针节点的链表
笔试做法:利用map
ListNode* copyListWithRand1(ListNode* head) {
unordered_map<ListNode*, ListNode*> umap;
ListNode* cur = head;
while (cur != nullptr) { // 将所有元素放到umap中
umap.insert({ cur, new ListNode(cur->val * 10) });
cur = cur->next;
}
cur = head;
while (cur != nullptr) {
umap[cur]->next = umap[cur->next];
umap[cur]->rand = umap[cur->rand];
cur = cur->next;
}
return umap.at(head);
}
面试做法
ListNode* copyListWithRand1(ListNode* head) {
unordered_map<ListNode*, ListNode*> umap;
ListNode* cur = head;
while (cur != nullptr) { // 将所有元素放到umap中
umap.insert({ cur, new ListNode(cur->val * 10) });
cur = cur->next;
}
cur = head;
while (cur != nullptr) {
umap[cur]->next = umap[cur->next];
umap[cur]->rand = umap[cur->rand];
cur = cur->next;
}
return umap[head];
}
ListNode* copyListWithRand2(ListNode* head) {
if (head == nullptr) {
return nullptr;
}
ListNode* cur = head;
while (cur != nullptr) { // 复制节点
ListNode* newNode = new ListNode(cur->val * (10));
ListNode* temp = cur->next;
newNode->next = cur->next;
cur->next = newNode;
cur = temp;
}
cur = head;
while (cur != nullptr) { // 构造节点random指针
if (cur->rand != nullptr) {
cur->next->rand = cur->rand->next;
}
else {
cur->next->rand = nullptr;
}
if (cur->next->next == nullptr) {
break;
}
cur = cur->next->next;
}
cur = head;
ListNode* temp = head->next;
while (cur != nullptr) { // 删除原来节点连线
ListNode* p = cur->next;
cur->next = nullptr;
if(p->next == nullptr) {
break;
}
cur = p->next;
p->next = cur->next;
}
return temp;
}
P7二叉树
基础知识
定义
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
存储方式
- 链表
- 数组
遍历方式
深度优先,广度优先
前中后指的是中的位置,其余都按照左右排列
-
前序
[中左右]
class Solution { public: vector<int> preorderTraversal(TreeNode* root) { vector<int> result; traversal(root, result); return result; } void traversal(TreeNode* cur, vector<int>& result) { if (cur == nullptr) { return; } result.push_back(cur->val); traversal(cur->left, result); traversal(cur->right, result); } };
class Solution { public: vector<int> preorderTraversal(TreeNode* root) { stack<TreeNode*> st; vector<int> result; if (root == NULL) return result; st.push(root); while (!st.empty()) { TreeNode* node = st.top(); // 中 st.pop(); result.push_back(node->val); if (node->right) st.push(node->right); // 右(空节点不入栈) if (node->left) st.push(node->left); // 左(空节点不入栈) } return result; } };
-
中序
[左中右]
void traversal(TreeNode* cur, vector<int>& vec) { if (cur == NULL) return; traversal(cur->left, vec); // 左 vec.push_back(cur->val); // 中 traversal(cur->right, vec); // 右 }
class Solution { public: vector<int> inorderTraversal(TreeNode* root) { vector<int> result; stack<TreeNode*> st; TreeNode* cur = root; while (cur != NULL || !st.empty()) { if (cur != NULL) { // 指针来访问节点,访问到最底层 st.push(cur); // 将访问的节点放进栈 cur = cur->left; // 左 } else { cur = st.top(); // 从栈里弹出的数据,就是要处理的数据(放进result数组里的数据) st.pop(); result.push_back(cur->val); // 中 cur = cur->right; // 右 } } return result; } };
-
后序
[左右中]
void traversal(TreeNode* cur, vector<int>& vec) { if (cur == nullptr) { return; } traversal(cur->left, vec); traversal(cur->right, vec); vec.push_back(cur->val); }
-
宽度优先遍历bfs
利用队列,按照宽度打印,将头节点入队后,弹出,再进左再进右
//宽度优先遍历 void bfs(Node* head) { if (head != nullptr) { queue<Node*>q; q.push(head); while (!q.empty()) { head = q.front(); q.pop(); cout << head->val << " "; if (head->left != nullptr) { q.push(head->left); } if (head->right != nullptr) { q.push(head->right); } } } }
题目求一棵二叉树的宽度
思路1:利用哈希表
利用bfs按照宽度遍历,则使用queue,将map中放入Node和其对应的行数
int getMaxWidth(Node* head) {
queue<Node*> myque; //队列
unordered_map<Node*, int> umap; // 设置哈希表,分别存放二叉树结点以及结点所在层数
int curlevel = 1; //当前层
int nodes = 0;//该层数目
int max = -1;//最大 实时更新
umap[head] = 1;
myque.push(head);
Node* cur = head;
while (!myque.empty()) {
cur = myque.front();
myque.pop();
if (cur->left != nullptr) {
umap[cur->left] = curlevel + 1;
myque.push(cur->left);
}
if (cur->right != nullptr) {
umap[cur->right] = curlevel + 1;
myque.push(cur->right);
}
if (umap[cur] == curlevel) { //当该节点层数与当前层数相同,则节点数目++
nodes++;
}
else { // 若不同,则更新max,更新curlevel
max = max > nodes ? max : nodes;
curlevel = umap[cur];
nodes = 1;
}
}
return max;
}
思路2 不用hashmap。需要用到几个变量:curend:当前层最后一个节点、nextend:下一层最后一个节点(始终是最近进栈的节点,换层的时候要置空)、curlevel当前层已经发现的节点数
int getMaxWidth02(Node* head) {
if (head == nullptr) {
return 0;
}
queue<Node*> myque;
Node* curend = head;
Node* nextend = nullptr;
int nodes = 1;
int maxNodes = INT32_MIN;
myque.push(head);
Node* cur = nullptr;
while (!myque.empty()) {
cur = myque.front();
myque.pop();
if (cur->left != nullptr) {
myque.push(cur->left);
nextend = cur->left;
}
if (cur->right != nullptr) {
myque.push(cur->right);
nextend = cur->right;
}
nodes++;
if (cur == curend) {
curend = nextend;
nextend = nullptr;
maxNodes = max(maxNodes, nodes);
nodes = 0;
}
}
return maxNodes;
}
判断 搜索二叉树
定义
- 是一个有序树
- 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
- 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
- 它的左、右子树也分别为二叉排序树
判断方式
利用中序排列,若为升序,则为搜索二叉树
判断 完全二叉树
定义
在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层,则该层包含 1~ 2^(h-1) 个节点。
判断方式
- 利用层序遍历
- 若某个节点有右孩子,没左孩子,则返回false
- 在满足上一条情况时,第一次遇到左右节点不全情况,则后续必须为叶节点
bool isCBT(Node* head) {
if (head == nullptr) {
return true;
}
queue<Node*> myque;
myque.push(head);
bool leaf = false;
Node* cur = nullptr;
while (!myque.empty()) {
cur = myque.front();
myque.pop();
if ((leaf && (cur->right != nullptr || cur->left != nullptr)) || (cur->left == nullptr && cur->right != nullptr)) {//某个节点有右孩子,没左孩子;第一次遇到左右节点不全情况,则后续必须为叶节点
return false;
}
if (cur->left != nullptr) {
myque.push(cur->left);
}
if (cur->right != nullptr) {
myque.push(cur->right);
}
else {
leaf = true;
}
}
return true;
}
判断 满二叉树
- 麻烦方法:求出二叉树深度,再求出节点个数,若满足2^n-1则为满二叉树
- 利用套路向左树要深度和节点数,向右数要深度和节点数看书否满足标准
//判断一颗二叉树是否是满二叉树
struct ReDa {
int depth;
int nodes;
bool isFbt;
ReDa(int depth, int nodes, bool isFbt) {
this->depth = depth;
this->nodes = nodes;
this->isFbt = isFbt;
}
};
ReDa isFull(Node* head) {
if (head == nullptr) {
return ReDa(0, 0, true);
}
ReDa leftData = isFull(head->left);
ReDa rightData = isFull(head->right);
int depth = max(leftData.depth, rightData.depth) + 1;
int nodes = leftData.nodes + rightData.nodes + 1;
bool isFbt = (nodes == (int)pow(2, depth) - 1) ? true : false;
return ReDa(depth, nodes, isFbt);
}
判断 平衡二叉树
平衡二叉树:任意一个节点,左树的高度和右树的高度差不超过1
套路:求解一个二叉树问题时,我可以向我的左树要信息,可以向我的右树要信息的情况下,列出可能性,向上层递归返回我这一层的信息。可以解决一切树型DP(动态规划)的问题,二叉树中最难的问题。
假设以x为头的子树,判断他是不是平衡二叉树,可以向我的左树要信息,可以向我的右树要信息的情况下,罗列可能性;1) 左子树要是平衡二叉树;2) 右子树要是平衡二叉树;3) 左子树、右子树的高度差小于2;这里只有一种可能性:以上三个条件都成立。所以左树需要给我:它是否是平的和高度;右树需要给我:它是否是平的和高度。
利用递归(黑盒)去解
//***************************平衡二叉树***************************//
struct ReturnType {
bool isBalance;
int height;
ReturnType(bool isBalance, int height) : isBalance(isBalance), height(height) {}
};
ReturnType process(Node* head) {
if (head == nullptr) {
return ReturnType(true, 0);
}
ReturnType left = process(head->left);
ReturnType right = process(head->right);
bool isBalance = (left.isBalance && right.isBalance) && (abs(left.height - right.height) < 2);
int height = max(left.height, right.height) + 1;
return ReturnType(isBalance, height);
}
bool isBalance(Node* head) {
if (process(head).isBalance) {
cout << "是";
}
else {
cout << "不是";
}
return process(head).isBalance;
}
给定两个二叉树的节点node1和node2,找到他们的最低公共祖先节点
力扣236
- 思路1:视频1:33分前
- 首先定义unordered_map为fathermap,存储所有结点的father结点至fathermap中
- 定义unordered_set,将node1所有父结点放入set中,遍历node2,若其父节点与set中重合,则找到最低公共祖先结点
// 生成fathermap,即每个节点对应的father记录在map中
void process(Node* head, unordered_map<Node*, Node*> fathermap) {
if (head == nullptr) {
return;
}
fathermap.insert({ head->left, head });
fathermap.insert({ head->right, head });
process(head->left, fathermap);
process(head->right, fathermap);
}
Node* lowestAncestor01(Node* head, Node* o1, Node* o2) {
if (head == o1 && o1 == o1) {
return head;
}
if (head == nullptr || o1 == nullptr || o2 == nullptr) {
return nullptr;
}
unordered_map<Node*, Node*> fathermap;
process(head, fathermap);
fathermap.insert({ head, head });
unordered_set<Node*> uset;
Node* cur = o1;
while (fathermap[cur] != cur) { // 没有到顶
uset.insert(cur);
cur = fathermap[cur];
}
uset.insert(head);
cur = o2;
while (fathermap[cur] != cur) {
if (uset.find(cur) != uset.end()) {
return cur;
}
cur = fathermap[cur];
}
return nullptr;
}
-
思路2
- o1可能为o2的LCA
- o1可能和o2不互为,需要往上找才行
两者为空返回空,两者有值返回头
public static Node lowestAncestor(Node head, Node o1, Node o2) { if (head == null || head == o1 || head == o2) { return head; } Node left = lowestAncestor(head.left, o1, o2); Node right = lowestAncestor(head.right, o1, o2); if (left != null && right != null) { return head; } return left != null ? left : right; }
在二叉树中找到一个节点的后继节点
思路
中序遍历特性,判断是否有右儿子,有的话后继为其最底左儿子,若没有则后继为parent中使其成为左树分支的最近一个parent,最后一个节点为null
public static Node getSuccessorNode(Node node) {
if (node == null) {
return node;
}
if (node.right != null) {
return getLeftMost(node.right);
} else {
Node parent = node.parent;
while (parent != null && parent.left != node) {
node = parent;
parent = node.parent;
}
return parent;
}
}
public static Node getLeftMost(Node node) {
if (node == null) {
return node;
}
while (node.left != null) {
node = node.left;
}
return node;
}
二叉树的序列化和反序列化
就是内存里的一棵树如何变成字符串形式,又如何从字符串形式变成内存里的树
//***************************二叉树的序列化和反序列化***************************//
string serialByPre(Node* head) { //先序
if (head == nullptr) {
return "#!";
}
string res;
res = to_string(head->val) + "!";
res += serialByPre(head->left);
res += serialByPre(head->right);
return res;
}
queue<string> splitString(string str, char spS) { // split 字符串 重要!!!
queue<string> q;
string s = " ";
for (auto ch : str) {
if (ch == spS) {
q.push(s);
s = " ";
}
else {
s = s + ch;
}
}
return q;
}
Node* reconPreOrder(queue<string> myque) {
string str = myque.front();
myque.pop();
if (str == "#") {
return nullptr;
}
Node* head = new Node(stoi(str));
head->left = reconPreOrder(myque);
head->right = reconPreOrder(myque);
return head;
}
Node* antiserialByPre(string str) {
if (str.length() < 3) {
return nullptr;
}
queue<string> myque = splitString(str, '!');
return reconPreOrder(myque);
}
剑指offer 48
折纸问题
void fold(int i, int N, bool updown) {
if (i > N) {
return;
}
fold(i + 1, N, true);
updown ? cout << "down " : cout << "up ";
fold(i + 1, N, false);
}
P9图
模板
class Node;//两个类相互包含,需要提前声明一下
class Edge {
public:
int weight;
Node* from;//有向图
Node* to;
Edge(int weight, Node* from, Node* to) {
this->weight = weight;
this->from = from;
this->to = to;
}
};
class Node {
public:
int val;
int in;//入度
int out;//出度
vector<Node*> nexts;
vector<Edge*> edges;//由当前点发散出去的边
Node(int val) {
this->val = val;
in = 0;
out = 0;
}
};
class Graph {
public:
unordered_map<int, Node*>nodes;//key:点的编号;value:实际的点
unordered_set<Edge*>edges;
};
宽度(广度)优先遍历BFS
- 利用队列实现
- 从源节点开始依次按照宽度进队列,然后弹出
- 每弹出一个点,把该节点所有没有进过队列的邻接点放入队列
- 直到队列变空
void BreadthFirst_Traveral(Node* node) {
if (node == nullptr) {
return;
}
queue<Node*> myque; //队列来记录
unordered_set<Node*> myset;//set来记录是否访问过
myque.push(node);
myset.insert(node);
Node* cur = nullptr;
while (!myque.empty()) {//首先将queue中第一个弹出,再将该结点next中未记录的放入queue并记录
cur = myque.front();
myque.pop();
cout << cur->value << ' ';//弹出时打印
for (auto iter : cur->next) {
if (myset.find(iter) == myset.end()) {
myque.push(iter);
myset.insert(iter);
}
}
}
}
深度优先遍历xDFS
- 利用栈实现
- 从源节点开始把节点按照深度放入栈,然后弹出
- 每弹出一个点,把该节点下一个没有进过栈的邻接点放入栈
- 直到栈变空
void DepthFirst_Traveral(Node* node) {//深度优先遍历,相当于一股脑先遍历到头
if (node == nullptr) {
return;
}
unordered_set<Node*> myset;//set记录是否访问过
stack<Node*> myst;//stack记录Node*
myset.insert(node);
myst.push(node);
cout << node->value;//第一次放入时打印
Node* cur = nullptr;
while (!myst.empty()) {
cur = myst.top();
myst.pop();
for (auto p : cur->next) {
if (myset.find(p) == myset.end()) {
cout << p->value;
myset.insert(p);
myst.push(cur);
myst.push(p);
break;//找到1个就行
}
}
}
}
拓扑排序算法
适用范围:要求有向图,且有入度为0的节点,且没有环
力扣207.课程表
- 使用map存储节点及其入度,使用queue存储入度为0的点
- 遍历图中所有结点,把入度为0的放入queue中
- 弹出queue中第一个入度为0的结点并记录
- 遍历入度为0的next结点,并将其入度-1,若有入度为0则放到queue里面
- 队列不位空,重复34
vector<Node*> Topu(Graph graph) {
unordered_map<Node*, int> umap;
queue<Node*> myque;
for (auto p : graph.nodes) {
umap.insert({ p.second, p.second->in });// 利用map记录节点及其入度
if (p.second->in == 0) {
myque.push(p.second);// 将入度为0的节点加入queue
}
}
vector<Node*> res;
while (!myque.empty()) {
Node* cur = myque.front();
myque.pop();
res.push_back(cur);
for (auto p : cur->nexts) {
umap[p]--;
if (umap[p] == 0) {
myque.push(p);
}
}
}
return res;
}
最小生成树
定义:使无向图所有节点连通且权重最小的边集,是最小权重生成树的简称。
算法:kruskal算法、prim算法
kruskal算法
要求:无向图
- 从最小边开始搜索,若没形成环加上,否则不加,继续循环
核心代码 改堆
public static class EdgeComparator implements Comparator<Edge> {
public int compare(Edge o1, Edge o2) {
return o1.weight - o2.weight;
}
}
public static Set<Edge> kruskalMST(Graph graph) {
UnionFind unionFind = new UnionFind();
unionFind.makeSets(graph.nodes.values());
PriorityQueue<Edge> priorityQueue = new PriorityQueue<>(new EdgeComparator());
for (Edge edge : graph.edges) {
priorityQueue.add(edge);
}
Set<Edge> result = new HashSet<>();
while (!priorityQueue.isEmpty()) {
Edge edge = priorityQueue.poll();
if (!unionFind.isSameSet(edge.from, edge.to)) {
result.add(edge);
unionFind.union(edge.from, edge.to);
}
}
return result;
}
并查集
public static class UnionFind {
private HashMap<Node, Node> fatherMap;
private HashMap<Node, Integer> rankMap;
public UnionFind() {
fatherMap = new HashMap<Node, Node>();
rankMap = new HashMap<Node, Integer>();
}
private Node findFather(Node n) {
Node father = fatherMap.get(n);
if (father != n) {
father = findFather(father);
}
fatherMap.put(n, father);
return father;
}
public void makeSets(Collection<Node> nodes) {
fatherMap.clear();
rankMap.clear();
for (Node node : nodes) {
fatherMap.put(node, node);
rankMap.put(node, 1);
}
}
public boolean isSameSet(Node a, Node b) {
return findFather(a) == findFather(b);
}
public void union(Node a, Node b) {
if (a == null || b == null) {
return;
}
Node aFather = findFather(a);
Node bFather = findFather(b);
if (aFather != bFather) {
int aFrank = rankMap.get(aFather);
int bFrank = rankMap.get(bFather);
if (aFrank <= bFrank) {
fatherMap.put(aFather, bFather);
rankMap.put(bFather, aFrank + bFrank);
} else {
fatherMap.put(bFather, aFather);
rankMap.put(aFather, aFrank + bFrank);
}
}
}
}
prim算法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cMGpLR4H-1671605563886)(C:\Users\MSI-NB\Desktop\左程云\图片\Prim.png)]
unordered_set<Edge*> Graph::MiniScanTree_Prim(Graph graph) {
priority_queue<Edge*, vector<Edge*>, prior> myqueue;
unordered_set<Node*> uset;
unordered_set<Edge*> result;
for (auto node : graph.nodes) {
if (uset.find(node.second) == uset.end()) {//如果发现结点不在set中执行
uset.insert(node.second);//首先set中插入结点
for (auto edges : node.second->edges) {//将所有对应的edges放入小根堆中
myqueue.push(edges);
}
while (!myqueue.empty()) {//当myqueue不为空
Edge* cur_edge = myqueue.top();//弹出最小边
myqueue.pop();
if (uset.find(cur_edge->to) == uset.end()) {//若最小边对应的to没被选中过,执行,否则继续弹出第二小
result.insert(cur_edge);//将该较小边放入result中
uset.insert(cur_edge->to);//将较小边对应的to放入set中
for (auto newEdges : cur_edge->to->edges) {//解锁to点的所有边
myqueue.push(newEdges);
}
}
}
}
}
return result;
}
单元最短路径算法—Dijkstra算法
适用范围:不存在累加权值为负的环
初始化一个节点到其他节点距离表,该表到自己的距离为0,到其他节点的距离都为正无穷,每一次在表中选择距离最小的点A,考察从A出发的边能否使得起始点到其他点的距离变小,如果存在这样的边,则更新距离表,使用完A之后,A到起始点的距离确定,不再修改;再选择次小的点……
Node* getMiniNode(unordered_set<Node*> uset, unordered_map<Node*, int> distanceMap)
{
Node* miniNode = nullptr;
for (pair<Node*, int> p : distanceMap) {
if (uset.find(p.first) == uset.end()) {
if (miniNode != nullptr) {
miniNode = p.second < distanceMap[miniNode] ? p.first : miniNode;
}
else {
miniNode = p.first;
miniNode = p.first;
}
}
}
return miniNode;
}
unordered_map<Node*, int> Dijkstral(Node* head) {
unordered_set<Node*> uset;// 存放已经遍历过的节点
unordered_map<Node*, int> distanceMap;
distanceMap.insert({ head, 0 });
Node* miniNode = getMiniNode(uset, distanceMap);
while (miniNode != nullptr) {
int distance = distanceMap[miniNode];
for (Edge* p : miniNode->edges) {
if (uset.find(p->to) == uset.end()) {
distanceMap.insert({ p->to, p->weight });
}
else {
distanceMap[p->to] = min(distanceMap[p->to], distance + p->weight);
}
}
uset.insert(miniNode);
miniNode = getMiniNode(uset, distanceMap);
}
return distanceMap;
}
P10前缀树和贪心算法
前缀树
定义:
class TrieNode {
public:
int pass;//有多少个走过该结点
int end;// 有多少个在该结点结束
vector<TrieNode*> next;// 存放TrieNode*的向量数组
TrieNode() {//构造函数
this->pass = 0;
this->end = 0;
this->next = vector<TrieNode*>(26, nullptr); //26个元素,每个为空指针s
}
};
生成前缀树
void Trie::insert(string word) {
if (word.length() == 0) {
return;
}
TrieNode* node = root; // 首先构造头结点
node->pass++; //头结点的pass++
for (char ch : word) { // 遍历word里的每个字符
if (node->next[ch - 'a'] == nullptr) { // 若next没有对应结点,则新建
node->next[ch - 'a'] = new TrieNode();
}
node = node->next[ch - 'a'];
node->pass++;
}
node->end++;
}
struct TrieNode {
int pass;
int end;
vector<TrieNode*> nexts;
TrieNode() {
this->pass = 0;
this->end = 0;
this->nexts = vector<TrieNode*>(26, nullptr);
}
};
class Trie {
private:
TrieNode* root;
public:
Trie() {
this->root = new TrieNode();
}
void insert(string str) {
if (str.length() == 0) {
return;
}
TrieNode* node = root;
node->pass++;
for (char ch : str) {
int index = ch - 'a';
if (node->nexts[index] == nullptr) {
node->nexts[index] = new TrieNode();
}
node->nexts[index]->pass++;
node = node->nexts[index];
}
node->end++;
cout << "添加成功" << str << endl;
}
int search(string str) {
if (str.length() == 0) {
return 0;
}
TrieNode* node = root;
for (char ch : str) {
int index = ch - 'a';
if (node->nexts[index] == nullptr) {
cout << "找到" << str << "有" << "0" << "次" << endl;
return 0;
}
else {
node = node->nexts[index];
}
}
cout << "找到" << str << "有" << node->end << "次" << endl;
return node->end;
}
int prefixNumber(string str) {
if (str.length() == 0) {
return 0;
}
TrieNode* node = root;
for (char ch : str) {
int index = ch - 'a';
if (node->nexts[index] == nullptr) {
cout << "以" << str << "为前缀有" << "0" << "次" << endl;
return 0;
}
else {
node = node->nexts[index];
}
}
cout << "以" << str << "为前缀有" << node->pass << "次" << endl;
return node->pass;
}
void deleteWord(string str) {
if (search(str)) {// 添加过该字符串
TrieNode* node = root;
root->pass--;
int index = -1;// 记录第一个pass为0的位置
TrieNode* pre = nullptr; // 记录pass最后一个不为0的指针
stack<TrieNode*> st;
bool needDetele = false;
for (int i = 0; i < str.length(); i++) {
int j = str[i] - 'a';
node->nexts[j]->pass--;
if (node->nexts[j]->pass == 0) {
needDetele = true;
index = index == -1 ? i : index;
pre = pre == nullptr ? node : pre;
st.push(node->nexts[j]);
}
node = node->nexts[j];
}
node->end--;
if (needDetele) {
pre->nexts[str[index] - 'a'] = nullptr;
while (!st.empty()) {
delete st.top();
st.pop();
}
}
}
}
};
贪心算法
在某一个标准下,优先考虑最满足标准的样本,最后考虑最不满足标准的样本,最终得到一个答案的算法,叫作贪心算法。
也就是说,不从整体最优上加以考虑,所做出的是在某种意义上的局部最优解。
题目1会议室
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CEIcmsQd-1671605563886)(C:\Users\MSI-NB\Desktop\左程云\图片\会议室.jpg)]
贪心方法:按照结束时间来排序,首先选取结束时间最早的,再依次跟上
class Program {
public:
double start;
double end;
Program(double start, double end) {
this->start = start;
this->end = end;
}
};
bool cmp(Program* o1, Program* o2) {
return o1->end < o2->end;
}
class Solution {
public:
int bestArrange(vector<Program*> program, int start) {
sort(program.begin(), program.end(), cmp);
cout << "经过end排序后" << endl;
for (int i = 0; i < program.size(); i++) {
cout << "start:" << program[i]->start << " end:" << program[i]->end << endl;
}
int res = 0;
cout << "最多有" << "分别为" << endl;
for (int i = 0; i < program.size(); i++) {
if (start <= program[i]->start) {
res++;
start = program[i]->end;
cout << "start:" << program[i]->start << " end:" << program[i]->end << endl;
}
}
cout << "最多有" << res << "个会议可以进行" << endl;
return res;
}
};
题目2字典序
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l0NdJPqM-1671605563887)(C:\Users\MSI-NB\Desktop\左程云\图片\字典.jpg)]
- 方法:a+b小于等于b+a则交换位置
- 将sort函数的cmp函数进行以上规则的改写
题目三金条切割
利用哈夫曼树
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0HaUan6Z-1671605563887)(C:\Users\MSI-NB\Desktop\左程云\图片\金条.jpg)]
哈夫曼算法描述:
- 根据给定权值n构建二叉树集合f,其中每个二叉树只有带权为w的根节点,没有左右子树
- 再f中选取两个权值最小的构建新的二叉树,删除老的两颗,在f中加入新的二叉树
- 重复步骤,直至只剩一棵二叉树
题目4项目收益
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8rXbPO9Q-1671605563887)(C:\Users\MSI-NB\Desktop\左程云\图片\项目收益解答.png)]
思路:
- 在小根堆中放入所有的项目,按照项目需要花费的多少排序
- 从小根堆中拿出项目花费小于初始资金的项目放入大根堆中,按照利润大小排序
- 则每次从大根堆中取出元素则为下个需要进行的项目
- 若大根堆中没有项目,或者所做项目之和超过k则停止
题目五中位数
一个数据流中,随时可以取得中位数
- 准备一个大根堆,一个小根堆
- 第一个数直接放入
- 第二个数cur开始,若cur比大根堆第一个数小,则放入大根堆,反之放入小根堆
- 若大小根堆数量差距大于1,则将size大的弹一个放到size小的去
- 若n为奇数,则中位数为size大第一个数,若为偶数,则中位数为两个堆的顶
P11暴力递归
暴力递归就是尝试
- 把问题转化为规模缩小了的同类问题的子问题
- 有明确的不需要继续进行递归的条件(base case)
- 有当得到了子问题的结果之后的决策过程
- 不记录每一个子问题的解
汉诺塔问题
- 设有i个待移动,from, to, other
- 将i-1个由from移到other中
- 将第i个由from移到to中
- 将i-1个由other移到to中
int hanoi(int i, string from, string to, string other) {
static int count = 0;
if (i == 1) { // base case
cout << from << "->" << to << endl;
count++;
}
else {
// 首先,先将i-1个由from放到other上
hanoi(i - 1, from, other, to);
// 再将最后第i个由from放到to上
cout << from << "->" << to << endl;
count++;
// 再将i-1个由other放到to上
hanoi(i - 1, other, to, from);
}
return count;
}
打印子序列
题目:打印一个字符串的全部子序列,包括空字符串
void printAllSubsquence(int i, string str) {
if (i == str.length()) {
cout << "(" << str << ")";
return;
}
printAllSubsquence(i + 1, str);// 第i个不为0递归
string temp = str;
str[i] = 0;
printAllSubsquence(i + 1, str);// 第i个为0递归
str = temp;
}
打印一个字符串的全部排列,要求不要出现重复的排列
- 第i个字符开始,前i-1个已经定下来了,交换j和i字符的位置,再进行递归
vector<string> Permutation(string str) {
vector<string> res;
if (str.length() == 0) {
return res;
}
process(0, str, res);
return res;
}
void process(int i, string str, vector<string> &res) {
if (i == str.length()) {
res.push_back(str);
return;
}
unordered_set<char> uset;// 不能设置为全局变量,应该设置为当前层的变量用来存目前已经有多少入库
for (int j = i; j < str.length(); j++) {
if (uset.find(str[j]) == uset.end()) {
uset.insert(str[j]);
swapstr(str, i, j);
process(i + 1, str, res);
swapstr(str, i, j);
}
}
}
void swapstr(string &s, int i, int j) {// 需要对参数进行修改则需要加入&号
char temp = s[j];
s[j] = s[i];
s[i] = temp;
}
拿牌
逆序栈,不用额外数据结构,只使用递归函数
void reverse(stack<int> st) {
if (st.empty()) {
return;
}
int result = getAndRemoveLastElement(st);
reverse(st);
st.push(result);
}
int getAndRemoveLastElement(stack<int> st) { // 返回栈底元素,并使栈整体下移
int result = st.top();
st.pop();
if (st.empty()) {
return result;
}
else {
int last = getAndRemoveLastElement(st);
st.push(result);
return last;
}
}
解码方法
- 在i字符中,前i-1都已经解码分配完成
- 第i个,若为1则可以分为单个和与后者结合;若为2要看与后者结合会不会超过26
- 其余3-9只能与自己结合
- 进行递归,设置好base case
int process(string str, int i) {
if (i == str.length()) {
return 1;
}
if (str[i] == '0') {
return 0;
}
else if (str[i] == '1') {
int res = process(str, i + 1);
if ((i + 1) < str.length()) {
res += process(str, i + 2);
cout << "!";
}
return res;
}
else if (str[i] == '2') {
int res = process(str, i + 1);
if ((i + 1) < str.length() && (str[i + 1] >= '0' && str[i + 1] <= '6')) {
res += process(str, i + 2);
}
return res;
}
return process(str, i + 1);
}
int numDecodings(string s) {
if (s.length() == 0) {
return 0;
}
return process(s, 0);
}
背包问题
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kxmaxiL3-1671605563888)(C:\Users\MSI-NB\Desktop\左程云\图片\背包问题.PNG)]
// i-1个背包已经选完,第i个背包开始
int process(vector<int> value, vector<int> weight, int i, int alreadywight, int bag) {
if (i == weight.size()) {
return 0;
}
if ((weight[i] + alreadywight) > bag) {
return process(value, weight, i + 1, alreadywight, bag);
}
else {
return max(
process(value, weight, i + 1, alreadywight, bag),
value[i] + process(value, weight, i + 1, alreadywight + weight[i], bag)
);
}
}
N皇后问题
bool isValid(vector<int> record, int i, int j) {
for (int k = 0; k < i; k++) { // 只需要遍历i前面的record即可
if (j == record[k] || (abs(i - k) == abs(record[k] - j))) {
return false;
}
}
return true;
}
int process1(int i, vector<int>& record, int n) {// 传统方法 , 其中record[i] = j表示,第i行的第j列已经有皇后了
if (i == n) {
return 1;
}
int res = 0;
for (int j = 0; j < n; j++) { // 例:在第一行i=0时,统计每一列为N皇后时的res总和
if (isValid(record, i, j)) {
record[i] = j;
res += process1(i + 1, record, n);
}
}
return res;
}
int process2(int limit, int colLim, int leftDiaLim, int rightDiaLim) {// 使用位运算加速
if (limit == colLim) {
return 1;
}
int res = 0;
int pos = 0;
int mostRightone = 0;
pos = limit & (~(colLim | leftDiaLim | rightDiaLim)); // 用1来表示可以放置皇后的点
while (pos != 0) { // 当还有放皇后的位置
mostRightone = pos & (~pos + 1); // 取出最右边的1
pos = pos - mostRightone;
res += process2(limit, colLim | mostRightone, (leftDiaLim | mostRightone) << 1, (rightDiaLim | mostRightone) >> 1); // 左极限为当前选择与上个做极限或并且左移一位
}
return res;
}