该训练营为蓝蓝考研(蓝颜知己)的算法训练营内容,题目来源有经典算法题、408统考算法题等等。分为7weeks共计39days练习,每日一道算法题训练,涵盖基本顺序表、链表和二叉树相关的基础算法,以及部分408真题中数据结构部分的算法题,并未包含图相关的算法,还需要进一步对图算法自行学习。 当然,对于顺序表、链表和二叉树的算法,也需要继续通过课后习题进行练习和巩固。
github项目地址–AlgorithmTrainingCamp
该训练合集仅包含2009、2010、2011、2013、2014、2017、2018、2020年真题。
剩余年份真题暴力题解补充见末尾。
- 目录
week1
week2
week3
week4(含2010、2013、2018)
week5(含2009、2020)
week6
week7(含2014、2017)
剩余年份真题补充(暴力)
2012 – 2015 – 2016 – 2019 – 2021 – 2022 – 2023 – 2024
如果时间充裕,可以前期进行PC端练习,后期再尝试完全在纸上进行手写练习;如果时间紧张则不建议PC端练习,直接手写。
个人最初是使用的Clion进行coding练习的,所以这些题目是可以成功测试运行的。所涉及的相关结构体及函数定义如下所示:
- 线性表List
- 顺序表SqList
- 头文件SqList.h
- 顺序表SqList
// SqList.h
// Created by Giperx on 2023/7/24.
//
#ifndef ALGCAMP_SEQLIST_H
#define ALGCAMP_SEQLIST_H
// 顺序表
typedef struct SqList{
int data[100];
int length;
}SqList;
// 初始化顺序表
void initSqList(SqList &list);
// 打印输出顺序表
void printSqList(SqList &list);
#endif //ALGCAMP_SEQLIST_H
- 线性表List
- 顺序表SqList
- SqList.cpp
- 顺序表SqList
//SqList.cpp
// Created by Giperx on 2023/7/27.
//
#include <iostream>
#include "SqList.h"
using namespace std;
void initSqList(SqList &list){
cout << "enter length of SqList:";
cin >> list.length;
cout << "enter values of SqList:";
for(int i = 0; i < list.length; i ++) cin >> list.data[i];
}
void printSqList(SqList &list){
cout << "length: " << list.length << endl;
for(int i = 0; i < list.length; i ++) cout << list.data[i] << ' ';
cout << endl;
}
- 线性表List
- 链表LinkList
- 头文件LinkList.h
- 链表LinkList
//LinkList.h
// Created by Giperx on 2023/8/15.
//
#ifndef ALGCAMP_LINKLIST_H
#define ALGCAMP_LINKLIST_H
typedef struct LNode{
int data;
struct LNode *next;
LNode(int val){
data = val;
next = nullptr;
}
}LNode, *LinkList;
// 初始化链表,带头结点
bool initLinkList(LinkList& L);
// 计算链表长度(不包括头结点)
int lengthofLinkList(LinkList& L);
// 输出链表信息
void printLinkList(LinkList& L);
#endif //ALGCAMP_LINKLIST_H
- 线性表List
- 链表LinkList
- LinkList.cpp
//LinkList.cpp
// Created by Giperx on 2023/8/15.
//
#include "LinkList.h"
#include "malloc.h"
#include "iostream"
using namespace std;
// 初始化链表,带头结点
bool initLinkList(LinkList& L){
L = (LNode*)malloc(sizeof (LNode));
if (!L) return false;
L->next = nullptr;
return true;
}
// 计算链表长度(不包括头结点)
int lengthofLinkList(LinkList& L){
int length = 0;
if (!L){
cout << "nullptr!" << endl;
} else if (!L->next) {
cout << "have not node!" << endl;
} else {
LNode *p = L->next;
while (p){
length++, p = p->next;
}
}
return length;
}
// 输出链表信息
void printLinkList(LinkList& L){
if (!L) cout << "nullptr!" << endl;
else if (!L->next) cout << "have not node!" << endl;
else{
cout << "length of LinkList:" << lengthofLinkList(L) << endl;
LNode *p = L->next;
while (p){
cout << p->data << ' ';
p = p->next;
}
cout << endl;
}
}
- 二叉树
- BiTree.h
//BiTree.h
// Created by Giperx on 2023/8/24.
//
#ifndef ALGCAMP_BITREE_H
#define ALGCAMP_BITREE_H
// 二叉树
typedef struct BiNode{
char data;
struct BiNode* left, *right;
}BiNode, *BiTree;
#endif //ALGCAMP_BITREE_H
- 二叉树
- BiTree.cpp
//BiTree.cpp
// Created by Giperx on 2023/8/24.
//
#include "BiTree.h"
- week1 回到目录
day1 累加求和
简单的for循环实现累计求和:1+2+3+4+…+10
day1 回到week1
#include<stdio.h>
int main(){
int sum = 0;
for(int i = 1; i <= 10; i ++){
sum += i;
}
printf("%d", sum);
return 0;
}
day2 字符串反转
day2 回到week1
#include<iostream>
#include<cstring>
using namespace std;
int main(){
char str[1001];
cin >> str;
for(int i = std::strlen(str) - 1; i >= 0; i --){
cout << str[i];
}
return 0;
}
实际上,C语言中可以利用string.h中的strrev(char*)函数进行反转。
#include<string.h>
strrev(str)
C++中algorithm头文件中的reverse(_BIter, _BIter)也可以对stringvector也可以
实现反转。
#include<algorithm>
reverse(str.begin(), str.end())
day3 顺序表删除最小并用尾数填补
day3 回到week1
思路
- 遍历顺序表,同时记录下每次最小的元素的值和下标位置,最后用尾部元素填补,并返回值。
答案
#include <iostream>
#include <vector>
using namespace std;
int removeMinValue(vector<int>& sequence) {
if (sequence.empty()) {
cerr << "顺序表为空!" << endl;
return -1; // 返回-1表⽰错误
}
int minInd = 0;
int minVal = sequence[0];
for (int i = 1; 1 < sequence.size(); i++) {
// ⽐较获取最⼩值和其索引
if(sequence[i] < minVal) {
minVal = sequence[i];
minInd = i;
}
}
int deletedVal = sequence[minInd];
sequence[minInd] = squence.back(); // 最后⼀个元素填补删除位
sequence.pop_back(); //删除最后⼀个元素
return deletedVal; // 返回删除值
}
- week2 回到目录
day4 顺序表逆置
答案回到week2
//直接下标处理完成交换
void reverse(Sqlist &L){
int tmp;//交换变量
for(int i = 0;i<L.length/2;i++){
tmp = L[i];
L[i] = L[L.length-1-i];
L[L.length-1-i] = tmp;
}
}
//双指针⽅式处理 完整代码
void reverse2(int arr[] , int len) {
int* first = arr;//指向第⼀个元素
int* end = first + len -1;//指向尾部元素
//只要前⾯⼩于后⾯就循环交换
while( first < end ) {
int temp = *first;
*first++ = *end;
*end-- = temp;
}
}
//len为数组⻓度
void reverse2(int arr[] , int len) {
//先把前指针指向数组头部
int* first = arr;、
//把后指针指向数组尾部
int* end = first + len -1;
while( first < end ) {
int temp = *first;
/*
这⾥优先级不⼀样
实际执⾏顺序应该是把end指针指向的元素赋给first指针指向的元素,然后再把first指针往后挪(*first的优先级>first++)
也就是相当于执⾏:*first = *end; first++;
*/
*first++ = *end;
/*
这步骤可能有个⼩疑问,既然两个指针要替换为什么不同时first++。end--呢(*first++ = *end--;)?
因为我们下⼀步还需要把temp的值赋给*end,如果提前把end--了,就相当于把temp赋给了end之前的那个元素,此时我们应该把temp赋
*/
*end-- = temp;
}
}
day5 删除所有值为x的元素
day08
思路尝试
p1用来保存覆盖位置,p2作for循环变量遍历数组
p2和p1同时从0开始遍历,遇到非x元素,则让data[p2]覆盖到data[p1]位置, 并且p1++;
否则跳过当前p2元素值为x
,并记录cnt++,继续往后遍历到上限length
length -= cnt长度去除值为x的元素的个数
答案回到week2
void deleteX(SqList &list, int x){
int p1 = 0, p2 = 0, cnt = 0;
for(; p2 < list.length; p2++){
//出现x则跳过
if (list.data[p2] == x) {
cnt++;
}
// 否则 将非x元素前移到p1处
else{
list.data[p1] = list.data[p2]
p1++;
}
}
// length变化
list.length -= cnt;
}
day6 删除下标范围内所有元素
思路尝试
双指针法,p1一个指向范围(i, j)的i,p2指向j+1,同时先后移动,并把data[p2]覆盖到p1位置上。
length -= (j - 1 + 1)
代码尝试回到week2
void deleteRange(SqList &list, int i, int j){
if(i < 0 || j >= list.length) return;
int p1 = i, p2 = j + 1;
while (p2 < list.length){
list.data[p1] = list.data[p2];
p1++, p2++;
}
list.length -= (j - i + 1);
}
day7 依照表头元素划分数组为左小右大两半
思路尝试
快排的划分操作,选取表头作哨兵,p1指向表头+1,p2指向表尾,双指针往内移动。
先从右指针p2开始往左移动,直到找到比哨兵(表头)小或等于的元素;
然后p1往右移动,直到找到比哨兵(表头)大的元素。
p1 < p2
交换p1和p2的元素。
循环操作,直到 p1 > p2
, 此时再将哨兵(表头)覆盖p2位置。
代码尝试回到week2
void partition(SqList &list){
int mid = list.data[0], p1 = 1, p2 = list.length - 1;
while(p1 <= p2){
while(p1 <= p2 && list.data[p2] > mid) p2--;
while (p1 <= p2 && list.data[p1] <= mid) p1++;
if(p1 < p2){
swap(list.data[p1], list.data[p2]);
}
}
swap(list.data[0], list.data[p2]);
}
思路尝试
类似day05顺序表中删除所有值为x的元素day05
双指针
遍历顺序表,p1保存位置,负责记录每次将要被覆盖的位置;
p2向后遍历,遇到范围内的元素直接跳过并且cnt+1,否则(不在范围内)
覆盖到p1位置,并p1++
直到p2==length退出循环
表长length -= cnt
代码尝试回到week2
void deleteValueRange(SqList &list, int a, int b){
if (a >= b || !list.length){
cout << "error!(must a < b and list.length != 0)" << endl;
return;
}
int p1 = 0, p2 = 0, cnt = 0;
while(p2 < list.length){
if (list.data[p2] < a || list.data[p2] > b){
list.data[p1] = list.data[p2];
p1++, p2++;
}
else{
cnt++, p2++;
}
}
list.length -= cnt;
}
day9 有序顺序表去重
思路尝试
与day8主体思路相似,双指针,p1指向每次将要被覆盖的位置,p2依次后移
p1 p2 从 1 开始
cmp为每次比较的元素值,初始化为data[0],每次p2移动后,则更新cmp为p2前一个值
p2移动过程中,如果cmp == data[p2], 则p2++, cnt++ 记录被删除(重复元素)的个数
否则 data[p1] = data[p2], 并 p1++, p2++
直到p2 == length
最后 length -= cnt
代码尝试回到week2
void deleteDuplicate(SqList &list){
if (!list.length){
cout << "error!" << endl;
return;
}
int p1 = 1, p2 = 1, cnt = 0, cmp = list.data[0];
while(p2 < list.length){
if (cmp == list.data[p2]) {
p2++, cnt++;
}
else{
list.data[p1] = list.data[p2];
cmp = list.data[p1];
p1++, p2++;
}
}
list.length -= cnt;
}
-
- week2小结
day04 - day09
- week2小结
主要是双指针的运用,day04顺序表逆置,左右交换;
day06删除下标范围内的元素,相当于两顺序表的前后合并;
day05删除所有值为x的元素、day08删除值范围内的元素、day09有序表去重是相同的,双指针,一个负责记录位置,一个负责移动判断。只不过前两个的判定对比是定下来的元素,去重则是每次判定对比的元素可能会发生变化,也就是最近的一个元素;
day07依表头划分左右两半partition实际上是快排算法的基础,通过双指针和哨兵进行比较再交换。
- week3 回到目录
day10 有序表删除值范围内元素
思路尝试
先判定给定范围是否合法,以及顺序表是否非空
遍历顺序表,找到给定值范围的顺序表的下标范围i、j(包括i、j上的元素)
如果给定范围b超过顺序表最大元素,则b == length - 1
调用day06的删除下标范围内所有元素函数deleteRange(SqList &list, int i, int j)
代码尝试回到week3
void deleteSortedValueRange(SqList &list, int a, int b){
if (a >= b || !list.length){
cout << "error!" << endl;
return;
}
int i = -1, j = -1;
for (int k = 0; k < list.length; ++k) {
// 第一个 >= a的元素的下标
if (i == -1 && list.data[k] >= a) i = k;
// 最后一个 <= 元素的下标
if (j == -1 && list.data[k] > b) j = k - 1;
}
// b超过 顺序表最大元素的值
if (j == -1) j = list.length - 1;
// 调用 删除下标范围内所有元素
// 若 i == -1 说明整个顺序表的元素都 < a
// deleteRange函数不做处理
deleteRange(list, i, j);
}
day11 有序表合并
思路尝试
双指针,p1指向list1,p2指向list2, new 一个SqList作为存储tmp, cnt = 0作为tmp的下标
依次进行对比,如果list1.data[p1] <= list2.data[p2] 则tmp.data[cnt++] = list1.data[p1++]
否则 (list1.data[p1] > list2.data[p2])
tmp.data[cnt++] = tmp.data[p2++]
p1或p2到达各自length时退出
没有到达尾部的list则继续全部移动到tmp
代码尝试回到week3
SqList* CombineSqList(SqList &list1, SqList &list2){
SqList *list3 = new SqList;
int p1 = 0, p2 = 0, cnt = 0;
while (p1 < list1.length && p2 < list2.length){
if (list1.data[p1] <= list2.data[p2]){
list3->data[cnt] = list1.data[p1];
p1++, cnt++;
} else{
list3->data[cnt] = list2.data[p2];
p2++, cnt++;
}
}
while(p1 < list1.length) list3->data[cnt++] = list1.data[p1++];
while(p2 < list2.length) list3->data[cnt++] = list2.data[p2++];
list3->length = cnt;
return list3;
}
day12 数组内两顺序表整体换位
思路尝试
先对前一个顺序表进行逆置,再对后一个顺序表逆置,最后整体进行逆置。
代码尝试回到week3
void superReverse(SqList &list, int l1, int l2){
int tmp = 0, i = 0, j = l1;
// 逆置 前一个顺序表
for(; i < l1/2; i ++){
tmp = list.data[i];
list.data[i] = list.data[l1 - i - 1];
list.data[l1 - i - 1] = tmp;
}
// 逆置后一个顺序表
for(; j < (list.length + l1)/2; j ++){
tmp = list.data[j];
list.data[j] = list.data[(list.length - 1) - (j - l1)];
list.data[(list.length - 1) - (j - l1)] = tmp;
}
// 整体逆置
for(i = 0; i < list.length / 2; i ++){
tmp = list.data[i];
list.data[i] = list.data[list.length - i - 1];
list.data[list.length - i - 1] = tmp;
}
}
day13 行列有序数组的元素查找
思路尝试
从左下角开始查找
元素等于target就返回true,
元素大于target就往上走 即 cow --
元素小于target就往右走 即 col ++
超出边界则返回flase
代码尝试回到week3
bool searchInSortedArray(vector<vector<int>> arr, int target){
// 从左下角元素开始往右上方搜索
int row = arr.size() - 1, col = 0;
while(row >= 0 && col < arr[0].size()){
if (arr[row][col] == target) return true;
else if (arr[row][col] < target) col ++;
else row --;
}
return false;
}
day 14 折半查找
代码尝试回到week3
int binarySearch(vector<int> arr, int target){
int l = 0, r = arr.size() - 1, mid;
while(l <= r){
mid = (r - l)/2 + l;
if (arr[mid] == target) return mid;
else if (arr[mid] < target) l = mid + 1;
else r = mid - 1;
}
return -1;
}
day 15 第k小
思路尝试
可以通过冒泡最小,循环k次。
或者利用排序解决,对整个数组进行排序;
或者利用小根堆,先建堆,然后弹出k次。
下面尝试利用快排思想:
- 第一次确定枢纽元素pivot位置后,判断该位置是否为所找位置,否则对左半区域或右半区域继续递归快排,直到找到位置为止。因为找第k小,而且k从1开始增大(数组下标从0开始增大),找前k个数出来就是第k小。
- 同理,如果是找第k大,则寻找第n-k+1小。
代码尝试回到week3
int quick_sort(vector<int> &q,int l, int r, int k){
if(l >= r) return q[l];
int x = q[l + r >> 1];
int i = l-1;
int j = r+1;
while(i<j){
do i++; while(q[i]<x);
do j--; while(q[j]>x);
if(i < j) swap(q[i],q[j]);
}
if(j - l + 1 >= k) return quick_sort(q, l, j,k);
else return quick_sort(q, j+1, r, k-(j-l+1));
}
//下为第k小的模板,查找第k大替换为quick_sort(q, 0, q.size()-1, n-K+1);
int findKth(vector<int> q, int n, int K) {
return quick_sort(q, 0, q.size()-1, K);
}
堆排序版本
//调整堆
void down(int u, int len){
int t = u;
// 把更小的数往上走,更大的数向下走
if (2 * u <= len && num[2 * u] < num[t]) t = 2 * u;
if ((2 * u + 1) <= len && num[2 * u + 1] < num[t]) t = 2 * u + 1;
if (t != u){
swap(num[t], num[u]);
down(t, len);
}
}
int main(){
int n, k, len;
cin >> n;
len = n;
//下标从1开始
for(int i = 1; i <= n; i++) cin >> num[i];
// 构建堆
for(int i = n/2; i >= 1; i--) down(i, len);
cin >> k;
for(int i = 1; i < k; i++){ //第k小
swap(num[1], num[len]);
len--;
down(1, len);
}
cout << num[1];// 堆顶即为第k小
// for(int i = 1; i <= n; i++) cout << endl << num[i] << ' ';
return 0;
}
-
- week3小结
day010 - day15
day10-day14 都涉及到对于顺序表的有序操作,day15则是经典算法题
- week3小结
- day10、day13、day14查找相关,
day10关键是寻找给定元素值区间内的
下标范围,遍历即可;
day13是在二维行列有序 但不是整体有序
的数组中寻target,虽然不是整体依次遍历,改变思路,对行和列进行遍历 换行、换列
;
day14正常二分背模板;
- day11、day12是同时两个顺序表操作
day11是两有序顺序表合并新的有序表,经典题目
day12是前后换位,局部逆置+整体逆置 = 内部不变、局部换位
- day15 是第k小,以前刷过第k大,原理相同。两种思路,排序以后输出,或者排序过程中找出来。
对于后者,可利用冒泡或者堆排序,也可以利用快排性质,每次分割以后,只去对目标位置所在那一半进行操作,不断切割。
- week4 回到目录
day16 冒泡排序&选择排序
思路尝试
- 冒泡排序属于交换排序,交换排序还有快排。
冒泡排序时间复杂度O(n^2) 稳定算法,适用于基本有序时 (基本有序时还可以用直接插入)
- 简单选择排序只有选择过程,每次确定一个元素正确位置时,只需要进行一次交换操作。选择排序还有堆排序。
简单选择排序时间复杂度O(n^2),不稳定算法 (会进行交换操作)
,适用于n较小或记录本身信息量较大时 (相较于直接插入排序,简单选择排序移动次数更少)
代码尝试回到week4
// 冒泡排序
void bubbleSort(int arr[], int n){
int tmp;
// 寻找n-1轮
for (int i = 0; i < n - 1; ++i) {
// 每次可以确定末尾的一个位置
for (int j = 0; j < n - i - 1; ++j) {
if (arr[j] > arr[j + 1]){
// 交换
tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
}
}
}
// 简单选择排序
void selectSort(int arr[], int n){
int tmp, mintmp;
// 选择n-1轮
for (int i = 0; i < n - 1; ++i) {
mintmp = i;
for (int j = i + 1; j < n; ++j) {
// 更小值,记录位置
if (arr[j] < arr[mintmp]) mintmp = j;
}
// 交换
tmp = arr[i];
arr[i] = arr[mintmp];
arr[mintmp] = tmp;
}
}
day17 快排&直接插入排序
思路尝试
- 快排属于交换排序,交换排序还有
冒泡排序
快排不稳定,时间复杂度O(n*logn),适合n较大时候;day15
- 直接插入排序 属于插入排序,需要移动一批元素
(每次将一个待排序的记录插入前面已有序子序列,还有折半插入和希尔排序)
直接插入排序是稳定的,时间复杂度O(n^2),适合n较小时候 (也可以用简单选择)
和 基本有序 (也可以用冒泡)
代码尝试回到week4
// 直接插入排序
void insertSort(int arr[], int n){
for (int i = 1; i < n; ++i) {
int key = arr[i]; // 哨兵
int sub = i - 1;
// 从后往前,找到正确位置
while (sub >= 0 && arr[sub] > key){
// 依次后移
arr[sub + 1] = arr[sub];
sub--;
}
// 此时arr[sub] <= key, arr[sub + 1]元素已经后移了
arr[sub + 1] = key;
}
}
// 快排
void quickSort(int arr[], int low, int high){
if (low >= high) return;
// partition
int l = low, r = high;
int povit = arr[l];
while(l < r){
while (l < r && arr[r] >= povit) r--;
arr[l] = arr[r];
while (l < r && arr[l] <= povit) l++;
arr[r] = arr[l];
}
// 此时l == r
arr[l] = povit;
// sort
quickSort(arr, low, l - 1);
quickSort(arr, l + 1, high);
}
day18 一维数组循环左移(2010)
思路尝试
- 常规:暴力。
时间O(n) 空间O(n)
利用辅组数组,先存下前p个元素,然后让原数组左移动p位,最后再把辅组数组内元素覆盖原数组后p位。
- 局部逆置+整体逆置。
时间O(n) 空间O(1)
和day12思路一样。相当于把前一个顺序表和后一个顺序表进行换位置。
代码尝试回到week4
void reverseArray(int arr[], int low, int high){
int tmp;
while (low < high){
tmp = arr[low];
arr[low] = arr[high];
arr[high] = tmp;
low++, high--;
}
}
void leftRotate(int arr[], int n, int p){
if (p > n || p < 0) return;
reverseArray(arr, 0, p - 1);
reverseArray(arr, p, n - 1);
reverseArray(arr, 0, n - 1);
}
day19 求得中位数(2011)
思路尝试
- 双指针 时间
O(n+m)
空间O(1)
pa指向序列A的第一个元素,pb指向序列b的第二个元素,cnt记录移动次数;
pa和pb的元素进行比较,pa <= pb则pa++后移动,否则pb++后移动,每次移动后cnt++计数;
每次移动计数后,特判cnt是否等于单个序列长度length(A和B长度相同)
,直接return。
代码尝试回到week4
int SerachMiddleInTwo(int arrA[], int arrB[], int length){
int pa = 0, pb = 0, cnt = 0;
while(pa < length && pb < length){
if (arrA[pa] <= arrB[pb]){
cnt++;
if (cnt == length) return arrA[pa];
pa++;
}
else{
cnt++;
if (cnt == length) return arrB[pb];
pb++;
}
}
}
day20 求主元素
思路尝试
- 投票算法 时间
O(n)
空间O(1)
- 通过不断抵消不同元素,来寻找候选主元素。
初始化时,cand = arr[0], cnt = 1;
从第二个元素开始遍历,如果和cand相同则cnt++,否则cnt–;
每当cnt==0时,则更新cand为当前元素,并置cnt=1。
最后再重新遍历数组,计算次数,如果>n/2则return cand,否则return -1.
代码尝试回到week4
int findMajorityElement(int arr[], int n){
if (!n) return -1;
int cand = arr[0], cnt = 1;
for (int i = 1; i < n; ++i) {
if (arr[i] == cand) cnt++;
else cnt--;
if (!cnt){
cand = arr[i];
cnt = 1;
}
}
cnt = 0;
for (int i = 0; i < n; ++i) {
if (arr[i] == cand) cnt++;
}
if (cnt > (n/2)) return cand;
else return -1;
}
day21 数组未出现的最小正整数(2018)
思路尝试
- 时间
O(n)
空间O(n)
开辟长度为arr长度n的辅助数组tmp,用来记录出现的正整数。
遍历数组,出现的如果是 <= 0的元素或者 > n的元素直接跳过;
否则将 tmp[arr[i] - 1] 置为1.
最后再遍历一遍辅助数组tmp,如果出现0则直接return i+1;
否则 return n+1.
代码尝试回到week4
int findMinNotIn(int arr[], int n){
if (!n) return 0;
int *tmp = (int*)malloc(4 * n);
for (int i = 0; i < n; ++i) {
tmp[i] = 0;
}
for (int i = 0; i < n; ++i) {
if (arr[i] <= 0 || arr[i] > n) continue;
else tmp[arr[i] - 1] = 1;
}
for (int i = 0; i < n; ++i) {
if (!tmp[i]) return i + 1;
}
return n + 1;
}
-
- week4小结
day16 - day21
- week4小结
- day16和day17是经典算法,day18-day21是算法真题。
– 交换排序:冒泡 和 快排。
— 冒泡O(n^2)(稳定)、快排(nlogn)(不稳定)
– 选择排序:简单选择(不稳定) (和堆排序)
---- 简单选择O(n^2)
– 插入排序:直接插入(稳定) (和折半插入、希尔)
---- 直接插入O(n^2)
基本有序时,适用冒泡(稳定)、直接插入(稳定)。
n较小时,适用简单选择(移动次数更少,记录本身信息量较大时可以,但(不稳定)、直接插入(稳定); n较大时,适用快排。
- 2010、2011、2013、2018
2010 一维数组循环左移 和day12相同,局部逆置+整体逆置
2011 求得中位数 与 day11合并两顺序表的双指针类似,都是对于两个数组整体进行遍历。
2013 求主元 投票算法 找到出现过半次数的元素,也可以空间换时间,进行哈希计数。
2018 数组中未出现最小正整数,常规算法题,利用等长数组,记录出现,如果全部出现则全为1,自然最小未出现是表长度n+1;否则数组中会出现0.
- week5 回到目录
day22 三元组的最小距离(2020)
思路尝试
- 贪心 + 三指针 时间O(n + m + p) 空间O(1)
三元组距离图示如下,时间长distance长度是2*ac,与中间的元素无关。
每次计算出最小的那个元素,然后只移动最小的元素,后移一位,重新计算,直到三元组的某一组到达表尾。
- 王道基本设计思想 2.2.3.14
1、使用minDist记录所有已处理的三元组的最小距离,初始为一个足够大整数0x7fffffff
2、集合S1、S2、S3分别保存在数组A、B、C中。数组的下标变量i=j=k=0,当i<|S1|、j<|S2|、k<|S3|时,循环执行下面:
---- 计算最小距离。
---- 更新最小距离。
---- 将三者中最小值的下标+1.
3、输出minDist,结束。
代码尝试回到week5
int findMinDistofTrip(int A[], int n, int B[], int m, int C[], int p){
int i = 0, j = 0, k = 0, minDist = INT_Max, tmp;
while (i < n && j < m && k < p && minDist > 0){
tmp = abs(A[i] - B[j]) + abs(A[i] - C[k]) + abs(B[j] - C[k]);
if (tmp < minDist) minDist = tmp;
if (firstIsMin(A[i], B[j], C[k])) i++;
else if (firstIsMin(B[i], A[j], C[k])) j++;
else k++;
}
return minDist;
}
day23 带头结点尾插法创建链表
链表结构定义与相关函数实现见文章开头。
代码尝试回到week5
void appendLinkListRear(LinkList &L, int data){
LNode *newNode = (LNode*) malloc(sizeof (LNode));
newNode->data = data;
newNode->next = nullptr;
LNode *p = L;
// 找到尾结点
while (p->next) p = p->next;
// 尾插
p->next = newNode;
}
day24 头插法
代码尝试回到week5
void appendLinkListFront(LinkList &L, int data){
LNode *newNode = (LNode*) malloc(sizeof (LNode));
newNode->data = data;
// 头插
newNode->next = L->next;
// 更新头结点next指针
L->next = newNode;
}
day25 反向输出链表各结点值
思路尝试
-
可以利用头插法,利用所给链表L生成新的链表,再直接print新的链表;
-
利用递归思想。
-
也可以直接反转链表,再输出。
代码尝试回到week5
```c
// 递归反向输出
void reversePrintLinkList(LinkList L){
if (!L) return;
reversePrintLinkList(L->next);
cout << L->data << endl;
}
// 反转链表 三指针
void reverseLinkList(LinkList &L){
LNode* pre = nullptr, *cur = L->next, *post = L->next;
while (cur){
//记录后一个结点
post = post->next;
//将当前结点反转
cur->next = pre;
//后移
pre = cur;
cur = post;
}
// 更新头结点所指一个结点
L->next = pre;
}
day26 删除不带头结点单链表值为x的结点
思路尝试
代码尝试回到week5
void deleteNodesWithValue(LinkList &L, int x){
if (!L) return;
LNode* curr = L, *prev = nullptr;
while (curr){
if (curr->data == x){
LNode* tmp = curr;
// 如果是第一个结点,特判
if (L == curr){
L = curr->next;
} else{
prev->next = curr->next;
}
curr = curr->next;
delete(tmp);
} else{
prev = curr, curr = curr->next;
}
}
}
day27 链表倒数第k个结点(2009)
思路尝试
利用双指针,通过遍历一遍链表,找到结点。
pre和post指针都初始化为L.
先让post指针后移k个结点,如果在在移动完k个结点前,post为null,说明长度<K,return 0;
再让pre指针移动到第一个结点位置。
之后对双指针同时后移,直到post到达最后一个结点。此时的pre所指结点为倒数第k个结点。
代码尝试回到week5
int findLastK(LinkList L, int k){
int i = k;
LNode *pre = L, *post = L;
// 先让post移动到第k个结点处
while (i && post){
post = post->next, i--;
}
// post为null, 链表长度小于k
if (!post) return 0;
pre = pre->next; // 此时,pre和post相差k-1个位置
// 当post为最后一个结点时,退出循环
while (post->next){
pre = pre->next, post = post->next;
}
cout << pre->data << endl;
return 1;
}
-
- week5小结
day22 - day27
- week5小结
-
本周是链表相关的算法和一道2009年链表真题,以及2020年的数组真题。
-
day23 - day26涉及链表的基本算法,头插法、尾插法,链表逆置,删除给定值的结点。
– day25 反向输出链表各结点值,实际上也可以利用递归,或者开辟数组存下来,或者利用头插法重新构建链表;
– 或者直接链表逆置算法,利用三指针算法。
-
day27 是2009年链表中删除倒数第k个结点,利用双指针,先拉开距离,再同时后移。
-
day22 是2020年三元组的最小距离,可以暴力解,但是时间复杂度O(n^3),利用贪心算法 三指针,可以达到线性。
- week6 回到目录
day28 两个链表AB奇偶存放
思路尝试
- 三指针
定义三个指针,遍历A的所有结点。pre初始化为头结点,指向前一个结点;cur初始化为第一个结点,作为当前结点;
post初始化为第一个结点,作为后一个结点。
pB作为B链表的尾结点指针。
如果cur不为空,则进行循环;cur为空时退出循环,返回B链表。
循环如下:
先让post后移.
1、如果cur结点值为奇数,则pre后移到当前结点cur,使当前结点留在A链表;
2、否则进行迁移,让cur结点迁移到pB结点后一个,即B链表尾部,作为B链表新的尾结点,并使新的尾结点的next为空,同时更新pB。
– 同时更新pre结点next为post,使得前一个结点的下一个结点为post,跳过cur当前偶数结点
3、最后再让cur后移,继续判定循环操作。
代码尝试回到week6
LinkList divideTwoLinkList(LinkList &A){
LinkList B = (LNode *) malloc(sizeof (LNode));
LNode *pre = A, *cur = A->next, *post = A->next;
LNode *pB = B; //pB指向B链表尾结点
while (cur){
post = post->next;
if (cur->data % 2){
// 此时 cur所指结点为奇数,跳过当前结点,留在链表A
pre = cur;
} else{
// 此时为偶数, 将当前结点移动至链表B尾
pre->next = post; // A链表pre结点跨过cur偶数结点
pB->next = cur, pB = pB->next; // 接入B链表尾,更新pB
pB->next = nullptr; // 保证B链表尾结点的next为nullptr
}
cur = post; // 更新cur为下一个结点
}
return B;
}
day29 删除一个最小值结点
思路尝试
- 双指针遍历,临时指针记录最小值结点,进行删除。
min记录最小值,pre指向前一个结点,cur指向当前结点,preTmp记录最小值结点的前一个结点,curTmp记录最小值结点。
– min初始化为第一个结点的值,preTmp初始化为头结点L,curTmp初始化为第一个结点;pre初始化为第一个结点,cur初始化为第一个结点的next。
1、开始遍历,对pre和cur同时后移,直到cur为空时跳出循环。循环如下:
2、如果cur结点的值 < min,则更新preTmp和curTmp,否则直接执行步骤3;
3、pre移动到cur,cur后移。
代码尝试回到week6
void delMinOfLinkList(LinkList &L){
if (!L->next) return;
int min = L->next->data;
LNode*preTmp = L, *curTmp = L->next;
LNode* pre = curTmp, *cur = curTmp->next;
while (cur){
if (cur->data < min){
min = cur->data, preTmp = pre, curTmp = cur;
}
pre = cur, cur = cur->next;
}
preTmp->next = curTmp->next;
free(curTmp);
}
day30 按递增输出单链表结点数据元素
思路尝试
- O(n)空间,开辟临时数组保存链表元素值,再对数组进行快排,最后输出。
代码尝试回到week6
void printSortLinkList(LinkList &L, int n){
if (!n || !L->next) return;
// 开临时数组
int* nums = (int*) malloc(sizeof (int) * n);
LNode* p = L->next;
int cnt = 0;
// 赋值临时数组nums
while (p){
nums[cnt++] = p->data;
p = p->next;
}
// 快排
quickSort(nums, 0, n - 1);
// 输出
for (int i = 0; i < n; ++i) {
cout << nums[i] << ' ';
}
cout << endl;
free(nums);
}
day31 求二叉树最大深度
代码尝试回到week6
int TreeDepth(BiTree T){
if (!T) return 0;
int leftDepth = TreeDepth(T->left);
int rightDepth = TreeDepth(T->right);
return leftDepth > rightDepth ? leftDepth + 1 : rightDepth + 1;
}
day32 - 33 二叉树三者遍历方式
思路尝试
代码尝试回到week6
// 先序遍历
void PreOrder(BiTree root){
if (root){
cout << root->data << ' ';
PreOrder(root->left);
PreOrder(root->right);
}
}
// 中序遍历
void InOrder(BiTree root){
if (root){
InOrder(root->left);
cout << root->data << ' ';
InOrder(root->right);
}
}
// 后序遍历
void PostOrder(BiTree root){
if (root){
PostOrder(root->left);
PostOrder(root->right);
cout << root->data << ' ';
}
}
-
- week6小结
day28 - day33
- week6小结
-
本周是关于链表和二叉树基本算法。
-
day28-day30是链表相关
– day28两个链表AB奇偶存放,利用三个指针,pre、cur、post往后遍历,根据值的奇偶性进行结点的移动;
– day29是删除最小值结点,暴力,用临时指针在遍历链表的过程中,不断更新最小值结点,最后删除;
– day30按递增输出单链表结点数据元素,开辟等长数组记录链表元素值,利用排序算法排序后输出。
- day31-day33是二叉树相关
– 先序、中序、后序遍历。
- week7 回到目录
day34 计算二叉树所有叶子结点的个数
代码尝试回到week7
int countLeafNode(BiTree T){
if (!T) return 0;
if (!T->left && !T->right) return 1;
return countLeafNode(T->left) + countLeafNode(T->right);
}
**day35 判断二叉树是否为二叉排序树 **
思路尝试
- 二叉排序树中序遍历是增序。
– pre初始化为最小值
– 对二叉树进行中序遍历,同时传入上一次遍历的结点值pre,如果>=当前结点则return false;否则继续遍历,最后return true.
代码尝试回到week7
bool judgeBST(BiTree T, char& pre){
if (!T) return true;
if (!judgeBST(T->left, pre)){
return false;
}
if (pre >= T->data) return false;
pre = T->data;
return judgeBST(T->right, pre);
}
day36 输出先序遍历的第k个结点
思路尝试
- 利用先序遍历,cnt为引用变量,每次访问当前结点前cnt++,作计数用。
– 先序遍历: 访问根结点 - 递归遍历左子树 - 递归遍历右子树
– 每次访问根节点进行特判cnt==k,输出后return,否则继续遍历。
代码尝试回到week7
void PreOrderTheK(BiTree T, int &cnt, int k){
if (!T) return;
cnt++;
if (cnt == k) {
cout << "Pre order Tree list, the K TNode is : " << T->data << endl;
return;
}
PreOrderTheK(T->left, cnt, k);
PreOrderTheK(T->right, cnt, k);
}
day37 找出二叉树中最大值的结点
思路尝试
代码尝试回到week7
void findMaxInBiTree(BiTree T, BiNode* &maxNode, int &maxData){
if (!T) return ;
if (T->data > maxData) {
maxNode = T, maxData = T->data;
}
findMaxInBiTree(T->left, maxNode, maxData);
findMaxInBiTree(T->right, maxNode, maxData);
}
day38 二叉树WPL(2014)
思路尝试
- 利用递归,根节点所在层作为第0层,deep作为层数进行函数传参.
– 每次特判是否为根节点,return当前结点的带权路径长度;
– 否则,继续递归遍历左右子树。
代码尝试回到week7
int calculateWPL(BiTree T, int deep){
if (!T) return 0;
// 根节点
if (!T->left && !T->right) return T->data * deep;
// 分子结点,继续递归
return calculateWPL(T->left, deep + 1) + calculateWPL(T->right, deep + 1);
}
day39 二叉树转换为中缀表达式(2017)
思路尝试
代码尝试回到week7
// 定义⼆叉树结构体
typedef struct BTNode {
string val;
BTNode* left;
BTNode* right;
}BTNode,*BTree;
// 递归将表达式转化为中缀表达式
string expressionToInfix(BTree root) {
if (!root) return "";
// 如果是叶⼦节点,直接返回值
if (!root->left && !root->right) return root->val;
// 递归处理左右⼦树
string leftExpr = expressionToInfix(root->left);
string rightExpr = expressionToInfix(root->right);
// 返回中缀表达式
return "(" + leftExpr + root->val + rightExpr + ")";
}
- 剩余年份真题暴力题解补充 回到目录
2012 – 2015 – 2016 – 2019 – 2021 – 2022 – 2023 – 2024
2012
回到补充
typedef struct Lnode
{
int data;
struct Lnode *next;
} Lnode, *Linklist;
// 链表结点的结构定义
Linklist searchCommon(Linklist L1, Linklist L2)
{
Linklist p = L1->next;
Linklist q = L2->next;
while (p != nullptr)
{
while (q != nullptr)
{
if (p == q)
{
return q;
}
q = q->next;
}
p = p->next;
q = q->next;
}
return nullptr;
}
评判标准:
2015
回到补充
2016
回到补充
2019
回到补充
2021
回到补充
2022
回到补充
2023
回到补充
2024
回到补充