蔚天灿雨的笔试记录

笔试(刷题)

文章目录

15. 三数之和

KMP算法。

堆排序

148. 排序链表

340. 至多包含 K 个不同字符的最长子串

31. 下一个排列

912. 排序数组

41. 缺失的第一个正数

124. 二叉树中的最大路径和

96. 不同的二叉搜索树

图的最短路径问题:Dijkstra算法

class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        // 想法,每次都剪断一个链,同时连接一个链
        ListNode *cur, *pre, *tmp;

        cur = head;
        pre = nullptr;

        while (cur){
            tmp = cur -> next;
            cur -> next = pre;
            pre = cur;
            cur = tmp;
        }

        return pre;
    }
};

一些小trick

1. 保证第一个传入参数一定比第二个大:

vector<int> intersect(vector<int>& nums1, vector<int>& nums2) {

   if (nums1.size() > nums2.size()) {
     return intersect(nums2, nums1);
    }
  …
}

2. 立即释放空间malloc_trim(0);

malloc_trim(0);

这个函数会将系统所有被延迟释放的空间,全都释放—比如vec.clear()之后可以调用这个(如果内存不够用的话),其实free() 也是延迟释放的。

——vec立即释放空间可以使用

vctor<int>().swap(vec);

3. 两个正数相加除以二尽量不溢出

l + (r - l) >> 1;

4. cin的空格处理

std::getline(std::cin, s); //如果想要输入带有空格的

5. 建立映射关系

// 1.可以用数组
const string letterMap[10] = {
    "", // 0
    "", // 1
    "abc", // 2
    "def", // 3
    "ghi", // 4
    "jkl", // 5
    "mno", // 6
    "pqrs", // 7
    "tuv", // 8
    "wxyz", // 9
};

// 2.可以用map
Map<Character, String> phoneMap = new HashMap<Character, String>() {{
            put('2', "abc");
            put('3', "def");
            put('4', "ghi");
            put('5', "jkl");
            put('6', "mno");
            put('7', "pqrs");
            put('8', "tuv");
            put('9', "wxyz");
}};


6. 关于函数取名问题,如果不会起名,可以用leetcode上的网址取名

比如131.分割回文串,网址为https://leetcode.cn/problems/palindrome-partitioning/,所以可以判断回文的英语应该是palindrome

7. 建立图

// unordered_map<出发机场, map<到达机场, 航班次数>> targets
unordered_map<string, map<string, int>> targets
for (auto vec : tickets) {
     targets[vec[0]][vec[1]]++; // 记录映射关系
}

8. 使用sort(带cmp函数)的注意事项

经典例子:452.用最少数量的箭引爆气球 – 注意超时原因(注意2)↓

// 按照数组的第一个元素进行升序排列
// 注意1: 使用的是小于号
// 注意2: 使用的是const vector<int>& a,而不是vector<int> a 作为传入变量(好处是不用再创建新的vector并赋值,可以节省运行时间)
static bool p_cmp(const vector<int>& a, const vector<int>& b) {
    // if (a[0] == b[0]) return a[1] < b[1];
    return a[0] < b[0];
}

sort(points.begin(), points.end(), p_cmp);

9. max和min函数的使用

// 如果定义了min和max变量,就不能再使用max(a, b)函数了,比如
int min = 0, max = 1;
max = max(min, max);
// 会报错,信息是不能识别max()函数,所以最好不要定义名称为max和min的变量

10. 不再使用push_back一个一个插入

vec.resize(int n, element); //表示调整容器vec的大小为n,扩容后的每个元素的值为element,默认为0

// 举例:
vector<int> tmp;
tmp.resize((int)nums.size(), 0);

刷题

0. 排序

排序算法 (C++)

写在前面

  1. 本文为自己实现的排序算法的简单总结(只有简单的思想 / 思路概括,没有详细的原理分析),主要用于复习,适合于有一定数据结构基础的coder。
  2. 持续更新中…(目前只完成了冒泡,选择,插入,希尔,快速,归并;还需要完成的有堆,桶,计数)
  3. 最后给出了一个可以运行的.cpp代码,包含所有的排序函数,可以复制后测试。
算法名称特点(思想)时间复杂度空间复杂度稳定性经典场景
[冒泡](#1. 冒泡排序)O(n^2)O(1)稳定
[选择](#2. 选择排序)每轮选择一个最小的O(n^2)O(1)不稳定
[插入](#3. 插入排序)将最后一个元素插入到有序序列中O(n^2)O(1)稳定
[希尔](#4. 希尔排序)多轮插入排序,以gap为间隔,gap初始化为n / 2,每轮gap /= 2平均O(n^1.3)O(1)不稳定
[快速](#5. 快速排序)选择一个privot,让它左边比它小,右边比它大;递归O(nlog(n))O(1)不稳定
[归并](#6. 归并排序)参照数组归并的思想,递归O(nlog(n))O(n)稳定
[堆](#7. 堆排序)O(nlog(n))不稳定
[计数](#8. 计数排序)构建一个max - min大小的count数组(保存每个数字的个数)O(n + k)O(k)稳定范围小,但是数据量大(比如100万个学生,0~500分,输入一个分数,请给出排名)
[桶(基数)](#9. 基数排序(桶))按照个位,十位,百位…排序O (k*(n + r))O(n + r)稳定

0. 公共函数

一个swap函数,一个print(打印数组)函数

// 公共函数,交换+打印数组
void swap(int &a, int &b) {
    int tmp = a;
    a = b;
    b = tmp;
}
void print_arr(vector<int>& arr) {
    for (auto e : arr) {
        cout << e << " ";
    }
    cout << endl;
}

1. 冒泡排序

void BubbleSort() {
    for (int i = 0; i < n; ++i) {
		for (int j = 0; j < n - i - 1; ++j) {
			if (arr[j] > arr[j + 1]) swap(arr[j], arr[j + 1]);
		}
	}
}

2. 选择排序

每轮选择最小的

// 选择排序 (每次选择最小值)
int FindMinPos(vector<int>& arr, int start_pos) {
    int min_pos = start_pos, min_value = arr[start_pos];
    int n = arr.size();
    for (int i = start_pos + 1; i < n; ++i) {
        if (arr[i] < min_value) {
            min_pos = i;
            min_value = arr[i];
        }
    }
    return min_pos;
}
void SelectSort(vector<int> arr) {
    int n = arr.size();
    for (int i = 0; i < n; ++i) {
        int min_pos = FindMinPos(arr, i);
        if (min_pos != i) {
            swap(arr[min_pos], arr[i]);
        }
    }
    cout << "select sort: ";
    print_arr(arr);
}

3. 插入排序

每轮将最后一个元素插入到有序序列中

// 插入排序(将无序序列插入到有序序列中)
// 每次循环中,最后一个是无序的,前面的都是有序的,只需要移动这个无序元素就行
void InsertSort(vector<int> arr) {
    int n = arr.size();
    for (int i = 1; i < n; ++i) {
        for (int j = i; j > 0; --j) {
            if (arr[j] < arr[j - 1]) swap(arr[j], arr[j - 1]);
        }
    }
    cout << "insert sort: ";
    print_arr(arr);
}

4. 希尔排序

多次选择排序

// 希尔排序
// 先分组,组内排序,然后分组的gap /= 2
// 其实是多轮插入排序,只不过交换的步长变成gap
void ShellSort(vector<int> arr) {
    int n = arr.size();
    // gap为步长InsertSort()几乎完全一样,只不过用gap替换了原来的1 (0, 即1 - 1,替换为gap - 1)
    for (int gap = n / 2; gap >= 1; gap /= 2) {
        // 这里的代码和
        for (int i = gap; i < n; ++i) {
            for (int j = i; j > gap - 1; j -= gap) {
                if (arr[j] < arr[j - gap]) swap(arr[j], arr[j - gap]);
            }
        }
    }
    cout << "shell sort:  ";
    print_arr(arr);
}

5. 快速排序

5.1 快速排序基础版

每次选择一个数字,让它左边比它小,右边比它大(递归)

时间复杂度:基于随机选取主元的快速排序**时间复杂度为期望O(nlogn)——但是最坏情况下是O(n^2) ( 原本就有序 / 倒序的时候) **

空间复杂度:O(h),其中 h为快速排序递归调用的层数。我们需要额外的O(h) 的递归调用的栈空间,由于划分的结果不同导致了快速排序递归调用的层数也会不同,最坏情况下需 O(n) 的空间,最优情况下每次都平衡,此时整个递归树高度为logn,空间复杂度为 O(logn)。

下面包括两个quick sort函数,一个是基础版本,另一个是radom版本(可以保证时间复杂度是O(nlogn)而不是O(n^2))—— 随机选取pivot的位置。

// 快速排序
// 每次找到一个基准,分成两份,让左边的都比它小,右边都比它大
int Partition(vector<int> &arr, int low, int high) {
    int pivot_value = arr[low];
    while (low < high) {
        while (low < high && arr[high] >= pivot_value) --high;
        arr[low] = arr[high];
        while(low < high && arr[low] <= pivot_value) ++low;
        arr[high] = arr[low];
    }
    arr[low] = pivot_value;
    return low;
}

void QuickSort(vector<int>& arr, int low, int high) {
    if (low < high) {
        int pivot_pos = Partition(arr, low, high);
        QuickSort(arr, low, pivot_pos - 1);
        QuickSort(arr, pivot_pos + 1, high);
    }
    // cout << "quick sort:  ";
    // print_arr(arr);
}

// 为了不改变原来nums数组,这里我建立一个函数调用QuickSort
void call_QuickSort(vector<int> arr) {
    QuickSort(arr, 0, arr.size() - 1);
    cout << "quick sort:  ";
    print_arr(arr);
}
5.2 快速排序Random

前面已经分析过,当原本数组有序 / 倒序的时候,时间复杂度和空间复杂度都比较搞,这是因为pivot的选择,总是选择第一个,这导致了一旦数组有序 / 倒顺,就变成了一个冒泡排序。

——因此改变策略,不再是以arr[low] 为 pivot,改为直接random 选择。

radomized_quick_sort:


6. 归并排序

数组归并的思想,递归

注意这里的vec赋值操作vector<int> aux_arr(arr.begin(), arr.end());,并不是引用,而是新建后赋值,详情见C++ vector的赋初值是引用了原来的vector还是新建vector?

// 归并排序
// 2路归并:先22排列有序,然后44,然后88,知道全局有序
void Merge(vector<int>& arr, int low, int mid, int high) {
    // 临时数组(注意这里的赋值操作,并不是引用,而是新建后赋值)
    vector<int> aux_arr(arr.begin(), arr.end());

    int i = low;
    int j = mid + 1;

    // 经典的归并数组(两个需要归并的有序数组分别为aux_arr的low ~ mid和aux_arr的mid+1 ~ high,归并后的数组为arr的low ~ high)
    for (int i = low, k = low, j = mid + 1; i <= mid && j <= high; ++k) {
        if (aux_arr[i] <= aux_arr[j]) {
            arr[k] = aux_arr[i++];
        } else {
            arr[k] = aux_arr[j++];
        }

        // 一个走完,接下来就是直接复制
        while (i <= mid) arr[k] = aux_arr[i++];
        while (j <= high) arr[k] = aux_arr[j++];
    }

}
void MergeSort(vector<int>& arr, int low, int high) {
    if (low < high) {
        int mid = (low + high) / 2;
        MergeSort(arr, low, mid);
        MergeSort(arr, mid + 1, high);
        Merge(arr, low, mid, high);
    }
}
void call_MergeSort(vector<int> arr) {
    MergeSort(arr, 0, arr.size() - 1);
}

7. 堆排序

8. 计数排序

经典例子:100万个学生,满分500分,输入一个分数,输出对应排名——适合数据范围小,但是数据量大的场景

创建一个max - min 大小的count数组,存储每个数字出现的个数。

时间复杂度O(n + k), 空间复杂度O(k),(其中k = max - min) 稳定。

——缺点

  • 只能做整数
  • max - min差距大的时候,空间复杂度太高
// 计数排序,创建一个max - min 大小的count数组,存储每个数字出现的个数
// find_min_and_max_for_count_sort:找到最大最小值
void find_min_and_max_for_count_sort(vector<int> &arr, int &min, int &max) {
    int n = arr.size();

    for (int i = 0; i < n; ++i) {
        if (arr[i] > max) max = arr[i];
        if (arr[i] < min) min = arr[i];
    }
}
void CountSort(vector<int> arr) {
    int min_num = INT32_MAX, max_num = INT32_MIN;
    find_min_and_max_for_count_sort(arr, min_num, max_num);
    
    vector<int> cnt(max_num - min_num + 1, 0);
    int n = arr.size();

    // 填充cnt数组
    for (int i = 0; i < n; ++i) {
        ++cnt[arr[i] - min_num];
    }

    // 遍历cnt数组,重新赋值给arr
    int arr_index = 0;
    for (int i = 0; i < max_num - min_num + 1; ++i) {
        while (cnt[i] > 0) {
            arr[arr_index++] = i + min_num;
            --cnt[i];
        }
    }

    cout << "count sort: ";
    print_arr(arr);
} 

9. 基数排序(桶)

思路很简单,就是创建10个桶,先排个位数(个位数相同的放到一个桶中),然后收集起来,再排十位数,再排百位数…

时间复杂度: O (k*(n + r)) ;空间复杂度O(n + r); (r是桶的大小,k是可以分成的基数的个数) 稳定性:稳定。

——当元素取值范围较大,但元素个数较少时可以利用基数排序

// 基数(桶)排序 radix sort / bucket sort
// bucket sort的辅助函数:求bucket sort的最大轮数(即最高位有多少位)
int bucket_sort_max_round(vector<int> &arr) {
    int max_r = 0;
    int n = arr.size();
    int tmp = arr[0];
    for (int i = 0; i < n; ++i) {
        if (arr[i] > tmp) tmp = arr[i];
    }

    while (tmp) {
        tmp /= 10;
        ++max_r;
    }

    return max_r;
}
void BucketSort(vector<int> arr) {
    int r = bucket_sort_max_round(arr); // 需要的轮数
    int n = arr.size();
    vector<vector<int>> bucket(10, vector<int>(n, 0)); // 10个桶子
    vector<int> bucket_size(10, 0); // 记录每个桶中有多少元素


    for (int i = 0; i < r; ++i) {
        // 每轮开始的时候每个桶的计数清零 (这里不必对桶进行清零,因为保存了桶中当前个数,并且每轮都会覆盖,所以不需要清零)
        for (int k = 0; k < 10; ++k) {
            bucket_size[k] = 0;
        }

        // 放入当前桶中
        for (int j = 0; j < n; ++j) {
            int cur_radix = (arr[j] % (int)pow(10, i + 1)) / (int)pow(10, i);
            bucket[cur_radix][bucket_size[cur_radix]] = arr[j];
            ++bucket_size[cur_radix];
        }

        // 一轮结束 —— 写回到arr中
        int arr_index = 0;
        for (int k = 0; k < 10; ++k) { 
            for (int l = 0; l < bucket_size[k]; ++l) {
                arr[arr_index++] = bucket[k][l];
            }
        }

    }

    cout << "radix (bucket) sort: ";
    print_arr(arr);
}

1. 二分查找

278. 第一个错误的版本

特别注意:(r - l) >> 1 + l的写法是完全错误的因为这样相当于(r - l) >> (1 + l)

例:寻找排序数组的左右边界:34. 在排序数组中查找元素的第一个和最后一个位置

class Solution {
public:
     vector<int> searchRange(vector<int>& nums, int target) {
        int n = nums.size();
        vector<int> ans(2, -1);
        if (n == 0) return ans;
        
        u_int l = 0, r = n - 1;
        u_int mid = 0;
        while (l < r) {
            //int mid = l + r >> 1;
			  // 这里可能溢出,因此改成 mid = l + (r - l) / 2;
            // mid  = (l + r) >> 1;
            mid = l + (r - l) / 2;
            if (nums[mid] >= target) r = mid;
            else l = mid + 1;
        }
        if (nums[r] != target) return ans;
        ans[0] = r;
        l = 0, r = n - 1;
        while (l < r) {
            int mid = (l + r + 1) >> 1;
            if (nums[mid] <= target) l = mid;
            else r = mid - 1;
        }
        ans[1] = r;

        return ans;
    }
};

2. 哈希(好处就是查找时间复杂度为1)

1.主要就是利用

unordered_map
unordered_set

set, map等都是红黑树实现的

小trick:可以直接根据vector nums;建立哈希

(leetcode 349.两个数组的交集)。注意下面,在数组上建立哈希,以及将unorded_set转成vector输出

class Solution {
public:
  vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
    unordered_set<int> result_set; // 存放结果
    unordered_set<int> nums_set(nums1.begin(), nums1.end());
    for (int num : nums2) {
      // 发现nums2的元素 在nums_set里又出现过
      if (nums_set.find(num) != nums_set.end()) {
        result_set.insert(num);
      }
    }
    return vector<int>(result_set.begin(), result_set.end());
  }
};

count(); // 计数(即用来查找是否有这个键值)
erase(); //删除
insert(); //插入
emplace(); //构造并插入

2. 在C++中,set 和 map 分别提供以下三种数据结构,其底层实现以及优劣如下表所示:

集合底层实现是否有序数值是否可以重复能否更改数值查询效率增删效率
std::set红黑树有序 O ( log ⁡ n ) O(\log n) O(logn) O ( log ⁡ n ) O(\log n) O(logn)
std::multiset红黑树有序 O ( log ⁡ n ) O(\log n) O(logn) O ( log ⁡ n ) O(\log n) O(logn)
std::unordered_set哈希表无序 O ( 1 ) O(1) O(1) O ( 1 ) O(1) O(1)

std::unordered_set底层实现为哈希表,std::set 和std::multiset 的底层实现是红黑树,红黑树是一种平衡二叉搜索树,所以key值是有序的,但key不可以修改,改动key值会导致整棵树的错乱,所以只能删除和增加。

映射底层实现是否有序数值是否可以重复能否更改数值查询效率增删效率
std::map红黑树key有序key不可重复key不可修改 O ( log ⁡ n ) O(\log n) O(logn) O ( log ⁡ n ) O(\log n) O(logn)
std::multimap红黑树key有序key可重复key不可修改 O ( log ⁡ n ) O(\log n) O(logn) O ( log ⁡ n ) O(\log n) O(logn)
std::unordered_map哈希表key无序key不可重复key不可修改 O ( 1 ) O(1) O(1) O ( 1 ) O(1) O(1)

3. 不要忘记数组(先排序) + 双指针,(比如17.三数之和, 18.四数之和)

利用排序数组的单调性,去重。利用双指针,减少时间复杂度。

4. 参考题目

496. 下一个更大元素 I – hash + 单调栈

5. 自定义数据结构使用unordered_set / unordered_map

// hash函数(用fp1来代替)
struct TupleHasher {
  std::size_t operator()(const SHA1FP &key) const { return key.fp1; }
};

// 用来确定最终相等的数据结构
struct TupleEqualer {
  bool operator()(const SHA1FP &lhs, const SHA1FP &rhs) const {
    return (lhs.fp1 == rhs.fp1) && (lhs.fp2 == rhs.fp2) &&
           (lhs.fp3 == rhs.fp3) && (lhs.fp4 == rhs.fp4);
  }
};

unordered_set<SHA1FP, TupleHasher, TupleEqualer>;
    
std::unordered_map<uint64_t, std::unordered_set<SHA1FP, TupleHasher, TupleEqualer>>

3. 字符串

1. 反转,删除操作

reverse(str.begin(), str.end());
erase(str.begin(), str.end());
erase (str.begin());

char c = s.charAt(i);

2. 字符串 / 数组 旋转

1. 左旋:剑指 Offer 58 - II. 左旋转字符串

比如,输入字符串"abcdefg"和数字2,该函数将返回左旋转两位得到的结果"cdefgab"。

class Solution {
public:
    string reverseLeftWords(string s, int n) {
        reverse(s.begin(), s.end());
        reverse(s.end() - n, s.end());
        reverse(s.begin(), s.end() - n);
        return s;
    }
};
2. 右旋:189. 轮转数组
class Solution {
public:
    void rotate(vector<int>& nums, int k) {
        k = k % nums.size();
        reverse(nums.begin(), nums.end());
        reverse(nums.begin(), nums.begin() + k);
        reverse(nums.begin() + k, nums.end());

    }
};
3. 反转单词 151. 颠倒字符串中的单词

——先反转整个字符串,然后反转所有单词。

3. KMP算法

KMP的经典思想就是:当出现字符串不匹配时,可以记录一部分之前已经匹配的文本内容,利用这些信息避免从头再去做匹配。例题:28 实现 strStr(); 459 重复的子字符串-- 还没做。

前缀:包含首字母,但是不包含尾字母的所有子串

后缀:包含尾字母,但是不包含首字母的所有子串

4. 数组

1. 求和

accumulate(nums.begin(), nums.end(), 0);

2. 排序

// 按照升序排列
sort(nums.begin(), nums.end());

sort(nums.begin(), nums.end(), std::less<int>);
sort(nums.begin(), nums.end(), std::greater<int>);

// 按照自己定义规则排列(注意要用static bool类型)
// 注意如果传int a, int b,那么需要复制,造成额外的时间开销
// 比如https://leetcode.cn/problems/minimum-number-of-arrows-to-burst-balloons/submissions/的提交记录
static bool abs_cmp(int &a, int &b){
    // 按照绝对值,降序排列
    return abs(a) > abs(b);
}
sort(nums.begin(), nums.end(), abs_cmp);

3. 二维数组初始化

vector<vector<int>> ans(m, vector<int>(n, 0));

4. 将一个数组赋值给另一个数组 – 注意,修改v2后v1同样会发生变化

vector<int> v1(v2);

但是以下的赋值方式,修改v2后v1均不会发生变化

vector<int> v2(v1.begin(), v1.end());

vector<int> v2 = v1;

5. 螺旋矩阵II

59. 螺旋矩阵 II

给你一个正整数 n ,生成一个包含 1 到 n2 所有元素,且元素按顺时针顺序螺旋排列的 n x n 正方形矩阵 matrix 。

class Solution {
public:
    vector<vector<int>> generateMatrix(int n) {
        int t = 0;      // top
        int b = n-1;    // bottom
        int l = 0;      // left
        int r = n-1;    // right
        vector<vector<int>> ans(n,vector<int>(n));
        int cnt = 1;
        while(cnt <= n*n){
            for(int i = l; i <= r; ++i, ++cnt) ans[t][i] = cnt;
            ++t;
            for(int i = t; i <= b; ++i, ++cnt) ans[i][r] = cnt;
            --r;
            for(int i = r; i >= l; --i, ++cnt) ans[b][i] = cnt;
            --b;
            for(int i = b; i >= t; --i, ++cnt) ans[i][l] = cnt;
            ++l;
        }
        return ans;
    }
};

6. 前缀和

——适用情况:连续子数组的和,满足XX 条件

6.1 560. 和为 K 的子数组 —— 前缀和 + 哈希表

class Solution {
public:
// 想法:前缀和?
// 看了题解:用前缀和和哈希表来做,prefix_sum[i] - k在哈希表中是否存在,如果存在就++cnt
    int subarraySum(vector<int>& nums, int k) {
        unordered_map<int, int> mp;
        mp[0] = 1;
        int count = 0, pre = 0;
        for (auto& x:nums) {
            pre += x;
            if (mp.find(pre - k) != mp.end()) {
                count += mp[pre - k];
            }
            mp[pre]++;
        }
        return count;
    }
};

5. 链表

1. 设计链表(注意c++写法)

class MyLinkedList {
public:
    // 定义链表节点结构体
    struct LinkedNode {
        int val;
        LinkedNode* next;
        LinkedNode(int val):val(val), next(nullptr){}
    };

    // 初始化链表
    MyLinkedList() {
        _dummyHead = new LinkedNode(0); // 这里定义的头结点 是一个虚拟头结点,而不是真正的链表头结点
        _size = 0;
    }

    // 获取到第index个节点数值,如果index是非法数值直接返回-1, 注意index是从0开始的,第0个节点就是头结点
    int get(int index) {
        if (index > (_size - 1) || index < 0) {
            return -1;
        }
        LinkedNode* cur = _dummyHead->next;
        while(index--){ // 如果--index 就会陷入死循环
            cur = cur->next;
        }
        return cur->val;
    }

    // 在链表最前面插入一个节点,插入完成后,新插入的节点为链表的新的头结点
    void addAtHead(int val) {
        LinkedNode* newNode = new LinkedNode(val);
        newNode->next = _dummyHead->next;
        _dummyHead->next = newNode;
        _size++;
    }

    // 在链表最后面添加一个节点
    void addAtTail(int val) {
        LinkedNode* newNode = new LinkedNode(val);
        LinkedNode* cur = _dummyHead;
        while(cur->next != nullptr){
            cur = cur->next;
        }
        cur->next = newNode;
        _size++;
    }

    // 在第index个节点之前插入一个新节点,例如index为0,那么新插入的节点为链表的新头节点。
    // 如果index 等于链表的长度,则说明是新插入的节点为链表的尾结点
    // 如果index大于链表的长度,则返回空
    void addAtIndex(int index, int val) {
        if (index > _size) {
            return;
        }
        LinkedNode* newNode = new LinkedNode(val);
        LinkedNode* cur = _dummyHead;
        while(index--) {
            cur = cur->next;
        }
        newNode->next = cur->next;
        cur->next = newNode;
        _size++;
    }

    // 删除第index个节点,如果index 大于等于链表的长度,直接return,注意index是从0开始的
    void deleteAtIndex(int index) {
        if (index >= _size || index < 0) {
            return;
        }
        LinkedNode* cur = _dummyHead;
        while(index--) {
            cur = cur ->next;
        }
        LinkedNode* tmp = cur->next;
        cur->next = cur->next->next;
        delete tmp;
        _size--;
    }

    // 打印链表
    void printLinkedList() {
        LinkedNode* cur = _dummyHead;
        while (cur->next != nullptr) {
            cout << cur->next->val << " ";
            cur = cur->next;
        }
        cout << endl;
    }
private:
    int _size;
    LinkedNode* _dummyHead;
};

2. 反转链表

(1) 双指针
img
class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        // 想法,每次都剪断一个链,同时连接一个链
        ListNode *cur, *pre, *tmp;

        cur = head;
        pre = nullptr;

        while (cur){
            tmp = cur -> next;
            cur -> next = pre;
            
            pre = cur;
            cur = tmp;
        }

        return pre;
    }
};
(2) 递归写法 —— 和双指针写法完全一样
class Solution {
public:
    ListNode* reverse(ListNode* pre,ListNode* cur){
        if(cur == NULL) return pre;
        ListNode* temp = cur->next;
        cur->next = pre;
        return reverse(cur,temp);
    }
    ListNode* reverseList(ListNode* head) {
        // 和双指针法初始化是一样的逻辑
        return reverse(NULL, head);
    }

};

3. 环形链表(非暴力),看接解析

4. 典型题目(反转 + 链表合并等)

143. 重排链表

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
// 想法:找到中间节点,然后把后半部分reverse,然后就类似于合并链表了

    ListNode* reverseList(ListNode* h) {
        // ListNode* dummy_h = new ListNode(0);
        // dummy_h -> next = h;

        ListNode* pre, *cur, *tmp;
        pre = h;
        cur = h -> next;
        h -> next = nullptr;

        while (cur) {
            tmp = cur -> next;
            cur -> next = pre;
            pre = cur;
            cur = tmp;
        }   

        return pre;
    }
    void mergeLists(ListNode* &l1, ListNode* &l2) {
        ListNode* tmp, *cur1 = l1, *cur2 = l2;

        while (cur2 && cur1) {
            tmp = cur2 -> next;
            cur2 -> next = cur1 -> next;
            cur1 -> next = cur2;
            cur1 = cur1 -> next -> next;
            cur2 = tmp;
        }
    }
    void printList(ListNode *h) {
        ListNode* cur_ptr = h;
        while (cur_ptr) {
            cout << cur_ptr -> val << " ";
            cur_ptr = cur_ptr -> next;
        }
        cout << endl;
    }
    void reorderList(ListNode* head) {
        if (head -> next == nullptr) return;

        // 快慢指针找到中间节点
        ListNode* fast = head, *slow = head;
        while (fast && fast -> next && fast -> next -> next) {
            fast = fast -> next -> next;
            slow = slow -> next;
        }

        // 分出两个链表(如果是奇数个,那么L1的个数会多一个)
        ListNode* L1 = head;
        ListNode* L2 = slow -> next;
        slow -> next = nullptr;

        // reverse L2
        L2 = reverseList(L2);

        // printList(L1);
        // printList(L2);

        // merge L1 & L2
        mergeLists(L1, L2);
    }
};

5. 合并两个有序链表

21. 合并两个有序链表

(1) 迭代(解答略)
(2) 递归

合并(L1, L2) 等价于,L1 -> next = Merge(L1 -> next, L2);

class Solution {
public:
    ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
        if (!list1) return list2;
        if (!list2) return list1;

        if (list1 -> val <= list2 -> val) {
            list1 -> next = mergeTwoLists(list1 -> next, list2);
            return list1;
        } else {
            list2 -> next = mergeTwoLists(list1, list2 -> next);
            return list2;
        }
    }
};

6. 寻找两个链表的交点(是否相交)

6.1. 王道数据结构,两个链表长度,然后长的链表,先走m - n步,然后curA和curB每次都等于next
6.2. 同上,只不过,不需要提前计算长度,而是:A走完,curA = headB, B走往,curB = headA,这样就保证了等长。

每步操作需要同时更新指针pA 和 pB。

  • 如果指针 pA 不为空,则将指针pA 移到下一个节点;如果指针pB 不为空,则将指针pB 移到下一个节点。

  • 如果指针 pA 为空,则将指针pA 移到链表 headB 的头节点;如果指针pB 为空,则将指针pB 移到链表headA 的头节点。

  • 当指针pA 和 pB 指向同一个节点或者都为空时,返回它们指向的节点或者null。

public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        if (headA == null || headB == null) {
            return null;
        }
        ListNode pA = headA, pB = headB;
        while (pA != pB) {
            pA = pA == null ? headB : pA.next;
            pB = pB == null ? headA : pB.next;
        }
        return pA;
    }
}

7. 环形链表的入口节点

142. 环形链表 II

7.1 hash 略
7.2 快慢指针

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CsKXz69m-1658928266693)(https://raw.githubusercontent.com/LEVI-Tempset/picgo-pics-for-typora/main/img20210318162938397.png)]

(x + y) * 2 = x + y + n (y + z)

x = (n - 1) (y + z) + z

也就是,相遇(p_fast == p_slow)后,让cur1从head开始,cur2从p_fast开始,每次走一步,当cur1 == cur2时,就是环的入口了

6. 栈与队列

1. 基础知识

栈提供push 和 pop 等接口,所有元素必须符合先进后出规则,所以栈不提供走访功能,也不提供迭代器(iterator)。 不像是set 或者map 提供迭代器iterator来遍历所有元素。栈是以底层容器完成其所有的工作,对外提供统一的接口,底层容器是可插拔的(也就是说我们可以控制使用哪种容器来实现栈的功能)。

栈的内部结构,栈的底层实现可以是vector,deque,list 都是可以的, 主要就是数组和链表的底层实现。

栈与队列理论3

我们常用的SGI STL,如果没有指定底层实现的话,默认是以deque为缺省情况下栈的低层结构。

std::stack<int, std::vector<int>> third; // 使用vector为底层容器的栈

队列中先进先出的数据结构,同样不允许有遍历行为,不提供迭代器, SGI STL中队列一样是以deque为缺省情况下的底部结构。

也可以指定list 为起底层实现,初始化queue的语句如下:

std::queue<int, std::list<int>> third; // 定义以list为底层容器的队列

2. 基本操作

s.push(x);
int size = s.size();
s.empty();

// 出栈并获取元素值:
int out = s.top();
s.pop()
    
// 插入元素
int a = 1;
que.push(a);

// 队列:
int head = q.front();
q.pop();
int tail = q.back();
int size = q.size();

3. 字符串的push_back和pop_back

1047. 删除字符串中的所有相邻重复项

class Solution {
public:
    string removeDuplicates(string s) {
        string stk;
        for (char ch : s) {
            if (!stk.empty() && stk.back() == ch) {
                stk.pop_back();
            } else {
                stk.push_back(ch);
            }
        }
        return stk;
    }
};

4. 后缀和中缀表达算式

逆波兰表达式(后缀表达式)150. 逆波兰表达式求值

—遇到数字,入栈,遇到符号,出栈两个数字,运算后结果入栈

我们平时都是中缀表达式。

5. 优先队列:堆(默认是大根堆)

#include  <queue>
priority_queue<int> heap;
std::priority_queue<int, std::vector<int>, std::greater<int>> small_top_heap;

// 自定义类型
struct cmp {
  	bool operator()(const pair<int, int> &a, const pair<int, int> &b) {
        return a.second < b.second;
    }
};
priority_queue<pair<int, int>, vector<pair<int, int>>, cmp> my_heap;

和队列基本操作相同:

​ · top 访问队头元素

​ · empty 队列是否为空

​ · size 返回队列内元素个数

​ · push 插入元素到队尾 (并排序)

​ · emplace 原地构造一个元素并插入队列

​ · pop 弹出队头元素

​ · swap 交换内容

例题:239. 滑动窗口最大值

记录大小的同时记录位置,当已经超出当前位置,那么就弹出,反之就记录

class Solution {
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        int n = nums.size();
        vector<int> ans;
        // 其实这是一个经典的用heap的题目
        priority_queue<pair<int, int>> heap;
        for (int i = 0; i < k; ++i){
            heap.emplace(nums[i], i);
        }
        ans.push_back(heap.top().first);
        for (int i = k; i < n; ++i){
            heap.emplace(nums[i], i);
            while (heap.top().second <= i - k){
                heap.pop();
            }
            ans.push_back(heap.top().first);
        }
        return ans;
    }  
};

6. 栈模拟队列

两个栈,一个in_stack,一个out_stack。

  • push的时候全都push到in_stack

  • pop的时候分两种情况:

    • 如果out_stack非空,那么out_stack.pop()
    • 如果out_stack为空,那么首先将in_stack全都pop到out_stack,然后继续pop

7. 队列模拟栈

一个队列模拟栈:pop()的时候,直接将前面n - 1个元素全部出队(同时插入到que中),然后出队最后一个元素即可

    int pop() {
        int size = que.size();
        size--;
        while (size--) { // 将队列头部的元素(除了最后一个元素外) 重新添加到队列尾部
            que.push(que.front());
            que.pop();
        }
        int result = que.front(); // 此时弹出的元素顺序就是栈的顺序了
        que.pop();
        return result;
    }

7. 二叉树

struct TreeNode {
    int val;
    TreeNode *left;
    TreeNode *right;
    TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};

1. 完全二叉树,满二叉树,二叉搜索树(Binary Search Tree, BST,左子树的节点 < 根节点 < 右子树),平衡二叉搜索树(AVL)

二叉树的遍历方式:

  • 深度优先遍历

    • 前序遍历(递归法,迭代法)(根左右)
    • 中序遍历(递归法,迭代法)(左根右)
    • 后序遍历(递归法,迭代法)(左右根)
  • 广度优先遍历

    • 层次遍历(迭代法)

其中:

  • 前序遍历:中左右

  • 中序遍历:左中右

  • 后序遍历:左右中

现在已经讲过了几种二叉树了,二叉树,二叉平衡树,完全二叉树,二叉搜索树,后面还会有平衡二叉搜索树。 那么一些同学难免会有混乱了,我针对如下三个问题,帮大家在捋顺一遍:

(1) 平衡二叉搜索树是不是二叉搜索树和平衡二叉树的结合?

​ 是的,是二叉搜索树和平衡二叉树的结合。

(2) 平衡二叉树与完全二叉树的区别在于底层节点的位置

​ 完全二叉树底层必须是从左到右连续的,且次底层是满的。

(3) 堆是完全二叉树和排序的结合,而不是平衡二叉搜索树

​ 堆是一棵完全二叉树,同时保证父子节点的顺序关系(有序)。
完全二叉树一定是平衡二叉树,堆的排序是父节点大于子节点,而搜索树是父节点大于左孩子,小于右孩子,所以堆不是平衡二叉搜索树。

2. 层序遍历(主要是队列的使用,pop,front等,精髓是size的使用来标记层数(其实就是记住本层的节点个数))

class Solution {
public:
    vector<vector<int>> levelOrder(TreeNode* root) {
        queue<TreeNode*> que;
        if (root != NULL) que.push(root);
        vector<vector<int>> result;
        while (!que.empty()) {
            int size = que.size();
            vector<int> vec;
            // 这里一定要使用固定大小size,不要使用que.size(),因为que.size是不断变化的
            for (int i = 0; i < size; i++) {
                TreeNode* node = que.front();
                que.pop();
                vec.push_back(node->val);
                if (node->left) que.push(node->left);
                if (node->right) que.push(node->right);
            }
            result.push_back(vec);
        }
        return result;
    }
};

3. 递归构造树

106. 从中序与后序遍历序列构造二叉树

4. 验证二叉搜索树

98. 验证二叉搜索树

二叉搜索树的性质:中序遍历是有序的

5. 二叉树的最近公共祖先

236. 二叉树的最近公共祖先 or 剑指 Offer 68 - II. 二叉树的最近公共祖先

利用后续遍历的特点,left,right,root

    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        if (root == p || root == q || !root) return root;
        TreeNode* left = lowestCommonAncestor(root->left, p, q);
        TreeNode* right = lowestCommonAncestor(root->right, p, q);
        if (left != NULL && right != NULL) return root;

        if (left == NULL && right != NULL) return right;
        else if (left != NULL && right == NULL) return left;
        else  { //  (left == NULL && right == NULL)
            return NULL;
        }
    }

6. 删除二叉搜索树的节点(画图解决就行)

关键在于,递归的删除,并不需要记住被删除节点的前一个节点。此外,代码随想录的删除空间操作可以学习。(先用一个tmpNode记住,然后delete,然后return tmpNode,注意free(root)不可以,但是delete root可以)

7. 树形结构的遍历 + 状态转移

968. 监控二叉树 – 后续遍历 + 状态转移

8. 二叉树的后续遍历

——好处:可以获得子树传递过来的信息,也就是当需要子树的时候,可以用这个

543. 二叉树的直径 labuladong 题解思路

class Solution {
private:
    int ans = 0;
    int maxDepth(TreeNode* rt) {
        if (!rt) return 0;
        int left_max_depth = maxDepth(rt -> left);
        int right_max_depth = maxDepth(rt -> right);
        int cur_max_diameter = left_max_depth + right_max_depth;
        if (ans < cur_max_diameter) ans = cur_max_diameter;
        return 1 + max(left_max_depth, right_max_depth);
    }
public:
// 想法:是不是就是左右子树的最大深度之和? —— 这样太浪费了,每个节点都遍历自己的所有子树。如何优化?
// ————————————————————后序遍历(从子树往根传递信息)
    int diameterOfBinaryTree(TreeNode* root) {
        maxDepth(root);
        return ans;
    }
};

9. 二叉树的非递归遍历

9.1 非递归——前序遍历 144. 二叉树的前序遍历
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;
    }
};
9.2 非递归——中序遍历 94. 二叉树的中序遍历
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;
    }
};
9.3 非递归——后序遍历 145. 二叉树的后序遍历
class Solution {
public:
    vector<int> postorderTraversal(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->left) st.push(node->left); // 相对于前序遍历,这更改一下入栈顺序 (空节点不入栈)
            if (node->right) st.push(node->right); // 空节点不入栈
        }
        reverse(result.begin(), result.end()); // 将结果反转之后就是左右中的顺序了
        return result;
    }
};

8. 回溯算法

1. 回溯简介

回溯的本质是穷举,穷举所有可能,然后选出我们想要的答案,如果想让回溯法高效一些,可以加一些剪枝的操作,但也改不了回溯法就是穷举的本质。

完全就是下面的模板

void backtracking(参数) {
    if (终止条件) {
        存放结果;
        return;
    }

    for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
        处理节点;
        backtracking(路径,选择列表); // 递归
        回溯,撤销处理结果
    }
}

// 比如77.组合问题
    vector<vector<int>> res;
    vector<int> path;
    void backTracking(int n, int k, int start_index) {
        if (path.size() == k) {
            res.push_back(path);
            return;
        }
        // 剪枝优化
        // for (int i = start_index; i <= n; ++i) {
        for(int i = start_index; i <= n - (k - path.size()) + 1; ++i) {
            path.push_back(i);
            // 如果这里用i而不是i+1,说明可以重复使用多次
            backTracking(n, k, i + 1); // 递归
            path.pop_back(); // 回溯,撤销处理的节点
        }
    }

    vector<vector<int>> combine(int n, int k) {
        backTracking(n, k, 1);
        return res;
    }

2. 回溯法解决的问题

  • 组合问题:N个数里面按一定规则找出k个数的集合
  • 切割问题:一个字符串按一定规则有几种切割方式
  • 子集问题:一个N个数的集合里有多少符合条件的子集
  • 排列问题:N个数按一定规则全排列,有几种排列方式
  • 棋盘问题:N皇后,解数独等等

3. 组合问题:递归和剪枝(画图)

77.组合

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FWee2rcM-1658928266695)(https://gitee.com/LEVI-Tempest/picgo-typora/raw/master/20210130194335207.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rSDZDyp5-1658928266695)(F:\Typora_Figures\笔试 + 面试\20210130194335207-16543502744395.png)]

4. 关于start_index

本题还需要startIndex来控制for循环的起始位置,对于组合问题,什么时候需要startIndex呢?

我举过例子,如果是一个集合来求组合的话,就需要startIndex,例如:77.组合 (opens new window)216.组合总和III (opens new window)

如果是多个集合取组合,各个集合之间相互不影响,那么就不用startIndex,例如:17.电话号码的字母组合

注意以上我只是说求组合的情况,如果是排列问题,又是另一套分析的套路,后面我再讲解排列的时候就重点介绍

5. 切割回文子串

113.分割回文串 (同93. 复原 IP 地址)

class Solution {
private:
    vector<vector<string>> result;
    vector<string> path; // 放已经回文的子串
    void backtracking (const string& s, int startIndex) {
        // 如果起始位置已经大于s的大小,说明已经找到了一组分割方案了
        if (startIndex >= s.size()) {
            result.push_back(path);
            return;
        }
        for (int i = startIndex; i < s.size(); i++) {
            if (isPalindrome(s, startIndex, i)) {   // 是回文子串
                // 获取[startIndex,i]在s中的子串
                string str = s.substr(startIndex, i - startIndex + 1);
                path.push_back(str);
            } else {                                // 不是回文,跳过
                continue;
            }
            backtracking(s, i + 1); // 寻找i+1为起始位置的子串
            path.pop_back(); // 回溯过程,弹出本次已经填在的子串
        }
    }
    bool isPalindrome(const string& s, int start, int end) {
        for (int i = start, j = end; i < j; i++, j--) {
            if (s[i] != s[j]) {
                return false;
            }
        }
        return true;
    }
public:
    vector<vector<string>> partition(string s) {
        result.clear();
        path.clear();
        backtracking(s, 0);
        return result;
    }
};

6. N皇后问题

第51题. N皇后 52. N皇后 II (两个题目做法完全一样)

class Solution {
public:
// 想法:回溯。
    vector<vector<string>> ans;
    // 判断当前位置是否可以防止Queen
    bool is_valid (int row, int col, vector<string>& board) {
        // return true;
        int n = board.size();
        // 不同行(不需要)
        // for (auto x : board[row]) {
        //     if (x == 'Q') return false;
        // }

        // 不同列
        for (auto x : board) {
            if (x[col] == 'Q') return false;
        }

        // 斜对角(只有它之前的才有可能有Q,后面都没可能,所以只检查两个方向)
        // 检查 ↖
        for (int i = row - 1, j = col - 1; i >=0 && j >= 0; i--, j--) {
            if (board[i][j] == 'Q') {
                return false;
            }
        }
        // 检查 ↗
        for(int i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) {
            if (board[i][j] == 'Q') {
                return false;
            }
        }
        
        return true;
    }
    void backTracking(int row, vector<string>& board) {
        int n = board.size();
        if (row == n) {
            ans.push_back(board);
            return;
        }

        // 针对当前行的每个元素
        for (int col = 0; col < n; ++col) {
            if (is_valid(row, col, board)) {
                board[row][col] = 'Q';
                backTracking(row + 1, board);
                board[row][col] = '.';
            }
        }
    }
    vector<vector<string>> solveNQueens(int n) {
        vector<string> board(n, string(n, '.'));
        backTracking(0, board);
        return ans;
    }
};

7. 括号生成

class Solution {
// 想法:回溯,保存在cur中,如果满足条件就返回
private: 
    vector<string> ans;
public:
    void backtracking(string& cur, int n, int left, int right) { //传入左右括号的个数
        if (cur.size() == 2 * n) {
            ans.push_back(cur);
            return ;
        }

        if (left < n) {
            cur += '(';
            backtracking(cur, n, left + 1, right);
            cur.erase(cur.size() - 1);
        }
        if (right < left) {
            cur += ')';
            backtracking(cur, n, left, right + 1);
            cur.erase(cur.size() - 1);
        }
    }
    vector<string> generateParenthesis(int n) {
        string tmp = "";
        backtracking(tmp, n, 0, 0);
        return ans;
    }
};

8. 还没有做的题目

52. N皇后 II – done

332.重新安排行程 – 主要是难在采用什么数据结构 --done

class Solution {
private:
// unordered_map<出发机场, map<到达机场, 航班次数>> targets
unordered_map<string, map<string, int>> targets;
bool backtracking(int ticketNum, vector<string>& result) {
    if (result.size() == ticketNum + 1) {
        return true;
    }
    for (pair<const string, int>& target : targets[result[result.size() - 1]]) {
        if (target.second > 0 ) { // 记录到达机场是否飞过了
            result.push_back(target.first);
            target.second--;
            if (backtracking(ticketNum, result)) return true;
            result.pop_back();
            target.second++;
        }
    }
    return false;
}
public:
    vector<string> findItinerary(vector<vector<string>>& tickets) {
        targets.clear();
        vector<string> result;
        for (const vector<string>& vec : tickets) {
            targets[vec[0]][vec[1]]++; // 记录映射关系
        }
        result.push_back("JFK"); // 起始机场
        backtracking(tickets.size(), result);
        return result;
    }
};

9. 回溯附加题目

698. 划分为k个相等的子集

129. 求根节点到叶节点数字之和 - 非经典回溯套路,但是思想完全一样

10. 层内去重

491. 递增子序列 —— 如何去掉[4, 7]和[4, 7] 的重复?—— 层内去重(uset)

输入:nums = [4,6,7,7]
输出:[[4,6],[4,6,7],[4,6,7,7],[4,7],[4,7,7],[6,7],[6,7,7],[7,7]]

// 版本一
class Solution {
private:
    vector<vector<int>> result;
    vector<int> path;
    void backtracking(vector<int>& nums, int startIndex) {
        if (path.size() > 1) {
            result.push_back(path);
            // 注意这里不要加return,要取树上的节点
        }
        unordered_set<int> uset; // 使用set对本层元素进行去重
        for (int i = startIndex; i < nums.size(); i++) {
            if ((!path.empty() && nums[i] < path.back())
                    || uset.find(nums[i]) != uset.end()) {
                    continue;
            }
            uset.insert(nums[i]); // 记录这个元素在本层用过了,本层后面不能再用了
            path.push_back(nums[i]);
            backtracking(nums, i + 1);
            path.pop_back();
        }
    }
public:
    vector<vector<int>> findSubsequences(vector<int>& nums) {
        result.clear();
        path.clear();
        backtracking(nums, 0);
        return result;
    }
};

9. 贪心算法

1. 理论基础

(1) 贪心的本质是选择每一阶段的局部最优,从而达到全局最优。–贪心算法并没有固定套路,难点在于如何通过局部最优得到全局最优

(2) 如何验证可不可以用贪心算法呢?

最好用的策略就是举反例,如果想不到反例,那么就试一试贪心吧

(3) 贪心问题的步骤–贪心算法一般分为如下四步:

  • 将问题分解为若干个子问题
  • 找出适合的贪心策略
  • 求解每一个子问题的最优解
  • 将局部最优解堆叠成全局最优解

2. 经典题目,需要关注差值或者连续和。

比如122.买卖股票的最佳时机II53.最大子数组和

//买卖股票的最佳时机
class Solution {
public:
// 想法:相邻两天之间的差值,好像比较重要?
// 所以先算差值,构成数组
// 应该是差值>0就操作
    int maxProfit(vector<int>& prices) {
        int max = 0;
        int n = prices.size();


        vector<int> delta_p(n - 1, 0);
        for (int i = 1; i < n; ++i) {
            delta_p[i - 1] = prices[i] - prices[i - 1];
        }
        
        // 只需要利润>0的时候才操作
        for (int i = 0; i < n - 1; ++i) {
            if (delta_p[i] > 0) max += delta_p[i];
        }
        return max;
    }
};

3. 还没做的题目

45.跳跃游戏II – 主要关注的是可以跳到的范围

714. 买卖股票的最佳时机含手续费–题解都没咋看懂…(一个方法团灭 LEETCODE 股票买卖问题)

4. 常考题目:

1. 135.分发糖果

– 先保证右边元素大于左边的时候,右边元素 = 当前元素 + 1;然后左边元素大于当前元素时候,左边元素 = 当前元素 + 1,但是为了保证右边的结果,因此应该取左边元素 = max(左边元素,当前元素 + 1);

10. 动态规划Dynamic Programming (DP)

1. 动态规划的理论基础

对于动态规划问题,我将拆解为如下五步曲,这五步都搞清楚了,才能说把动态规划真的掌握了!

– 关键:第i步可以通过i-1和i-2推导出来(最经典的有70. 爬楼梯509. 斐波那契数

  1. 确定dp数组(dp table)以及下标的含义
  2. 确定递推公式
  3. dp数组如何初始化
  4. 确定遍历顺序
  5. 举例推导dp数组

一些同学可能想为什么要先确定递推公式,然后在考虑初始化呢?

因为一些情况是递推公式决定了dp数组要如何初始化!

2. 01背包问题(本质就是任何一个元素都只有0或者1两种状态)

—这里的0,1可以是选中不选中(比如1049. 最后一块石头的重量 II),也可以是正负号(比如494. 目标和

有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Wkayrp0U-1658928266696)(https://gitee.com/LEVI-Tempest/picgo-typora/raw/master/2021011010304192.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oehoec3n-1658928266696)(F:\Typora_Figures\笔试 + 面试\2021011010304192-16548268528395.png)]

dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少

再回顾一下dp[i][j]的含义:从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。

那么可以有两个方向推出来dp[i][j],

  • 不放物品i:由dp[i - 1][j]推出,即背包容量为j,里面不放物品i的最大价值,此时dp[i][j]就是dp[i - 1][j]。(其实就是当物品i的重量大于背包j的重量时,物品i无法放进背包中,所以被背包内的价值依然和前面相同。)
  • 放物品i:由dp[i - 1][j - weight[i]]推出,dp[i - 1][j - weight[i]] 为背包容量为j - weight[i]的时候不放物品i的最大价值,那么dp[i - 1][j - weight[i]] + value[i] (物品i的价值),就是背包放物品i得到的最大价值

所以递归公式: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);

void test_2_wei_bag_problem1() {
    vector<int> weight = {1, 3, 4};
    vector<int> value = {15, 20, 30};
    int bagweight = 4;

    // 二维数组
    vector<vector<int>> dp(weight.size(), vector<int>(bagweight + 1, 0));

    // 初始化
    for (int j = weight[0]; j <= bagweight; j++) {
        dp[0][j] = value[0];
    }

    // weight数组的大小 就是物品个数
    for(int i = 1; i < weight.size(); i++) { // 遍历物品
        for(int j = 0; j <= bagweight; j++) { // 遍历背包容量
            if (j < weight[i]) dp[i][j] = dp[i - 1][j];
            else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
        }
    }

    cout << dp[weight.size() - 1][bagweight] << endl;
}

int main() {
    test_2_wei_bag_problem1();
}

需要注意的是,转化为1维数组,必须是倒序遍历,不然会有重复出现

// 01 背包问题
// 一维数组解题
void test_1_wei_bag_problem() {
    vector<int> weight = {1, 3, 4};
    vector<int> value = {15, 20, 30};
    int bagWeight = 4;

    // 初始化
    vector<int> dp(bagWeight + 1, 0);
    for(int i = 0; i < weight.size(); i++) { // 遍历物品
        for(int j = bagWeight; j >= weight[i]; --j) { // 遍历背包容量
            dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
        }
    }
    cout << dp[bagWeight] << endl;
}

3. 完全背包问题

就是每个元素可以重复(无限次)使用。

在01背包的基础上很容易改进,就是把从大到小遍历,改成从小到大遍历就行

//01背包问题
for(int i = 0; i < weight.size(); i++) { // 遍历物品
        for(int j = bagWeight; j >= weight[i]; --j) { // 遍历背包容量
            dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
        }
    }

//完全背包问题
for(int i = 0; i < weight.size(); i++) { // 遍历物品
        for(int j = weight[i]; j <= bagWeight; ++j) { // 遍历背包容量
            dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
        }
    }

比如518. 零钱兑换 II

解法不难,注意:如果把两个for交换顺序,代码如下:

for (int j = 0; j <= bagWeight; j++) { // 遍历背包容量
    for (int i = 0; i < weight.size(); i++) { // 遍历物品
        if (j - weight[i] >= 0) dp[j] += dp[j - weight[i]];
    }
}

背包容量的每一个值,都是经过 1 和 5 的计算,包含了{1, 5} 和 {5, 1}两种情况。

此时dp[j]里算出来的就是排列数!

4. 多重背包问题

类似于01背包(元素数量 = 1)和完全01背包(元素数量无限)的结合体:每个元素的数量个数给定。

–解决方案十分简单,把多重背包摊开,完全就是一个01背包问题了(将每个元素按照个数,添加到一个新的数组里面)。

5. 股票问题(买卖股票的最佳时机)

6. 子序列问题

(1) 最长连续递增子序列

dp[i],i以前(包括i)以nums[i]结尾的,最长连续递增子序列的长度

// 想法:经典算法题目,动态规划
// dp[i] dp[i] i之前(包括i)以nums[i]结尾的最长连续递增子序列长度
    int lengthOfLIS(vector<int>& nums) {
        int n = nums.size();
        int ans = 1;
        vector<int> dp(n, 1);
        for (int i = 1; i < n; ++i) {
            for (int j = 0; j < i; ++j) {
                if(nums[i] > nums[j]) 
                    dp[i] = max(dp[i], dp[j] + 1);
            }
            if (dp[i] > ans) ans = dp[i];
        }
        return ans;
    }
(2) 最长公共子数组

dp[i][j]:以nums1[i]为结尾的,以nums[j]为结尾的最长公共子序列的长度

        vector<vector<int>> dp(n1, vector<int>(n2, 0));
        // 初始化nums[0]这一行和nums[i][0]这一列
        for (int i = 0; i < n1; ++i) {
            if (nums1[i] == nums2[0]) {
                dp[i][0] = 1;
                ans = 1;
            }
        }
        for (int j = 0; j < n2; ++j) {
            if(nums1[0] == nums2[j]) {
                dp[0][j] = 1;
                ans = 1;
            }
        }

		for (int i = 1; i < n1; ++i) {
            for (int j = 1; j < n2; ++j) {
                if (nums1[i] == nums2[j]) dp[i][j] = dp[i - 1][j - 1] + 1;
                if (dp[i][j] > ans) ans = dp[i][j];
            }
        }
(3) 最长公共子序列LCS

dp[i][j]:长度为[0, i - 1]的字符串text1与长度为[0, j - 1]的字符串text2的最长公共子序列为dp[i][j] (为了逃避初始化,所以用i-1而不是i)

class Solution {
public:
    int longestCommonSubsequence(string text1, string text2) {
        vector<vector<int>> dp(text1.size() + 1, vector<int>(text2.size() + 1, 0));
        for (int i = 1; i <= text1.size(); i++) {
            for (int j = 1; j <= text2.size(); j++) {
                if (text1[i - 1] == text2[j - 1]) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                } else {
                    dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
                }
            }
        }
        return dp[text1.size()][text2.size()];
    }
};
(4) 不同的子序列115. 不同的子序列

dp[i][j]:以i-1为结尾的s子序列中出现以j-1为结尾的t的个数为dp[i][j]。

class Solution {
public:
    int numDistinct(string s, string t) {
        vector<vector<uint64_t>> dp(s.size() + 1, vector<uint64_t>(t.size() + 1));
        for (int i = 0; i < s.size(); i++) dp[i][0] = 1;
        for (int j = 1; j < t.size(); j++) dp[0][j] = 0;
        for (int i = 1; i <= s.size(); i++) {
            for (int j = 1; j <= t.size(); j++) {
                if (s[i - 1] == t[j - 1]) {
                    dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];
                } else {
                    dp[i][j] = dp[i - 1][j];
                }
            }
        }
        return dp[s.size()][t.size()];
    }
};
(6) 求得最终的LCS串(之前是求长度)

其实就是倒过来遍历一遍数组,因为数组中元素来源:

if (text1[i - 1] == text2[j - 1]) {
    dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
    dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
}

所以数组中有三种方向,要么来源于左侧,要么来源于上面,要么来源于斜上角落(只有来源于斜上角的才是包含在LCS序列中的元素)

可以参考 A = “ABCBDAB” 和 B = “BDCABA”

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Gy56ZOHC-1658928266697)(F:\Typora_Figures\笔试 + 面试\watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MDY3MzYwOA==,size_16,color_FFFFFF,t_70.png)]

(7) 编辑距离问题 72. 编辑距离

if (word1[i - 1] == word2[j - 1])
    不操作
if (word1[i - 1] != word2[j - 1])
    增 dp[i][j] = dp[i - 1][j] + 1;
    删 dp[i][j] = dp[i][j - 1] + 1; (word1增加一个元素 = word2 删除一个元素)
    换 dp[i][j] = dp[i - 1][j - 1] + 1;
class Solution {
public:
// 想法:简化版是583. 两个字符串的删除操作(LCS问题),现在可以替换和插入?
// 想法:是否可以使用LCS,找到相同序列,然后删除 / 替换? -- 好像不行? 万一有多个LCS怎么处理?
// 还是用LCS类似的思想dp[i][j] 分别表示word1.substr(i - 1) 和word2.substr(j - 1) 的最少操作数
    int minDistance(string word1, string word2) {
        int n1 = word1.size(), n2 = word2.size();
        vector<vector<int>> dp (n1 + 1, vector<int>(n2 + 1, 0));

        for (int i = 0; i <= n1; i++) dp[i][0] = i;
        for (int j = 0; j <= n2; j++) dp[0][j] = j;
        for (int i = 1; i <= n1; ++i) {
            for (int j = 1; j<= n2; ++j) {
                if (word1[i - 1] == word2[j - 1]) dp[i][j] = dp[i - 1][j - 1];
                else dp[i][j] = min({dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]}) + 1;
            }
        }

        return dp[n1][n2];
    }
};

还可以做115. 不同的子序列 问题

(8) 回文子序列(数组)问题

都是动态规划问题

回文子数组:

class Solution {
public:
// 想法:
// 1. 暴力? 因为n只有1000
// 2. 动态规划dp[i][j] 表示[i, j]是否是回文的
    int countSubstrings(string s) {
        int n = s.size();
        int ans = 0;

        vector<vector<bool>> dp(n, vector<bool>(n, false));

        for (int i = n - 1; i >= 0; --i) {
            for (int j = i; j < n; ++j) {
                if (s[i] == s[j]) {
                    if (j - i <= 1) { // i = j (a) 和 j = i+1 (aa)
                        ++ans;
                        dp[i][j] = true;
                    } else if (dp[i + 1][j - 1]) { // 情况三
                        ++ans;
                        dp[i][j] = true;
                    }
                }
            }
        }
        return ans;
    }
};

回文子序列

class Solution {
public:
//想法: dp[i][j] ([i, j]回文子序列的长度,如果不是回文,那么赋值为0)
// 保留最大值即可
    int longestPalindromeSubseq(string s) {
        int n = s.size();
        int ans = 0;
        vector<vector<int>> dp (n, vector<int>(n, 0));
        for (int i = 0; i < n; ++i) {
            dp[i][i] = 1;
            ans = 1;
        }
        for (int i = n - 1; i >= 0; --i) {
            for (int j = i + 1; j < n; ++j) {
                if (s[i] == s[j]) dp[i][j] = dp[i + 1][j - 1] + 2;
                else dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);
                if (dp[i][j] > ans) ans = dp[i][j];
            }
        }
        return ans;
    }
};
(9) 最长递增子序列的个数

673. 最长递增子序列的个数



7. 买卖股票的最佳时机

总结,都可以用dp[i][j]来动态规划,其中i表示第i天,j表示状态(持有,不持有 / 不操作,第一次买入,第一次卖出,第二次买入,第二次卖出,…)

(1) 121. 买卖股票的最佳时机 (最多只能买卖一次)
– 贪心是最简单的(记录当前下标之前的最小值,假设当天卖出,一定是最小值点买入)
dp[i][j]:j = 0表示持有,j = 1表示不持有,第i天持有 / 不持有的最大值
int maxProfit(vector<int>& prices) {
    int n = prices.size();
    vector<vector<int>> dp (n, vector<int>(2, 0));

    dp[0][0] = -prices[0];
    dp[0][1] = 0;
    for (int i = 1; i < n; ++i) {
        // 注意这里只能是- prices[i],而不是dp[i - 1][1] - prices[i],因为只能买卖一次,上一天如果不持有,而这一天持有股票,那么应该是-prices[i]
        dp[i][0] = max(dp[i - 1][0],  - prices[i]);
        dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i]);
    }

    return dp[n - 1][1];
}
(2) 122. 买卖股票的最佳时机 II (可以买卖无限次)

贪心就是只要差值 > 0就买(吃掉所有涨幅)

int maxProfit(vector<int>& prices) {
    int n = prices.size();
    vector<vector<int>> dp (n, vector<int>(2, 0));

    dp[0][0] = -prices[0];
    dp[0][1] = 0;
    for (int i = 1; i < n; ++i) {
        // 注意这里是dp[i - 1][1] - prices[i],因为可以买卖多次
        dp[i][0] = max(dp[i - 1][0],  - prices[i]);
        dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i]);
    }

    return dp[n - 1][1];
}
(3) 123. 买卖股票的最佳时机 III (最多只能买卖两次)

dp[i][j]表示第i天的最大利润,j = 0表示没有操作过,j = 1表示第一次买入(未必是第i天第一次买入),j = 2表示第一次卖出,j = 3表示第二次买入,j = 4表示第二次卖出

int maxProfit(vector<int>& prices) {
    int n = prices.size();
    if (n == 1) return 0;

    vector<vector<int>> dp (n, vector<int>(5, 0));
    dp[0][0] = 0;
    dp[0][1] = -prices[0];
    dp[0][3] = -prices[0];
    for (int i = 1; i < n; ++i) {
        dp[i][0] = dp[i - 1][0];
        dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
        dp[i][2] = max(dp[i - 1][2], dp[i - 1][1] + prices[i]);
        dp[i][3] = max(dp[i - 1][3], dp[i - 1][2] - prices[i]);
        dp[i][4] = max(dp[i - 1][4], dp[i - 1][3] + prices[i]);
    }
    return dp[n - 1][4];
}
class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int n = prices.size();
        int buy1 = -prices[0], sell1 = 0;
        int buy2 = -prices[0], sell2 = 0;
        for (int i = 1; i < n; ++i) {
            buy1 = max(buy1, -prices[i]);
            sell1 = max(sell1, buy1 + prices[i]);
            buy2 = max(buy2, sell1 - prices[i]);
            sell2 = max(sell2, buy2 + prices[i]);
        }
        return sell2;
    }
};

作者:LeetCode-Solution
链接:https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-iii/solution/mai-mai-gu-piao-de-zui-jia-shi-ji-iii-by-wrnt/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
(4) 188. 买卖股票的最佳时机 IV (最多只能买卖k次数)
class Solution {
public:
// 想法类似123. 买卖股票的最佳时机III
// dp[i][j] 表示第i天,第j状态下的最大利润
// j = 0表示无操作,j = 1,表示第一次买入,j = 2,表示第一次卖出,j = 3 表示第二次买入...
// j = 2 * k - 1表示第k次买入, j = 2 * k表示第k次卖出
    int maxProfit(int k, vector<int>& prices) {
        int n = prices.size();
        if (!n) return 0;
        vector<vector<int>> dp(n, vector<int> (2*k + 1, 0));

        for (int j = 1; j < 2*k + 1; ++j) {
            if (j % 2 == 1) dp[0][j] = -prices[0];
        }
        for (int i = 1; i < n; ++i) {
            for (int j = 0; j < 2 * k + 1; ++j) {
                if(!j) dp[i][j] = dp[i - 1][j];
                else if (j % 2 == 1) dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - 1] - prices[i]);
                else if (j % 2 == 0) dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - 1] + prices[i]);
            }
        }

        return dp[n - 1][2 * k];
    }
};
(5) 309. 最佳买卖股票时机含冷冻期

关键是,设置为几个状态

  • 状态一:买入股票状态(今天买入股票,或者是之前就买入了股票然后没有操作)
  • 卖出股票状态,这里就有两种卖出股票状态
    • 状态二:两天前就卖出了股票,度过了冷冻期,一直没操作,今天保持卖出股票状态
    • 状态三:今天卖出了股票
  • 状态四:今天为冷冻期状态,但冷冻期状态不可持续,只有一天!

j的状态为:

  • 0:状态一
  • 1:状态二
  • 2:状态三
  • 3:状态四
class Solution {
public:
//想法: 还是和之前一样动态规划?
// dp[i][j] j状态下,第i天可以获取到的最大利润,j = 0持有状态 / (两天或者两天前就卖出)不持有 / (今天)卖出 /冻结期
    int maxProfit(vector<int>& prices) {
        int n = prices.size();
        if (n == 0) return 0;
        vector<vector<int>> dp(n, vector<int>(4, 0));
        dp[0][0] -= prices[0]; 
        for (int i = 1; i < n; i++) {
            dp[i][0] = max(dp[i - 1][0], max(dp[i - 1][3], dp[i - 1][1]) - prices[i]);
            dp[i][1] = max(dp[i - 1][1], dp[i - 1][3]);
            dp[i][2] = dp[i - 1][0] + prices[i];
            dp[i][3] = dp[i - 1][2];
        }
        return max(dp[n - 1][3],max(dp[n - 1][1], dp[n - 1][2]));
    }
};
(6) 714. 买卖股票的最佳时机含手续费

关键是贪心,这个题目和不含手续费的类似,但是不能再delta > 0就交易了,其实本质是最低点买入,最高点卖出

  • 情况一:收获利润的这一天并不是收获利润区间里的最后一天(不是真正的卖出,相当于持有股票),所以后面要继续收获利润。
  • 情况二:前一天是收获利润区间里的最后一天(相当于真正的卖出了),今天要重新记录最小价格了。
  • 情况三:不作操作,保持原有状态(买入,卖出,不买不卖)
class Solution {
public:
    int maxProfit(vector<int>& prices, int fee) {
        int result = 0;
        int minPrice = prices[0]; // 记录最低价格
        for (int i = 1; i < prices.size(); i++) {
            // 情况二:相当于买入
            if (prices[i] < minPrice) minPrice = prices[i];

            // 情况三:保持原有状态(因为此时买则不便宜,卖则亏本)
            if (prices[i] >= minPrice && prices[i] <= minPrice + fee) {
                continue;
            }

            // 计算利润,可能有多次计算利润,最后一次计算利润才是真正意义的卖出
            if (prices[i] > minPrice + fee) {
                result += prices[i] - minPrice - fee;
                minPrice = prices[i] - fee; // 情况一,这一步很关键
            }
        }
        return result;
    }
};

dp做法和122. 买卖股票的最佳时机 II(可以买卖无限次)相同,只不过卖出的时候多加手续费

int maxProfit(vector<int>& prices, int fee) {
    int n = prices.size();
    vector<vector<int>> dp(n, vector<int> (2,0));

    dp[0][0] = -prices[0];
    for (int i = 1; i < n; ++i) {
        dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] - prices[i]);
        dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - fee + prices[i]);
    }

    return dp[n - 1][1];
}

8. 第一遍不会做的题目

8.1 整数拆分

343. 整数拆分 给定一个正整数 n ,将其拆分为 k正整数 的和( k >= 2 ),并使这些整数的乘积最大化。

返回 你可以获得的最大乘积

class Solution {
public:
// 动态规划:dp[i]: 分拆数字i可以得到的最大乘积
// dp[i]可以怎么来?从1~j,两种可能,要么
// 是j * (i - j) 直接相乘。
// 要么是j * dp[i - j],相当于是拆分(i - j)

    int integerBreak(int n) {
        vector<int> dp(n + 1, 0);
        
        // dp[0] = 1;
        // dp[1] = 1;
        dp[2] = 1;
        for (int i = 3; i <= n; ++i) {
            for (int j = 1; j < i; ++j) {
                int tmp = max(j * (i - j), j * dp[i - j]);
                if (tmp > dp[i]) {
                    dp[i] = tmp;
                }
            }
        }
        return dp[n];
    }
};
8.2 不同的二叉搜索树

96. 不同的二叉搜索树


416. 分割等和子集

1049. 最后一块石头的重量 II – 本质和416完全一样

494. 目标和 (关键在于,sum是固定的,所以一定存在l + r = sum, l - r = target,所以可以转化为:l - (sum - l) = target,所以有l = (sum + target) / 2)

337. 打家劫舍 III – 树的问题,其实是遍历顺序的问题

1035. 不相交的线 – 思路问题,不相交 —— 这就是说明在字符串A中 找到一个与字符串B相同的子序列,且这个子序列不能改变相对顺序,只要相对顺序不改变,链接相同数字的直线就不会相交。,其实就是求两个字符串的最长公共子序列的长度!

115. 不同的子序列 – 难点在于递推公式

72. 编辑距离

309. 最佳买卖股票时机含冷冻期 – 几个状态怎么设置

9. 还没做的题

96. 不同的二叉搜索树

11. 单调栈

1. 单调栈的理论基础(时间复杂度 = O(n),用空间换时间)

那有同学就问了,我怎么能想到用单调栈呢? 什么时候用单调栈呢?

通常是一维数组,要寻找任一个元素的右边或者左边第一个比自己大或者小的元素的位置,此时我们就要想到可以用单调栈了

单调栈的本质是空间换时间,因为在遍历的过程中需要用一个栈来记录右边第一个比当前元素高的元素,优点是只需要遍历一次。

2. 具体做法

建立一个栈,使得栈底元素最大,如果当前元素大于栈顶,栈顶元素弹出,写入ans,如果当前元素小于栈顶,直接push

739. 每日温度

class Solution {
public:
    vector<int> dailyTemperatures(vector<int>& T) {
        stack<int> st; // 递增栈
        vector<int> result(T.size(), 0);
        for (int i = 0; i < T.size(); i++) {
            while (!st.empty() && T[i] > T[st.top()]) { // 注意栈不能为空
                result[st.top()] = i - st.top();
                st.pop();
            }
            st.push(i);

        }
        return result;
    }
};

3. 接雨水

3.1 11. 盛最多水的容器 —— 双指针
3.2 接雨水 42. 接雨水
(1) 暴力

每次找到最左边最大值,最右边最大值,用两个最大值中最小的那一个,减去当前height[i] 就是当前格子的雨水高度,当前格子的雨水宽度为1,累计计算即可

int trap(vector<int>& height)
{
    int ans = 0;
    int size = height.size();
    for (int i = 1; i < size - 1; i++) {
        int max_left = 0, max_right = 0;
        for (int j = i; j >= 0; j--) { //Search the left part for max bar size
            max_left = max(max_left, height[j]);
        }
        for (int j = i; j < size; j++) { //Search the right part for max bar size
            max_right = max(max_right, height[j]);
        }
        ans += min(max_left, max_right) - height[i];
    }
    return ans;
}
(2) 动态规划

(1) 中的暴力算法,每次都得重新遍历找最大值和最小值,这里可以使用两个数组,保存“从当前节点往右看的最大值”, ”从当前节点往左看的最大值“

int trap(vector<int>& height)
{
    if (height == null)
        return 0;
    int ans = 0;
    int size = height.size();
    vector<int> left_max(size), right_max(size);
    left_max[0] = height[0];
    for (int i = 1; i < size; i++) {
        left_max[i] = max(height[i], left_max[i - 1]);
    }
    right_max[size - 1] = height[size - 1];
    for (int i = size - 2; i >= 0; i--) {
        right_max[i] = max(height[i], right_max[i + 1]);
    }
    for (int i = 1; i < size - 1; i++) {
        ans += min(left_max[i], right_max[i]) - height[i];
    }
    return ans;
}
(3) 单调栈

只有上升的时候才会形成低洼,下降的时候入栈,当当前元素大于栈顶的时候,出站,累计当前雨水,如果栈顶还是小于当前元素,就继续出战。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4tMG543z-1658928266698)(F:\Typora_Figures\笔试 + 面试\image-20220720205021304.png)]

int trap(vector<int>& height)
{
    int ans = 0, current = 0;
    stack<int> st;
    while (current < height.size()) {
        while (!st.empty() && height[current] > height[st.top()]) {
            int top = st.top();
            st.pop();
            if (st.empty())
                break;
            int distance = current - st.top() - 1;
            int bounded_height = min(height[current], height[st.top()]) - height[top];
            ans += distance * bounded_height;
        }
        st.push(current++);
    }
    return ans;
}
3.3 84. 柱状图中最大的矩形

这里和3.2做法相反:

  1. 动态规划,求左边比它小的元素的位置,右边比他小的元素的位置。

  2. 单调栈——当右边元素比它小的时候出栈。s = height * width = height[i] * width)

(1) 动态规划:寻找左边第一个小于它的坐标和右边第一个小于它的坐标
class Solution {
public:
    int largestRectangleArea(vector<int>& heights) {
        vector<int> minLeftIndex(heights.size());
        vector<int> minRightIndex(heights.size());
        int size = heights.size();

        // 记录每个柱子 左边第一个小于该柱子的下标
        minLeftIndex[0] = -1; // 注意这里初始化,防止下面while死循环
        for (int i = 1; i < size; i++) {
            int t = i - 1;
            // 这里不是用if,而是不断向左寻找的过程
            while (t >= 0 && heights[t] >= heights[i]) t = minLeftIndex[t];
            minLeftIndex[i] = t;
        }
        // 记录每个柱子 右边第一个小于该柱子的下标
        minRightIndex[size - 1] = size; // 注意这里初始化,防止下面while死循环
        for (int i = size - 2; i >= 0; i--) {
            int t = i + 1;
            // 这里不是用if,而是不断向右寻找的过程
            while (t < size && heights[t] >= heights[i]) t = minRightIndex[t];
            minRightIndex[i] = t;
        }
        // 求和
        int result = 0;
        for (int i = 0; i < size; i++) {
            int sum = heights[i] * (minRightIndex[i] - minLeftIndex[i] - 1);
            result = max(sum, result);
        }
        return result;
    }
};
(2) 单调栈,当小于它的时候出栈(注意开头和结尾加入的0!!!)
// 版本一
class Solution {
public:
    int largestRectangleArea(vector<int>& heights) {
        stack<int> st;
        heights.insert(heights.begin(), 0); // 数组头部加入元素0
        heights.push_back(0); // 数组尾部加入元素0
        st.push(0);
        int result = 0;
        // 第一个元素已经入栈,从下标1开始
        for (int i = 1; i < heights.size(); i++) {
            // 注意heights[i] 是和heights[st.top()] 比较 ,st.top()是下标
            if (heights[i] > heights[st.top()]) {
                st.push(i);
            } else if (heights[i] == heights[st.top()]) {
                st.pop(); // 这个可以加,可以不加,效果一样,思路不同
                st.push(i);
            } else {
                while (heights[i] < heights[st.top()]) { // 注意是while
                    int mid = st.top();
                    st.pop();
                    int left = st.top();
                    int right = i;
                    int w = right - left - 1;
                    int h = heights[mid];
                    result = max(result, w * h);
                }
                st.push(i);
            }
        }
        return result;
    }
};

12. 图

1. 基本理论 —— 创建图

img

(1) 邻接矩阵
vector<vector<int>> matrix;
(2) 邻接表(类似于hash桶)

——节省空间,并且不能快速知道两个节点是否连接 (需要遍历相同行)

vector<vector<int>> matrix;
graph = [[1,2],[3],[3],[]]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CUKyxSS9-1658928266699)(F:\Typora_Figures\笔试 + 面试\all_1.jpg)]

2. 最短路径问题 Dijkstra

struct cmp{
    bool operator()(pair<int, int>& a, pair<int, int>& b) {
        return a.second < b.second;
    }
};
vector<int> dijkstra(int start) {
    // 初始化
    int n = graph.size();
    for (int i = 0; i < n; ++i) {
        // visited.push_back(false);
        path.push_back(-1);
        distance.push_back(INT_MAX);
    }
    // visited[start] = true;
    distance[start] = 0;
    path[start] = start;
    
	// q中保存id & dis (以dis排序的小根堆)
    priority_queue<pair<int, int>, vector<pair<int, int>>, cmp> q;
    q.push({start, 0});
    while (!q.empty()) {
        int cur_id = q.top().first;
        int cur_dis = q.top().second;
        q.pop();

		// 保证不是重复(因为把所有节点都push进去,因此可能有重复,重复的就出去,但是不进行任何操作了)
        if (cur_dis > distance[cur_id]) {
            continue;
        }

        // 将cur_id相邻的节点装入队列
        for (pair<int, int>& neighbor : graph[cur_id]) {
            int next_id = neighbor.first;
            int next_dis = distance[cur_id] + neighbor.second;
			
            // 如果需要更新,就更新dis,并push进去,反之就不管
            if (next_dis < distance[next_id]) {
                distance[next_id] = next_dis;
                q.push({next_id, next_dis});
                path[next_id] = cur_id;
            }
        }
    }

    return distance;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KEd99CDL-1658928266699)(F:\Typora_Figures\笔试 + 面试\image-20220719205350437.png)]

3. 并查集(带路径压缩) – 图的连通性问题 / 查找环

如果新加入的两个点的父节点相同,那么肯定存在环。

路径压缩的find(每次find都整理) —— 初始化的时候parent[i] = i;

int find_root(int x, vector<int>& parent) {
    if(parent[x] == x) return x;
    return parent[x] = find_root(parent[x], parent);
}

经典例子:323. 无向图中连通分量的数目

class Solution {
// 想法:并查集, 初始化假设有n个连通分量(cnt = n),当成功union之后,--cnt;最后 ret cnt;
private:
    int find_root(int x, vector<int>& parent) {
        if(parent[x] == x) return x;
        return parent[x] = find_root(parent[x], parent);
    }

    // false表示没有真正合并(原本就在一起),true表示合并了,加入xy之后合并了两个联通分量
    bool union_vertices(int x, int y, vector<int>& parent) {
        int x_root = find_root(x, parent);
        int y_root = find_root(y, parent);
        if (x_root == y_root) return false;

        parent[x_root] = y_root;
        return true;
    }
public:
    int countComponents(int n, vector<vector<int>>& edges) {
        vector<int> parent(n, -1); 
        for (int i = 0; i < n; ++i) parent[i] = i; // 初始化parent[x] = x;
        int  ans = n; 

        for (auto eg : edges) {
            if (union_vertices(eg[0], eg[1], parent)) --ans;
        }
        return ans;
    }
};

685. 冗余连接 II – 使用多次并查集

6106. 统计无向图中无法互相到达点对数 — 不加rank,超时,加rank才可以过

bilibili并查集

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ihcZhdtF-1658928266699)(F:\Typora_Figures\笔试 + 面试\image-20220624145228919.png)]

4. 拓扑排序

210. 课程表 II —— 对当前图进行遍历,如果有环,那么ret null,反之,ret后序遍历的reverse

class Solution {
// 想法:拓扑排序,其实就是如果没有环存在,那么就是后序遍历,仔reverse就是结果
private:
    vector<int> ans;
    bool has_cycle = false;
    vector<int> post_order;

    vector<bool> cur_path; // 当前路径(进入当前节点赋值为true,离开的时候赋值为false;如果当前为true,并且再次进入,说明有问题)
    vector<bool> visited;  // 因为可能出现环,因此要用这个避免死循环
    void construct_cur_and_visited(int n) {
        for (int i = 0; i < n; ++i) {
            cur_path.push_back(false);
            visited.push_back(false);
        }
    }

    void dfs(vector<vector<int>>& graph, int vtx_idx) {
        if (cur_path[vtx_idx]) has_cycle = true; // 如果已经在当前路径了,说明存在环。
        if(visited[vtx_idx] || has_cycle) {
            return; 
        }

        visited[vtx_idx] = true;
        cur_path[vtx_idx] = true;

        // 继续遍历当前节点的所有后继节点
        for (auto& elem : graph[vtx_idx]) {
            dfs(graph, elem);
        }
        post_order.push_back(vtx_idx);
        cur_path[vtx_idx] = false;
    }
public:
    vector<int> findOrder(int numCourses, vector<vector<int>>& prerequisites) {
        // 初始化 + 新建图
        construct_cur_and_visited(numCourses);
        vector<vector<int>> graph(numCourses, vector<int>());
        for (auto& prereq : prerequisites) {
            graph[prereq[1]].push_back(prereq[0]);
        }

        // 遍历图
        for (int i = 0; i < numCourses; ++i) dfs(graph, i);

        // 如果存在环,那么肯定是空
        if (has_cycle) return {};

        // 如果没有环,那么返回后序遍历的reverse
        reverse(post_order.begin(), post_order.end());
        return post_order;
    }
};

5. 岛屿问题 —— 递归感染!(其实都是深度优先遍历dfs)

695. 岛屿的最大面积

1254. 统计封闭岛屿的数目

200. 岛屿数量

694. 不同岛屿的数量——dfs的时候做记录——尽量看一下怎么做的,这个应该是最难的岛屿问题了

比如:695. 岛屿的最大面积 :解法如下


class Solution {
public:
// 想法:类似于200. 岛屿数量
// 对一个岛屿进行递归感染(只管上下左右,然后递归,感染的效果是,将这个island中的陆地变成2)
    int accumulate_this_island(vector<vector<int>>& grid, int x, int y) {
        int sum = 0;

        int m = grid.size(), n = grid[0].size();
        if (x < 0 || y < 0 || x >= m || y >= n) {
            // 已经越界
            return 0;
        }
        
        if (grid[x][y] == 1) {
            grid[x][y] = 2;
            sum += 1;
            sum += accumulate_this_island(grid, x - 1, y);
            sum += accumulate_this_island(grid, x + 1, y);
            sum += accumulate_this_island(grid, x, y - 1);
            sum += accumulate_this_island(grid, x, y + 1);
        }

        return sum;
    }
    int maxAreaOfIsland(vector<vector<int>>& grid) {
        int ans = 0;
        int m = grid.size(), n = grid[0].size();
        for (int i = 0 ; i < m; ++i) {
            for (int j = 0; j < n; ++j) {
                if (grid[i][j] == 1) {
                    int tmp = accumulate_this_island(grid, i, j);
                    if (tmp > ans) ans = tmp;
                }
            }
        }

        return ans;
    }
};

6. 图的遍历dfs(类似回溯)

——如果有环,那么就用visited[n]数组,防止死循环

回溯算法和 DFS 算法的区别所在:回溯算法关注的不是节点,而是树枝。

797. 所有可能的路径

class Solution {
// 想法:dfs(回溯),如果相同就加入
private:
    vector<vector<int>> ans;
    vector<int> cur_road;
public:
    void dfs(vector<vector<int>>& graph, int cur, int n) {
        if (cur == n) {
            ans.push_back(cur_road);
            return;
        }
        for (auto &cur_next : graph[cur]) {
            cur_road.push_back(cur_next);
            dfs(graph, cur_next, n);
            cur_road.pop_back();
        }
    }
    vector<vector<int>> allPathsSourceTarget(vector<vector<int>>& graph) {
        cur_road.push_back(0);
        dfs(graph, 0, graph.size() - 1);
        return ans;
    }
};

7. 二分图(给图着色——两种颜色)

886. 可能的二分法

class Solution {
// 想法: 二分图问题
private:
    vector<bool> color;
    vector<bool> visited;
    bool can_finish = true;
    void construct_color_visited(int n) {
        for (int i = 0; i < n; ++i) {
            color.push_back(false);
            visited.push_back(false);
        }
    }
    void construct_graph(vector<vector<int>>& graph, vector<vector<int>>& dislikes) {
        for (auto &dislike : dislikes) {
            // 因为是无向图,因此需要插入两次。
            graph[dislike[0] - 1].push_back(dislike[1] - 1); //index从1开始
            graph[dislike[1] - 1].push_back(dislike[0] - 1); 
        }
    }
    void traverse(vector<vector<int>>& graph, int v){
        if (can_finish == false) return;

        visited[v] = true;
        for (auto &w : graph[v]) {
            if (!visited[w]) {
                color[w] = !color[v];
                traverse(graph, w);
            } else if (color[w] == color[v]) can_finish = false;
        }
    }
    void print(vector<vector<int>>& graph) {
        for (auto & line : graph) {
            for (auto& v : line) cout << v << " ";
            cout << endl;
        }
    }
public:
    bool possibleBipartition(int n, vector<vector<int>>& dislikes) {
        construct_color_visited(n);
        vector<vector<int>> graph(n, vector<int>());
        construct_graph(graph, dislikes);
        // print(graph);

        for (int v = 0; v < n - 1; ++v) {
            if (!visited[v]) traverse(graph, v);
        }
        return can_finish;
    }
};

8. 广度优先搜索(类似于层序遍历)

1162. 地图分析
class Solution {
// 想法:n比较小,直接用暴力(递归感染)?——关键问题是,怎么确定是距离最近的,而且还得是距离最大的单元格?——这个会超时
// 正确做法为:广度优先搜索(类似于树的层序遍历)
// 第一轮:把所有1入队,记录层号为0;然后队列pop,同时把相邻的0入队,记录层号为1;然后继续队列pop,同时把相邻的0入队,记录层号为2
// 同时记录一个visited,访问过的就不访问了👆,其中层号就是它的最大[曼哈顿距离]

private:
    typedef struct point_s{
        int x;
        int y;
        void add(int a, int b) {
            x = a;
            y = b;
        }
    }point;

    void push_into_queue(int x, int y, queue<point>& que, vector<vector<bool>>& visited, int &visited_cnt){
        if (x < 0 || y < 0 || x >= visited.size() || y >= visited.size()) return;
        if (!visited[x][y]) {
            point tmp = {x, y};
            que.push(tmp);
            visited[x][y] = true;
            ++visited_cnt;
        }
    }
    void print(vector<vector<int>>& graph) {
        for (auto line : graph) {
            for (auto elem : line) {
                cout << elem << " ";
            }
            cout << endl;
        }
        cout << "-----" << endl;
    }
    vector<vector<int>> graph;
public:
    int maxDistance(vector<vector<int>>& grid) {
        int n = grid.size();
        vector<vector<bool>> visited(n, vector<bool>(n, false));
        queue<point> q;
        int level = 0;
        int visited_cnt = 0;
        // 第0轮,把所有1放进去
        point tmp;  
        for (int i = 0; i < n; ++i) {
            for (int j = 0; j < n; ++j) {
                if (grid[i][j] == 1) {
                    tmp.add(i, j);
                    q.push(tmp);
                    visited[i][j] = true;
                }
            }
        }
        visited_cnt = q.size();
        if (visited_cnt == n * n || visited_cnt == 0) return -1; // 全1 or 全0
        
        while (visited_cnt < n * n) {
            int cur_level_n = q.size();
            ++level;
            for (int i = 0; i < cur_level_n; ++i) {
                point cur_p = q.front();
                q.pop();
                int x = cur_p.x, y = cur_p.y;
                push_into_queue(x - 1, y, q, visited, visited_cnt);
                push_into_queue(x + 1, y, q, visited, visited_cnt);
                push_into_queue(x, y - 1, q, visited, visited_cnt);
                push_into_queue(x, y + 1, q, visited, visited_cnt);
            }
        }
        return level; //ans = level (最大层数)
    }

9. 最小生成树

9.1 Kruskal 求最小生成树

——同并查集算法,首先对图的边按照权重排序,然后进行并查集操作(如果已经连通就不管了)

9.2 Prime 求最小生成树

——Prim算法,每次选一个点加入已经连接的图(图始终是连通的),要求加入的时候选取的是到这个点最小的一条边

13. 设计类题目

1. LRU算法 – 设计合理的数据结构;合理划分子模块。

146. LRU 缓存 – 双链表 + hash

请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。
实现 LRUCache 类:

  • LRUCache(int capacity) 以 正整数 作为容量 capacity 初始化 LRU 缓存
  • int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。
  • void put(int key, int value) 如果关键字 key 已经存在,则变更其数据值 value ;如果不存在,则向缓存中插入该组 key-value 。如果插入操作导致关键字数量超过 capacity ,则应该 逐出 最久未使用的关键字。
  • 函数 get 和 put 必须以 O(1) 的平均时间复杂度运行。
class LRUCache {
public:
// 想法:本科的时候好像实现过 -- 用双向链表(因为要频繁删修改元素位置,如果用数组需要移动元素,如果用单链表需要找到pre)
// 又因为经常需要寻找尾巴--可以尝试让L变成头,L->pre是尾巴,L为dummyHead
// 为了让get也是O(1),还需要一个hash -- 但是链表查找也要是O(1),所以hash可以弄成:unordered_map<int, pair <int, ListNode*>> -- 不如改成unordered_map<int, ListNode*>

// 之前的写法太冗余了 -- 修改一下
// makeRecently(删除当前位置,移动到末尾)
// addRecently(添加元素)
// deleteLeastRecently(删除最近最少使用)
// deleteKey(删除元素 -- makeRecently & deleteLeastRecently 都要使用)

    typedef struct ListNode {
        int val;
        struct ListNode* next;
        struct ListNode* pre;
        ListNode (int x) : val(x), next(nullptr), pre(nullptr) {}
    }ListNode;

    LRUCache(int capacity) {
        L = new ListNode(0);
        L -> pre = L;
        L -> next = L;
        g_capacity = capacity;
    }
    
    void deleteKey(ListNode* tmp) {
        // ListNode* tmp = h[key];

        tmp -> next -> pre = tmp -> pre;
        tmp -> pre -> next = tmp -> next;

        auto iter = h.begin();
        while (iter -> second != tmp) {
            ++iter;
        }
        h.erase(iter);
    }
    void makeRecently(int key) {
        ListNode* cur = h[key];

        // 原来节点裂开
        cur -> next -> pre = cur -> pre;
        cur -> pre -> next = cur -> next;
        
        // 添加到末尾
        L -> pre -> next = cur;
        cur -> pre = L -> pre;
        cur -> next = L;
        L -> pre = cur;
    }
    void addRecently(int key, int value) {
        ListNode* new_elem = new ListNode(value);
        h[key] = new_elem;

        L -> pre -> next = new_elem;
        new_elem -> pre = L -> pre;
        new_elem -> next = L;
        L -> pre = new_elem;
    }

    int get(int key) {
        if (h.find(key) != h.end()) {
            makeRecently(key);
            return h[key] -> val;
        }
        return -1;
    }
    
    void put(int key, int value) {
        if (get(key) != -1) {
            // 已经在了,先找到,然后断开,然后插入
            h[key] -> val = value;
            makeRecently(key);
        } else {
            // 创建新的节点
            addRecently(key, value);
            ++cur_num;
            // 如果容量满了,需要先删除
            if (cur_num > g_capacity) {
                deleteKey(L -> next);
                --cur_num;
            } 
        } 
    }

        void print_List() {
        ListNode* cur = L -> pre;
        while (cur != L) {
            cout << "cur(pre) -> val" << cur -> val << " ";
            cur = cur -> pre;
        }
        cout << endl;

        cur = L -> next;
        while (cur != L) {
            cout << "cur(next) -> val" << cur -> val << " ";
            cur = cur -> next;
        }
        cout << endl;
    }
    void print_hash() {
        for (auto x : h) {
            cout << "hash table vale = " <<x.second -> val <<" ";
        }
        cout <<endl;
    }
private:
    // 注意,头节点是不参与当前的
    ListNode* L;
    unordered_map<int, ListNode*> h;
    int g_capacity = 0; // global capacity
    int cur_num = 0;
};

2. LFU 算法

460. LFU 缓存

14. 位运算

1. 异或 (如果出现偶数次,异或 = 0)—— 136. 只出现一次的数字

给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。

说明:你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗?

答案是使用位运算。对于这道题,可使用异或运算 ⊕。异或运算有以下三个性质。

  1. 任何数和 0 做异或运算,结果仍然是原来的数,即 a⊕0=a。
  2. 任何数和其自身做异或运算,结果是 0,即a⊕a=0。
  3. 异或运算满足交换律和结合律,即a⊕b⊕a=b⊕a⊕a=b⊕(a⊕a)=b⊕0=b。
class Solution {
public:
    int singleNumber(vector<int>& nums) {
        int ret = 0;
        for (auto e: nums) ret ^= e;
        return ret;
    }
};

15. 前缀树

1. 构建一个前缀树 (就是每个节点只存一个char的多叉树 )

class Trie {
private:
    vector<Trie*> children;
    bool isEnd;

    Trie* searchPrefix(string prefix) {
        Trie* node = this;
        for (char ch : prefix) {
            ch -= 'a';
            if (node->children[ch] == nullptr) {
                return nullptr;
            }
            node = node->children[ch];
        }
        return node;
    }

public:
    Trie() : children(26), isEnd(false) {}

    void insert(string word) {
        Trie* node = this;
        for (char ch : word) {
            ch -= 'a';
            if (node->children[ch] == nullptr) {
                node->children[ch] = new Trie();
            }
            node = node->children[ch];
        }
        node->isEnd = true;
    }

    bool search(string word) {
        Trie* node = this->searchPrefix(word);
        return node != nullptr && node->isEnd;
    }

    bool startsWith(string prefix) {
        return this->searchPrefix(prefix) != nullptr;
    }
};

16. 差分数组

1109. 航班预订统计

// 看了题解,用差分数组来做👇
// 1)对于原始数组arr[a, b, c, d],其差分数组为:diff[a, b-a, c-b, d-c]
// 2)差分数组的前缀和数组 == 原始数组,即:求差分数组的前缀和数组,即可还原回去。[a, a + b-a, a+b-a + c-b, ...]
// 3)对原始数组的区间增加,可以转化为对其差分数组的两点增加( O(n) -> O(1) ):
//    假设对arr[i ... j]区间每个元素全部增加delta,则等价于:diff[i] += delta,diff[j+1] -= delta
    vector<int> corpFlightBookings(vector<vector<int>>& bookings, int n) {
        vector<int> nums(n);
        for (auto& booking : bookings) {
            nums[booking[0] - 1] += booking[2];
            if (booking[1] < n) {
                nums[booking[1]] -= booking[2];
            }
        }
        for (int i = 1; i < n; i++) {
            nums[i] += nums[i - 1];
        }
        return nums;
    }

字符串函数

1. 子串

string substr(start, len);
string substr(start); //返回从start开始,到末尾的字符串

2. 字符串首地址

string str = “abd”;
str.c_str(); //这个在我的EDCR中经常使用

3. 字符串查找

int pos = str.find(sub_str, 1); // 从下标 = 1开始查找,找到就返回位置,找不到返回-1。
int pos = str.rfind(); //反向查找

4. int转string函数

string s = to_string(int_arr[i]);

// itoa 函数,返回的是char* 并且用法如下
// radix为转换时所用基数
char *itoa(int value,char *string,int radix);

// 相比而言,atoi()就很好用
int atoi(char *nptr);

但是特别注意,atoi函数是会溢出的,具体可以参考 leetcode93.复原IP地址 的提交记录

vector

1.数组初始化,可以有五种方式,举例说明如下:

vector<int> a(10); //定义了10个整型元素的向量(尖括号中为元素类型名,它可以是任何合法的数据类型),但没有给出初值,其值是不确定的。
vector<int>a(10,1); //定义了10个整型元素的向量,且给出每个元素的初值为1
vector<int>a(b); //用b向量来创建a向量,整体复制性赋值(修改a,b也会发生变化)
vector<int> a = b; //a数组是新的数组,但是数组中每个元素和b中元素相同(修改a,b不会发生变化)
vector<int>a(b.begin(), b.begin()+3); //定义了a值为b中第0个到第2个(共3个)元素(修改a,b不会发生变化)
int b[7]={1,2,3,4,5,9,8}; vector<int> a(b,b+7); //从数组中获得初值

//其中多维数组可以初始化为:(m行,n列,初始化为0)
vector<vector<int>> vec(m, vector<int>(n, 0));

2.vector对象的几个重要操作,举例说明如下:

1)a.assign(b.begin(), b.begin()+3);//b为向量,将b的0~2个元素构成的向量赋给a2)a.assign(4,2);//是a只含4个元素,且每个元素为23)a.back();//返回a的最后一个元素4)a.front();//返回a的第一个元素5)a[i]; //返回a的第i个元素,当且仅当a[i]存在2013-12-076)a.clear();//清空a中的元素7)a.empty();//判断a是否为空,空则返回ture,不空则返回false8)a.pop_back();//删除a向量的最后一个元素9)a.erase(a.begin()+1,a.begin()+3);//删除a中第1个(从第0个算起)到第2个元素,也就是说删除的元素从a.begin()+1算起(包括它)一直到a.begin()+3(不包括它)10)a.push_back(5);//在a的最后一个向量后插入一个元素,其值为511)a.insert(a.begin()+1,5);//在a的第1个元素(从第0个算起)的位置插入数值5,如a为1,2,3,4,插入元素后为1,5,2,3,412)a.insert(a.begin()+1,3,5);//在a的第1个元素(从第0个算起)的位置插入3个数,其值都为513)a.insert(a.begin()+1,b+3,b+6);//b为数组,在a的第1个元素(从第0个算起)的位置插入b的第3个元素到第5个元素(不包括b+6),如b为1,2,3,4,5,9,8,插入元素后为1,4,5,9,2,3,4,5,9,814)a.size();//返回a中元素的个数;15)a.capacity();//返回a在内存中总共可以容纳的元素个数16)a.rezize(10);//将a的现有元素个数调至10个,多则删,少则补,其值随机17)a.rezize(10,2);//将a的现有元素个数调至10个,多则删,少则补,其值为218)a.reserve(100);//将a的容量(capacity)扩充至100,也就是说现在测试a.capacity();的时候返回值是100.这种操作只有在需要给a添加大量数据的时候才 显得有意义,因为这将避免内存多次容量扩充操作(当a的容量不足时电脑会自动扩容,当然这必然降低性能) 19)a.swap(b);//b为向量,将a中的元素和b中的元素进行整体性交换20)a==b; //b为向量,向量的比较操作还有!=,>=,<=,>,<21find(element);

(22) vec.push_back(element);

3. 清空vector(是否清空占用内存)

size和capacity

size:vector容器真实大小,对应resize调整size大小,增加的元素为默认值。
capacity:预分配的内存空间,对应reserve调整capacity大小。只是调整capacity大小,内存还是野的,如果用“[]”进行访问,可能出现数组越界。

#include<vector>
#include<iostream>
using namespace std;
int main(){
	vector<int>vec;
	for (int i = 0; i<100; i++){
		vec.push_back(i);
	}
	cout << vec.size() << endl;//输出100
	cout << vec.capacity() << endl;//输出141
	vec.reserve(200);
	cout << vec.capacity() << endl;//输出200
	vec.resize(150);
	cout << vec.size() << endl;//输出150
	//cout << vec[199] << endl;//发生数组越界
}
clear和swap(清空操作)
    //1.清除元素不回收内存
	vec.clear();
	cout << vec.size() << endl;//输出0
	cout << vec.capacity() << endl;//输出141
	//2.清除元素回收内存
	vector<int>().swap(vec);//或者vec.swap(vector<int>());
	cout << vec.size() << endl;//输出0
	cout << vec.capacity() << endl;//输出0

4. return时,凭空构建vector

// 表示返回空的vec
return vector<int>();

// 空vec
return vector<int> {};

// 凭空构建vector = {1,2}
return vector<int> {1, 2};

// return类型转换后的vec
unordered_set<int> st;
...
return vector<int>(st.begin(), st.end());

面试

项目

嵌入式智能温度计

传感器获取环境温度,(驱动)内核模块加载,QT绘制环境温度曲线,网络连接和显示

read + write 操作寄存器 (写的时候用violate – 不过cache)

使用函数操作硬件

基于PYNQ的卷积神经网络实现和加速(本科毕设)

卷积神经网络**,嵌入式IP核设计,软件驱动操作IP核**(GPIO – PYNQ支持pytho写驱动程序)-- PYNQ提供了一个操作系统和python环境

read + write 操作寄存器 (写(内存)的时候要用cached = 0)

申请连续内存空间(因为IP的逻辑是物理上,但是其实板子上是dram上 – 用了一个别的函数申请连续空间)

使用函数操作硬件

主要过程:

技术面试

1. 内存分配

**一个由C/C++**编译的程序占用的内存分为以下几个部分

(1) 栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。

(2) 堆区(heap**)** — 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表,呵呵。

(3) 全局区(静态区)(static)—,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 - 程序结束后由系统释放。

(4) 文字常量区 —常量字符串就是放在这里的。 程序结束后由系统释放

(5) 程序代码区—存放函数体的二进制代码。

2. 描述一个程序从代码是如何转成机器码执行的

(0) 编写代码

键盘输入(接口,有刷新,会记录目前的地址),中断,保护现场,然后交由中断处理程程序(根据地址判断输入字符,然后输出到标准输出上),然后返回。 —— coding完毕

(1) 代码编译成可执行文件:预处理 - 编译 - 优化 - 汇编 - 链接

1.编译预处理
读取c源程序,对其中的伪指令(以#开头的指令)和特殊符号进行处理
[析] 伪指令主要包括以下四个方面
(1)宏定义指令,如#define Name TokenString,#undef等。对于前一个伪指令,预编译所要做的是将程序中的所有Name用TokenString替换,但作为字符串常量的Name则不被替换。对于后者,则将取消对某个宏的定义,使以后该串的出现不再被替换。
(2)条件编译指令,如#ifdef,#ifndef,#else,#elif,#endif,等等。这些伪指令的引入使得程序员可以通过定义不同的宏来决定编译程序对哪些代码进行处理。预编译程序将根据有关的文件,将那些不必要的代码过滤掉
(3)头文件包含指令,如#include “FileName"或者#include 等。在头文件中一般用伪指令#define定义了大量的宏(最常见的是字符常量),同时包含有各种外部符号的声明。采用头文件的目的主要是为了使某些定义可以供多个不同的C源程序使用。因为在需要用到这些定义的C源程序中,只需加上一条#include语句即可,而不必再在此文件中将这些定义重复一遍。预编译程序将把头文件中的定义统统都加入到它所产生的输出文件中,以供编译程序对之进行处理。
包含到c源程序中的头文件可以是系统提供的,这些头文件一般被放在/usr/include目录下。在程序中#include它们要使用尖括号(<>)。另外开发人员也可以定义自己的头文件,这些文件一般与c源程序放在同一目录下,此时在#include中要用双引号(”")。
(4)特殊符号,预编译程序可以识别一些特殊的符号。例如在源程序中出现的LINE标识将被解释为当前行号(十进制数),FILE则被解释为当前被编译的C源程序的名称。预编译程序对于在源程序中出现的这些串将用合适的值进行替换。
预编译程序所完成的基本上是对源程序的“替代”工作。经过此种替代,生成一个没有宏定义、没有条件编译指令、没有特殊符号的输出文件。这个文件的含义同没有经过预处理的源文件是相同的,但内容有所不同。下一步,此输出文件将作为编译程序的输出而被翻译成为机器指令。

2.编译阶段
经过预编译得到的输出文件中,将只有常量。如数字、字符串、变量的定义,以及C语言的关键字,如main,if,else,for,while,{,},+,-,*,\,等等。编译程序所要作得工作就是通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码表示或汇编代码。
在编译的过程中,所有的全局变量在内存中的标识是虚拟地址,而不是我们在开发过程中定义的名称。例如int a = 1;这里的a在汇编代码中就不存在了,取而代之的是一个地址。在汇编文件中有一个符号表,它指明了这个地址的名称为a,以及其他信息,用于以后的debug。由于并非是可执行文件(在可执行文件中所有变量、调用的地址才能真正确定),这些地址是未确定的,所以对于这些数据(变量、函数)有relocation table,需要在最后的链接过程中对全局变量、函数relocation。

3.优化阶段
优化处理是编译系统中一项比较艰深的技术。它涉及到的问题不仅同编译技术本身有关,而且同机器的硬件环境也有很大的关系。优化一部分是对中间代码的优化。这种优化不依赖于具体的计算机。另一种优化则主要针对目标代码的生成而进行的。上图中,我们将优化阶段放在编译程序的后面,这是一种比较笼统的表示。
对于前一种优化,主要的工作是删除公共表达式、循环优化(代码外提、强度削弱、变换循环控制条件、已知量的合并等)、复写传播,以及无用赋值的删除,等等。
后一种类型的优化同机器的硬件结构密切相关,最主要的是考虑是如何充分利用机器的各个硬件寄存器存放的有关变量的值,以减少对于内存的访问次数。另外,如何根据机器硬件执行指令的特点(如流水线、RISC、CISC、VLIW等)而对指令进行一些调整使目标代码比较短,执行的效率比较高,也是一个重要的研究课题。
经过优化得到的汇编代码必须经过汇编程序的汇编转换成相应的机器指令,方可能被机器执行。

4.汇编过程
汇编过程实际上指把汇编语言代码翻译成目标机器指令的过程。对于被翻译系统处理的每一个C语言源程序,都将最终经过这一处理而得到相应的目标文件。目标文件中所存放的也就是与源程序等效的目标的机器语言代码。
目标文件由段组成。通常一个目标文件中至少有两个段:
代码段  该段中所包含的主要是程序的指令。该段一般是可读和可执行的,但一般却不可写。
数据段  主要存放程序中要用到的各种全局变量或静态的数据。一般数据段都是可读,可写,可执行的。
UNIX环境下主要有三种类型的目标文件:(Linux目标文件格式为ELF类型)
(1)可重定位文件  其中包含有适合于其它目标文件链接来创建一个可执行的或者共享的目标文件的代码和数据。
(2)共享的目标文件 静态链接库和动态链接库 这种文件存放了适合于在两种上下文里链接的代码和数据。
第一种是链接程序可把它与其它可重定位文件及共享的目标文件一起处理来创建另一个目标文件;
第二种是动态链接程序将它与另一个可执行文件及其它的共享目标文件结合到一起,创建一个进程映象。
(3)可执行文件   它包含了一个可以被操作系统创建一个进程来执行之的文件。
汇编程序生成的实际上是第一种类型的目标文件。对于后两种还需要其他的一些处理方能得到,这个就是链接程序的工作了。
5.链接程序
由汇编程序生成的目标文件并不能立即就被执行,其中可能还有许多没有解决的问题。例如,某个源文件中的函数可能引用了另一个源文件中定义的某个符号(如变量或者函数调用等);在程序中可能调用了某个库文件中的函数,等等。所有的这些问题,都需要经链接程序的处理方能得以解决。
链接程序的主要工作就是将有关的目标文件彼此相连接,也即将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够诶操作系统装入执行的统一整体。
根据开发人员指定的同库函数的链接方式的不同,链接处理可分为两种:

(3) 执行阶段 shell

(4) 缺页中断?

(5) 系统调用阶段 Trap

系统调用被刻意设计的看起来像是函数调用,但是背后的user/kernel转换比函数调用要复杂的多。之所以这么复杂,很大一部分原因是要保持user/kernel之间的隔离性,内核不能信任来自用户空间的任何内容。

这个helo.c程序里面,使用了write(1, "helo\n", 5); – write其实是通过SYS_write函数的系统调用实现的。

.global write
write:
 li a7, SYS_write
 ecall
 ret

Trap的时候,我们需要做什么?(当前处于user mode,现在需要执行系统调用)

  • 保存32个用户寄存器
  • 保存当前PC
  • mode切换成supervisor
  • SATP从user page table切换成kernel_pagetable
  • Stack Frame和Stack Pointer都需要改变,因为需要一个stack来调用kernel的函数
  • 跳入kernel
  1. 对于write,三个参数,分别保存到a0,a1,a2寄存器,这个时候代码还运行在用户态,用的还是用户pagetable
  2. 开始执行ecall指令,不会切换page table(即还是使用user pagetable),但是每个user pagetable都有一个trampoline page,是由内核小心的映射到每一个user page table中,以使得当我们仍然在使用user page table时,内核在一个地方能够执行trap机制的最开始的一些指令。
  3. 保存用户寄存器内容 (uservec) – 保存到trampoline page中的trapframe中。
    (1)静态链接 在这种链接方式下,函数的代码将从其所在地静态链接库中被拷贝到最终的可执行程序中。这样该程序在被执行时这些代码将被装入到该进程的虚拟地址空间中。静态链接库实际上是一个目标文件的集合,其中的每个文件含有库中的一个或者一组相关函数的代码。(个人备注:静态链接将链接库的代码复制到可执行程序中,使得可执行程序体积变大)
    (2)动态链接  在此种方式下,函数的代码被放到称作是动态链接库或共享对象的某个目标文件中。链接程序此时所作的只是在最终的可执行程序中记录下共享对象的名字以及其它少量的登记信息。在此可执行文件被执行时,动态链接库的全部内容将被映射到运行时相应进程的虚地址空间。动态链接程序将根据可执行程序中记录的信息找到相应的函数代码。(个人备注:动态链接指的是需要链接的代码放到一个共享对象中,共享对象映射到进程虚地址空间,链接程序记录可执行程序将来需要用的代码信息,根据这些信息迅速定位相应的代码片段。)

对于可执行文件中的函数调用,可分别采用动态链接或静态链接的方法。使用动态链接能够使最终的可执行文件比较短小,并且当共享对象被多个进程使用时能节约一些内存,因为在内存中只需要保存一份此共享对象的代码。但并不是使用动态链接就一定比使用静态链接要优越。在某些情况下动态链接可能带来一些性能上损害。

经过上述五个过程,C源程序就最终被转换成可执行文件了。缺省(默认)情况下这个可执行文件的名字被命名为a.out。
————————————————
原文链接:https://blog.csdn.net/dreamispossible/article/details/89389181

静态变量的作用域,生命周期,存储位置

主管面试

主要思想是介绍自己,然后主管会根据介绍问一些问题,当然也可能是职业规划。

自我介绍

  1. 背景

    面试官你好,首先我来说一下基础背景。我叫魏灿,是哈工大深圳的研二硕士,本硕都是哈工大计算机的。研究生阶段的主要研究方向,大方向是存储,举例来说是冗余数据的消除,比较偏向底层,常用语言是C和C++(不过这里我总结我的C++主要是C数据结构 + STL,用的C++特性比较少)

  2. 项目

    接下来主要说一下项目经历,本科的时候主要是以课设为主,语言、项目都比较杂。有java写的数独游戏,php、python、html写的网页,还有嵌入式C语言的温度计,python、c写的嵌入式的卷积申请网络等。研究生阶段的项目主要有两个,一个是差量同步系统(简单来说就是同步新版本数据到客户端做备份,要求是速度越快越好)这个项目发了一篇CCF-A类的论文;研究生阶段的另一个项目是差量压缩压缩率的估计,简单来说就是在不进行压缩的情况下,估计压缩算法的压缩率,主要是用了很多哈希,哈希桶,链表等。

具体询问环节可能出现的问题

1. 困难的事情,如何解决的?

研究生阶段做的项目,周期都比较长,经常会越到许多难以解决的问题。比如实验效果不好,如何改进(这种开放性的问题是很令人头疼的,毕竟无从下手)。有时候会在一个技术点上卡住一天,甚至几天,一般这个时候的解决办法不再是无脑冲代码,而是通过文档,或者周报总结,给出可能的技术点和策略,先选择最简单的去做改进,一个个来,在时间有限的情况下,想做到突破,可能追求广度,而不是深度是个不错的选择。还有就是刚开始构建一个项目的时候,能跑起来实现最基础的功能是很重要的,像我的项目,刚开始一周就差不多写出来框架,能跑了,当然后面可能会花一两周的时间重构,保持后面修改和测试更轻松。

2. 什么课程印象深刻?

汇编和微机原理。

本科时候的计算机课程,是一个系统,让我们基本了解计算机,认识计算机。刚开始是语言课,主要是工具,后面是数据结构,算法,是思想,再往后是计算机网络、编译原理、操作系统、体系结构,是小系统,这个时候基本了解了计算机,但是还是对计算机如何操作硬件,以及一些课程里面的经典描述(比如中断)不够清晰。汇编和微机原理,写了很多汇编程序,接口,驱动程序等,有点直接操作计算机的意思,同时理解了跳跃表,中断向量(中断服务程序的入口地址或存放中断服务程序的首地址)是怎么回事,中断的保护现场(主要是寄存器暂存),缓存刷新,硬件刷新频率,感觉对编译原理,计算机系统,优化,等都有很好的帮助,而且当时实验很多,上课和考试以写代码为主。

标准流程图画法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DBZNVZRq-1658928266700)(https://gitee.com/LEVI-Tempest/picgo-typora/raw/master/image-20220429195729449.png)]

处理而得到相应的目标文件。目标文件中所存放的也就是与源程序等效的目标的机器语言代码。
目标文件由段组成。通常一个目标文件中至少有两个段:
代码段  该段中所包含的主要是程序的指令。该段一般是可读和可执行的,但一般却不可写。
数据段  主要存放程序中要用到的各种全局变量或静态的数据。一般数据段都是可读,可写,可执行的。
UNIX环境下主要有三种类型的目标文件:(Linux目标文件格式为ELF类型)
(1)可重定位文件  其中包含有适合于其它目标文件链接来创建一个可执行的或者共享的目标文件的代码和数据。
(2)共享的目标文件 静态链接库和动态链接库 这种文件存放了适合于在两种上下文里链接的代码和数据。
第一种是链接程序可把它与其它可重定位文件及共享的目标文件一起处理来创建另一个目标文件;
第二种是动态链接程序将它与另一个可执行文件及其它的共享目标文件结合到一起,创建一个进程映象。
(3)可执行文件   它包含了一个可以被操作系统创建一个进程来执行之的文件。
汇编程序生成的实际上是第一种类型的目标文件。对于后两种还需要其他的一些处理方能得到,这个就是链接程序的工作了。
5.链接程序
由汇编程序生成的目标文件并不能立即就被执行,其中可能还有许多没有解决的问题。例如,某个源文件中的函数可能引用了另一个源文件中定义的某个符号(如变量或者函数调用等);在程序中可能调用了某个库文件中的函数,等等。所有的这些问题,都需要经链接程序的处理方能得以解决。
链接程序的主要工作就是将有关的目标文件彼此相连接,也即将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够诶操作系统装入执行的统一整体。
根据开发人员指定的同库函数的链接方式的不同,链接处理可分为两种:

(3) 执行阶段 shell

(4) 缺页中断?

(5) 系统调用阶段 Trap

系统调用被刻意设计的看起来像是函数调用,但是背后的user/kernel转换比函数调用要复杂的多。之所以这么复杂,很大一部分原因是要保持user/kernel之间的隔离性,内核不能信任来自用户空间的任何内容。

这个helo.c程序里面,使用了write(1, "helo\n", 5); – write其实是通过SYS_write函数的系统调用实现的。

.global write
write:
 li a7, SYS_write
 ecall
 ret

Trap的时候,我们需要做什么?(当前处于user mode,现在需要执行系统调用)

  • 保存32个用户寄存器
  • 保存当前PC
  • mode切换成supervisor
  • SATP从user page table切换成kernel_pagetable
  • Stack Frame和Stack Pointer都需要改变,因为需要一个stack来调用kernel的函数
  • 跳入kernel
  1. 对于write,三个参数,分别保存到a0,a1,a2寄存器,这个时候代码还运行在用户态,用的还是用户pagetable
  2. 开始执行ecall指令,不会切换page table(即还是使用user pagetable),但是每个user pagetable都有一个trampoline page,是由内核小心的映射到每一个user page table中,以使得当我们仍然在使用user page table时,内核在一个地方能够执行trap机制的最开始的一些指令。
  3. 保存用户寄存器内容 (uservec) – 保存到trampoline page中的trapframe中。
    (1)静态链接 在这种链接方式下,函数的代码将从其所在地静态链接库中被拷贝到最终的可执行程序中。这样该程序在被执行时这些代码将被装入到该进程的虚拟地址空间中。静态链接库实际上是一个目标文件的集合,其中的每个文件含有库中的一个或者一组相关函数的代码。(个人备注:静态链接将链接库的代码复制到可执行程序中,使得可执行程序体积变大)
    (2)动态链接  在此种方式下,函数的代码被放到称作是动态链接库或共享对象的某个目标文件中。链接程序此时所作的只是在最终的可执行程序中记录下共享对象的名字以及其它少量的登记信息。在此可执行文件被执行时,动态链接库的全部内容将被映射到运行时相应进程的虚地址空间。动态链接程序将根据可执行程序中记录的信息找到相应的函数代码。(个人备注:动态链接指的是需要链接的代码放到一个共享对象中,共享对象映射到进程虚地址空间,链接程序记录可执行程序将来需要用的代码信息,根据这些信息迅速定位相应的代码片段。)

对于可执行文件中的函数调用,可分别采用动态链接或静态链接的方法。使用动态链接能够使最终的可执行文件比较短小,并且当共享对象被多个进程使用时能节约一些内存,因为在内存中只需要保存一份此共享对象的代码。但并不是使用动态链接就一定比使用静态链接要优越。在某些情况下动态链接可能带来一些性能上损害。

经过上述五个过程,C源程序就最终被转换成可执行文件了。缺省(默认)情况下这个可执行文件的名字被命名为a.out。
————————————————
原文链接:https://blog.csdn.net/dreamispossible/article/details/89389181

静态变量的作用域,生命周期,存储位置

主管面试

主要思想是介绍自己,然后主管会根据介绍问一些问题,当然也可能是职业规划。

自我介绍

  1. 背景

    面试官你好,首先我来说一下基础背景。我叫魏灿,是哈工大深圳的研二硕士,本硕都是哈工大计算机的。研究生阶段的主要研究方向,大方向是存储,举例来说是冗余数据的消除,比较偏向底层,常用语言是C和C++(不过这里我总结我的C++主要是C数据结构 + STL,用的C++特性比较少)

  2. 项目

    接下来主要说一下项目经历,本科的时候主要是以课设为主,语言、项目都比较杂。有java写的数独游戏,php、python、html写的网页,还有嵌入式C语言的温度计,python、c写的嵌入式的卷积申请网络等。研究生阶段的项目主要有两个,一个是差量同步系统(简单来说就是同步新版本数据到客户端做备份,要求是速度越快越好)这个项目发了一篇CCF-A类的论文;研究生阶段的另一个项目是差量压缩压缩率的估计,简单来说就是在不进行压缩的情况下,估计压缩算法的压缩率,主要是用了很多哈希,哈希桶,链表等。

具体询问环节可能出现的问题

1. 困难的事情,如何解决的?

研究生阶段做的项目,周期都比较长,经常会越到许多难以解决的问题。比如实验效果不好,如何改进(这种开放性的问题是很令人头疼的,毕竟无从下手)。有时候会在一个技术点上卡住一天,甚至几天,一般这个时候的解决办法不再是无脑冲代码,而是通过文档,或者周报总结,给出可能的技术点和策略,先选择最简单的去做改进,一个个来,在时间有限的情况下,想做到突破,可能追求广度,而不是深度是个不错的选择。还有就是刚开始构建一个项目的时候,能跑起来实现最基础的功能是很重要的,像我的项目,刚开始一周就差不多写出来框架,能跑了,当然后面可能会花一两周的时间重构,保持后面修改和测试更轻松。

2. 什么课程印象深刻?

汇编和微机原理。

本科时候的计算机课程,是一个系统,让我们基本了解计算机,认识计算机。刚开始是语言课,主要是工具,后面是数据结构,算法,是思想,再往后是计算机网络、编译原理、操作系统、体系结构,是小系统,这个时候基本了解了计算机,但是还是对计算机如何操作硬件,以及一些课程里面的经典描述(比如中断)不够清晰。汇编和微机原理,写了很多汇编程序,接口,驱动程序等,有点直接操作计算机的意思,同时理解了跳跃表,中断向量(中断服务程序的入口地址或存放中断服务程序的首地址)是怎么回事,中断的保护现场(主要是寄存器暂存),缓存刷新,硬件刷新频率,感觉对编译原理,计算机系统,优化,等都有很好的帮助,而且当时实验很多,上课和考试以写代码为主。

标准流程图画法

[外链图片转存中…(img-DBZNVZRq-1658928266700)]

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值