408数据结构常考算法基础训练


该训练营为蓝蓝考研蓝颜知己)的算法训练营内容,题目来源有经典算法题、408统考算法题等等。分为7weeks共计39days练习,每日一道算法题训练,涵盖基本顺序表、链表和二叉树相关的基础算法,以及部分408真题中数据结构部分的算法题,并未包含图相关的算法,还需要进一步对图算法自行学习。 当然,对于顺序表、链表和二叉树的算法,也需要继续通过课后习题进行练习和巩固。
github项目地址–AlgorithmTrainingCamp

该训练合集仅包含2009、2010、2011、2013、2014、2017、2018、2020年真题。
剩余年份真题暴力题解补充见末尾。

如果时间充裕,可以前期进行PC端练习,后期再尝试完全在纸上进行手写练习;如果时间紧张则不建议PC端练习,直接手写。
个人最初是使用的Clion进行coding练习的,所以这些题目是可以成功测试运行的。所涉及的相关结构体及函数定义如下所示:

  • 线性表List
    • 顺序表SqList
      • 头文件SqList.h
// 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.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.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"


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; // 返回删除值
}


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]);
}


day8 删除给定元素值范围内所有元素

在这里插入图片描述

思路尝试

类似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

主要是双指针的运用,day04顺序表逆置,左右交换;

day06删除下标范围内的元素,相当于两顺序表的前后合并;

day05删除所有值为x的元素、day08删除值范围内的元素、day09有序表去重是相同的,双指针,一个负责记录位置,一个负责移动判断。只不过前两个的判定对比是定下来的元素,去重则是每次判定对比的元素可能会发生变化,也就是最近的一个元素;

day07依表头划分左右两半partition实际上是快排算法的基础,通过双指针和哨兵进行比较再交换。



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则是经典算法题
  • day10、day13、day14查找相关,

day10关键是寻找给定元素值区间内的下标范围,遍历即可;

day13是在二维行列有序 但不是整体有序 的数组中寻target,虽然不是整体依次遍历,改变思路,对行和列进行遍历 换行、换列

day14正常二分背模板;

  • day11、day12是同时两个顺序表操作

day11是两有序顺序表合并新的有序表,经典题目

day12是前后换位,局部逆置+整体逆置 = 内部不变、局部换位

  • day15 是第k小,以前刷过第k大,原理相同。两种思路,排序以后输出,或者排序过程中找出来。
    对于后者,可利用冒泡或者堆排序,也可以利用快排性质,每次分割以后,只去对目标位置所在那一半进行操作,不断切割。


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
  • 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.



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
  • 本周是链表相关的算法和一道2009年链表真题,以及2020年的数组真题。

  • day23 - day26涉及链表的基本算法,头插法、尾插法,链表逆置,删除给定值的结点。

– day25 反向输出链表各结点值,实际上也可以利用递归,或者开辟数组存下来,或者利用头插法重新构建链表;

– 或者直接链表逆置算法,利用三指针算法。

  • day27 是2009年链表中删除倒数第k个结点,利用双指针,先拉开距离,再同时后移。

  • day22 是2020年三元组的最小距离,可以暴力解,但是时间复杂度O(n^3),利用贪心算法 三指针,可以达到线性。



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
  • 本周是关于链表和二叉树基本算法。

  • day28-day30是链表相关

– day28两个链表AB奇偶存放,利用三个指针,pre、cur、post往后遍历,根据值的奇偶性进行结点的移动;

– day29是删除最小值结点,暴力,用临时指针在遍历链表的过程中,不断更新最小值结点,最后删除;

– day30按递增输出单链表结点数据元素,开辟等长数组记录链表元素值,利用排序算法排序后输出。

  • day31-day33是二叉树相关

– 先序、中序、后序遍历。



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 + ")";
}

20122015201620192021202220232024


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
回到补充

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值