• PAT甲级考试知识点总结和一些看法


    0 引言

    本人今年PAT甲级考了95分,平时力扣也有再刷(大概有360题),感觉PAT主要还是面向考研党的,里面的题目其实难度是小于力扣的,但这种难度的题目浙大去年考研机试居然有20%的0分我其实不是很理解。

    PAT主要是考察编码能力(一道题写个一两百行太正常不过了)和数据结构基础,对于算法只会考察一些比较经典的算法比如排序算法啥的,而力扣的题目算法考察相对会多一点,而且PAT的题目不会告诉你测试样例,输入输出也很恶心,总会在意想不到的地方恶心你一下,所以我个人认为对于找工作党如果力扣的投入产出比是1:1,那么PAT就是1:0.5。

    PAT的签约企业主要是一些外企和顶级大厂比如谷歌、微软、阿里、腾讯啥的,而且基本要求甲级满分,其实感觉PAT满分不容易的因为他总有几个点会恶心你,关键的是他还不告诉你测试样例(被恶心坏了),除了这些大厂其他签约企业基本都是在杭州,所以如果是为了找工作并且不打算来杭州发展感觉PAT确实没啥用(当然如果你是为了提升编程能力和数据结构基础,还是很推荐的~)

    下面推荐一些书籍、博客、视频教程和我总结归纳的一些考点

    书籍:

    《大话数据结构》:还是挺适合入门的,但其实写得还是比较浅,应付PAT甲级还是不太够
    《数据结构与算法分析》:都打算学数据结构与算法了,大名鼎鼎的黑皮书,你说要不要看麻~

    博客:

    我推荐柳神!https://www.liuchuo.net/

    视频教程:

    我推荐y总的PAT甲级辅导课不过有点小贵,量力而行吧~

    https://www.acwing.com/activity/content/27/

    在这里插入图片描述

    1 STL

    1.1 vector

    动态数组

    • size:大小
    • push_back:向数组末尾插入一个元素
    • pop_back:向数组末尾弹出一个元素
    • resize:重新定义数组大小

    1.2 unordered_map

    哈希表

    • size:大小
    • [ ]:赋值与取值

    1.3 unordered_set

    哈希集合

    • size:大小
    • insert:插入一个元素
    • erase:删除一个元素
    • count:某个元素在集合中的数量

    1.4 stack

    • push:向栈顶插入一个元素
    • pop:弹出栈顶元素
    • size:大小
    • top:获取栈顶元素

    1.5 queue

    队列

    • push:向队尾插入元素
    • pop:队首弹出一个元素
    • front:获取队首元素
    • size:大小

    1.6 deque

    双端队列

    • size:大小
    • push_back:队尾插入元素
    • pop_back:队尾弹出元素
    • push_front:队首插入元素
    • pop_front:队首弹出元素
    • front:获取队首元素
    • back:获取队尾元素

    1.7 priority_queue

    优先队列(堆)

    • push:插入一个元素
    • pop:删除堆顶元素
    • top:获取堆顶元素
    • size:大小

    大顶堆:prority_queue,less>

    小顶堆:prority_queue,greater>

    2 基础数据结构及其算法

    2.1 栈

    stack

    2.1.1 单调栈

    给定一个长度为 NN 的整数数列,输出每个数左边第一个比它小的数,如果不存在则输出 −1。

        stack s;
        while(n--) {
            int x;
            cin >> x;
            while(!s.empty() && s.top() >= x) s.pop();
            if(s.empty()) cout << -1 << ' ';
            else cout << s.top() << ' ';
            s.push(x);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    2.2 队列

    queue

    2.2.1 单调队列

    最典型的就是滑动窗口问题了

    /**
    * 使用单调队列,保证这个队列是一个单调递减的队列另一个单调递增
    */
    void maxSlidingWindow(vector& nums, int k) {
        deque q;
        deque q2;
        int pre = nums[0];
        for(int i = 0; i < nums.size(); i++) {
            if(i >= k && pre == q.front()) q.pop_front();
            if(i >= k && pre == q2.front()) q2.pop_front();
            while(!q.empty() && q.back() < nums[i]) q.pop_back();
            while(!q2.empty() && q2.back() > nums[i]) q2.pop_back();
            q.push_back(nums[i]);
            q2.push_back(nums[i]);
            if(i >= k-1) {
                res1.push_back(q.front());
                res2.push_back(q2.front());
                pre = nums[i-k+1]; 
            }
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    2.3 链表

    数组模拟链表

    链表如果使用指针的方式比较慢会超时,所以一般会用数组模拟链表

    vector LinkedList;
    //尾插法
    void add(int x) {
    	LinkedList.push_back(x);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    2.4 堆

    prority_queue

    2.4.1 down

    const int N = 110;
    int heap[N];//小顶堆
    int n;//堆的大小
    void down(int u) {
    	int t = u;
    	if (2 * u <= n && heap[2 * u] < heap[t]) t = 2 * u;
    	if (2 * u + 1 <= n && heap[2 * u + 1] < heap[t]) t = 2 * u + 1;
    	if (t != u) {
    		swap(heap[u], heap[t]);
    		down(t);
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    2.4.2 up

    const int N = 110;
    int heap[N];//小顶堆
    int n;//堆的大小
    
    void up(int u) {
    	while (u / 2 && heap[u / 2] > heap[u]) {
    		swap(heap[u / 2], heap[u]);
    		u = u / 2;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    2.5 树

    树也一般不能使用链表,也是使用数组来模拟的

    2.5.1 二叉树

    const int N = 110;
    struct Node
    {
    	int data, left = -1, right = -1;
    };
    
    vector BinaryTree(N);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    一般直接以data为key放到数组的相应位置则表示插入节点,-1表示指向NULL

    有时N比较大所以可能会爆内存故采用unordered_map来节省空间

    const int N = 110;
    struct Node
    {
    	int data, left = -1, right = -1;
    };
    
    unordered_map BinaryTree;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    2.5.2 多叉树

    多叉树一般是用一个二维数组来维护,第一个坐标就是这个节点的data,第二维表示这个节点能直接到达的点

    vector Tree[N];
    //给a节点插入一个b元素
    void add(int a, int b) {
    	Tree[a].push_back(b);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    2.5.3 遍历

    树有四种遍历方式,下面以二叉树为例

    2.5.3.1 preorder
    vector BinaryTree[N];
    
    void preorder(int root) {
    	if (root == -1) return;
    	printf("%d", BinaryTree[root].data);
    	preorder(BinaryTree[root].left);
    	preorder(BinaryTree[root].right);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    2.5.3.2 inorder
    vector BinaryTree[N];
    
    void inorder(int root) {
    	 if (root == -1) return;
    	 inorder(BinaryTree[root].left);
    	 printf("%d", BinaryTree[root].data);
    	 inorder(BinaryTree[root].right);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    2.5.3.3 postorder
    vector BinaryTree[N];
    
    void postorder(int root) {
    	if (root == -1) return;
    	postorder(BinaryTree[root].left);
    	postorder(BinaryTree[root].right);
    	printf("%d", BinaryTree[root].data);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    2.5.3.4 levelorder
    vector BinaryTree[N];
    
    void levelorder(int root) {
    	queue q;
    	q.push(root);
    	while (q.size()) {
    		int size = q.size();
    		for (int i = 0; i < size; i++) {
    			int t = q.front();
    			q.pop();
    			printf("%d", t);
    			if (BinaryTree[t].left != -1) q.push(BinaryTree[t].left);
    			if (BinaryTree[t].right != -1) q.push(BinaryTree[t].right);
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    2.5.4 构造

    2.5.4.1 通过先序遍历和中序遍历构造二叉树
    int build(vector preorder, vector inorder) {
    	if (preorder.size() <= 0 || inorder.size() <= 0) return -1;
    	int root = preorder[0];
    	int k = 0;
    	while (k < inorder.size() && inorder[k] != root) k++;
    	BinaryTree[root].data = root;
    	BinaryTree[root].left = build(
    		vector(preorder.begin() + 1, preorder.begin() + k + 1),
    		vector(inorder.begin(), inorder.begin() + k)
    	);
    	BinaryTree[root].right = build(
    		vector(preorder.begin() + k + 1,preorder.end()),
    		vector(inorder.begin() + k + 1,inorder.end())
    	);
    	return root;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    当然这种写法比较容易爆内存所以也可以使用指针的形式来写

    int build(int pl, int pr, int il, int ir) {
    	if (pl > pr || il > ir) return -1;
    	int root = preorder[pl];
    	int k = il;
    	while (k <= ir && inorder[k] != root) k++;
    	BinaryTree[root].data = root;
    	BinaryTree[root].left = build(pl + 1, k - il + pl, il, k - 1);
    	BinaryTree[root].right = build(pr - ir + k + 1, pr, k + 1, ir);
    	return root;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    2.5.4.2 通过后序遍历和中序遍历构造二叉树
    int build(vector postorder, vector inorder) {
    	if (postorder.size() <= 0 || inorder.size() <= 0) return -1;
    	int root = postorder[postorder.size() - 1];
    	int k = 0;
    	while (k < inorder.size() && inorder[k] != root) k++;
    	BinaryTree[root].data = root;
    	BinaryTree[root].left = build(
    		vector(postorder.begin(),postorder.begin() + k),
    		vector(inorder.begin(), inorder.begin() + k)
    	);
    	BinaryTree[root].right = build(
    		vector(postorder.begin() + k, postorder.end() - 1),
    		vector(inorder.begin() + k + 1, inorder.end())
    	);
    	return root;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    当然这种写法比较容易爆内存所以也可以使用指针的形式来写

    int build(int pl, int pr, int il, int ir) {
    	if (pl > pr || il > ir) return -1;
    	int root = postorder[pr];
    	int k = il;
    	int k = il;
    	while (k <= ir && inorder[k] != root) k++;
    	BinaryTree[root].left = build(pl, k - il + pl - 1, ir, k - 1);
    	BinaryTree[root].right = build(k - il + pl, pr - 1, k + 1, ir);
    	return root;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    2.5.4.3 通过前序遍历和后序遍历来判断是否能构造一棵二叉树
    bool canbuild(vector pre, vector post) {
    	if (pre.size() != post.size()) return false;
    	if (pre.size() == 0 && post.size() == 0) return true;
    	if (pre[0] != post[post.size() - 1]) return false;
    	
    	int root = pre[0];
    	tree[root].data = root;
    	if (pre.size() > 1) {
    		int left = pre[1];
    		int right = post[post.size() - 2];
    		if (left != right) {
    			int k1 = -1;
    			for (int i = 0; i < pre.size(); i++) {
    				if (pre[i] == right) {
    					k1 = i;
    					break;
    				}
    			}
    			if (k1 == -1) return false;
    			int k2 = -1;
    			for (int i = 0; i < post.size(); i++) {
    				if (post[i] == left) {
    					k2 = i;
    					break;
    				}
    			}
    			if (k2 == -1) return false;
    			return canbuild(
    					vector (pre.begin() + 1,pre.begin() + k1),
    					vector (post.begin(),post.begin() + k2 + 1)
    				) && 
    				canbuild(
    					vector (pre.begin() + k1,pre.end()),
    					vector (post.begin() + k2  +1,post.end() - 1)
    				);
    		}
    		else {
    			return canbuild(
    				vector(pre.begin() + 1, pre.end()),
    				vector(post.begin(), post.end() - 1)
    			);
    		}
    	}
    	return true;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45

    2.5.5 AVL的插入操作

    AVL是自平衡二叉搜索树,相较于ADT多了一个旋转自平衡的操作

    明白这一点后其实就比较简单了所有函数和ADT完全相同只是添加一个自平衡函数balance()

    原先insert返回t现在返回balance(t)删除同理

    自平衡

    那么AVL是怎么实现自平衡的呢其实很简单一共有四种情况

    LL

    LL 往左子树的左子树插入导致的不平衡 右旋:
    第一步:将根节点的左孩子替换此节点 AvlNode k = t.left;
    第二步:将 k节点的右孩子替换根节点的左孩子 t.left = k.right;
    第三步:将根节点替换为 k节点的右孩子 k.right = t;
    
    • 1
    • 2
    • 3
    • 4

    RR

    RR 往右子树的右子树插入导致的不平衡 左旋:
    第一步:将根节点的右孩子替换此节点 AvlNode k = t.right;
    第二步:将 k节点的左孩子替换根节点的右孩子 t.right = k.left;
    第三步:将根节点替换为 k节点的左孩子 k.left = t;
    
    • 1
    • 2
    • 3
    • 4

    LR

    LR 往左子树的右子树插入导致的不平衡 先对左子树左旋,再对根右旋
    
    • 1

    RL

    RL 往右子树的左子树插入导致的不平衡 先对右子树右旋,再对根左旋
    
    • 1
    const int N = 30;
    typedef struct node {
        int data;
        struct node* left;
        struct node* right;
    }Node;
    
    typedef Node* Tree;
    
    int getHeight(Tree root) {
        if (root == NULL) return 0;
        return max(getHeight(root->left), getHeight(root->right)) + 1;
    }
    
    //右旋
    Tree rotateWithRight(Tree root) {
        Tree t = root->left;
        root->left = t->right;
        t->right = root;
        return t;
    }
    //左旋
    Tree rotateWithLeft(Tree root) {
        Tree t = root->right;
        root->right = t->left;
        t->left = root;
        return t;
    }
    //先堆左子树左旋,在对根右旋
    Tree DoubleWithRight(Tree root) {
        root->left = rotateWithLeft(root->left);
        return rotateWithRight(root);
    }
    
    //先堆右子树右旋,在对根左旋
    Tree DoubleWithLeft(Tree root) {
        root->right = rotateWithRight(root->right);
        return rotateWithLeft(root);
    }
    
    /**
    * 分为四种情况
    * LL 往左子树的左子树插入导致的不平衡 右旋
    * RR 往右子树的右子树插入导致的不平衡 左旋
    * LR 往左子树的右子树插入导致的不平衡 先对左子树左旋,再对根右旋
    * RL 往右子树的左子树插入导致的不平衡 先对右子树右旋,再对根左旋
    */
    Tree balance(Tree root) {
        if (root == NULL) return root;
        //左子树失衡
        if (getHeight(root->left) - getHeight(root->right) > 1) {
            //LL
            if (getHeight(root->left->left) > getHeight(root->left->right)) {
                root = rotateWithRight(root);
            }
            //LR
            else if (getHeight(root->left->left) < getHeight(root->left->right)) {
                root = DoubleWithRight(root);
            }
        }
        //右子树失衡
        else if (getHeight(root->left) - getHeight(root->right) < -1) {
            //RL
            if (getHeight(root->right->left) > getHeight(root->right->right)) {
                root = DoubleWithLeft(root);
            }
            //RR
            else if (getHeight(root->right->left) < getHeight(root->right->right)) {
                root = rotateWithLeft(root);
            }
        }
        return root;
    }
    
    Tree insert(Tree tree, int x) {
        if (tree == NULL) {
            Tree node = new Node();
            node->data = x;
            node->left = NULL;
            node->right = NULL;
            return node;
        }
        if (x < tree->data) {
            tree->left = insert(tree->left, x);
        }
        else {
            tree->right = insert(tree->right, x);
        }
        return balance(tree);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90

    2.5.6 判断是否为红黑树

    它具有以下 5 个属性:

    • 节点是红色或黑色。
    • 根节点是黑色。
    • 所有叶子都是黑色。(叶子是 NULL节点)
    • 每个红色节点的两个子节点都是黑色。
    • 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。

    分析:判断以下⼏几点:
    1.根结点是否为⿊黑⾊色
    2.如果⼀一个结点是红⾊色,它的孩⼦子节点是否都为⿊黑⾊色
    3.从任意结点到叶⼦子结点的路路径中,⿊黑⾊色结点的个数是否相同

    typedef struct node {
        int data;
        struct node* left;
        struct node* right;
    }Node;
    
    typedef node* Tree;
    
    bool judge1(Tree root) {
        if (root == NULL) return true;
        if (root->data < 0) {
            if (root->left != NULL && root->left->data < 0) return false;
            if (root->right != NULL && root->right->data < 0) return false;
        }
        return judge1(root->left) && judge1(root->right);
    }
    
    int getBlackHeight(Tree root) {
        if (root == NULL) return 0;
        int count = root->data > 0 ? 1 : 0;
        return max(getBlackHeight(root->left), getBlackHeight(root->right)) + count;
    }
    
    bool judge2(Tree root) {
        if (root == NULL) return true;
        int left = getBlackHeight(root->left);
        int right = getBlackHeight(root->right);
        if (left != right)return false;
        return judge2(root->left) && judge2(root->right);
    }
    
    bool isRBT(Tree tree) {
        return tree->data < 0 || !judge1(tree) || !judge2(tree);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    2.6 图

    2.6.1 图的表示

    图同样也是不能使用链表来表示否则会超时

    1. 稀疏图:邻接表 int g[N][N];
    2. 稠密图:邻接矩阵 vector g[N];

    2.6.2 dijkstra

    既然图有两种表示方式自然dijkstra算法也有两种写法

    2.6.2.1 经典dijkstra
    int g[N][N];//邻接矩阵
    int dist[N];//最短路
    bool st[N];//标记是否被访问过
    const int INF = INT_MAX;//最大值
    int n;//点数
    void dijkstra(int start) {
        //1 初始化
        for(int i = 1; i <= n; i++) {
            dist[i] = INF;
        }
        dist[start] = 0;
        
        //2 迭代每一个未被标记且路径最短的点
        for(int i = 1; i <= n; i++) {
            //初始化这个点
            int t = -1;
            //遍历所有点找到未被标记且路径最短的点
            for(int j = 1; j <= n; j++) {
                if(!st[j] && (t == -1 || dist[j] < dist[t])) {
                    t = j;
                }
            }
            //将这个点标记
            st[t] = true;
            //用这个点更新其他点
            //具体就是 min(源->j,源->t + t->j)
            for(int j = 1; j <= n; j++) {
                dist[j] = min(dist[j],dist[t] + g[t][j]);
            }
        }
        
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    2.6.2.1 堆优化dijkstra
    vector g[N];
    int dist[N];//最短路
    bool st[N];//标记是否被访问过
    const int INF = INT_MAX;//最大值
    int n;//点数
    void dijkstra(int start) {
    	//定义一个小顶堆
    	priority_queue, greater> heap;
    	//1. 初始化,并将第一个点的边放入堆
    	for (int i = 1; i <= n; i++) {
    		dist[i] = INF;
    	}
    	dist[start] = 0;
    	heap.push({ start,0 });
    	//遍历所有边
    	while (heap.size()) {
    		//拿出最小边
    		auto min_edge = heap.top();
    		heap.pop();
    		if (st[min_edge.end]) continue;
    		st[min_edge.end] = true;
    		//更新这条最小边对应点的所有边
    		for (int i = 0; i < g[min_edge.end].size(); i++) {
    			auto edge = g[min_edge.end][i];
    			//遍历这个点的所有边,更新路径
    			//min(源->edge.end, 源->t.end + t.end->edge.wight)
    			//将新的最小路径放入堆
    			if (dist[min_edge.end] + edge.weight < dist[edge.end]) {
    				dist[edge.end] = dist[min_edge.end] + edge.weight;
    				heap.push({ edge.end,dist[edge.end] });
    			}
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    2.6.3 遍历(Flood Fill)

    Flood Fill:其实就是暴搜

    2.6.3.1 dfs
    int g[N][N];
    
    int d[][2]{
    	{1,0},
    	{-1,0},
    	{0,1},
    	{0,-1}
    };
    
    int dfs(int x, int y) {
    	if (g[x][y] == 0) return 0;
    	g[x][y] = 0;
    	int cnt = 1;
    	for (int i = 0; i < 6; i++) {
    		int a = x + d[i][0], b = y + d[i][1];
    		if (a >= 0 && a < n && b >= 0 && b < n) {
    			cnt += dfs(a, b);
    		}
    	}
    	return cnt;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    2.6.3.2 bfs
    int g[N][N];
    
    struct node {
    	int x, y;
    };
    
    int d[][2]{
    	{1,0},
    	{-1,0},
    	{0,1},
    	{0,-1}
    };
    
    int bfs(int y, int x) {
    	g[y][x] = 0;
    	queue q;
    	q.push({ x,y });
    	int cnt = 1;
    	while (q.size()) {
    		int size = q.size();
    		for (int i = 0; i < size; i++) {
    			node t = q.front();
    			q.pop();
    			for (int j = 0; j < 6; j++) {
    				int a = t.x + d[j][0], b = t.y + d[j][1];
    				if (a >= 0 && a < n && b >= 0 && b < n) {
    					g[b][a] = 0;
    					cnt++;
    					q.push({ a,b });
    				}
    			}
    		}
    	}
    	return cnt;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    2.7 并查集

    并查集主要有两个操作一个是合并两个集合另一个查看两个元素是否在一个集合,这两个操作都是基于find操作的

    2.7.1 find

    int p[N];
    
    int find(int x) {
    	if (x == p[x]) p[x] = find(p[x]);
    	return p[x];
    }
    
    void merge(int a, int b) {
    	int fa = find(a), fb = find(b);
    	if (fa != fb) {
    		p[fb] = find(fa);
    	}
    }
    
    bool check(int a, int b) {
    	return find(a) == find(b);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    2.8 哈希

    二次探测法

    2.9对顶堆

    典型例子是数据流中的中位数

        //大顶堆存左半边
        priority_queue,less> left;
        //小顶堆存右半边
        priority_queue,greater> right;
        /** initialize your data structure here. */
        MedianFinder() {
        }
        
        //如果left大小等于right那么插入一个元素到left
        //否则插入到right
        //插入规则是先插入到另一个堆,然后拿这个堆顶插入到这个堆
        void addNum(int num) {
            if(left.size() == right.size()) {
                right.push(num);
                left.push(right.top());
                right.pop();
            }
            else if(left.size() == right.size() + 1) {
                left.push(num);
                right.push(left.top());
                left.pop();
            }
        }
        
        //如果是奇数那么直接那左半边第一个数
        //如果是偶数则左右各取一个算平均值
        double findMedian() {
            if(left.size() > right.size()) return left.top();
            return 1.0 * (left.top() + right.top()) / 2;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    3 排序算法

    3.1 插入排序

    3.1.1 插入排序

    //直接插入排序
    void insert_sort(int arr[], int size) {
    	for (int i = 1; i < size; i++) {
    		int j = 0;
    		int t = arr[i];
    		for (j = i; j > 0; j--) {
    			if (t < arr[j - 1]) {
    				arr[j] = arr[j - 1];
    			}
    			else {
    				break;
    			}
    		}
    		arr[j] = t;
    	}
    }
    
    //折半插入排序
    void BInsertSort(int arr[], int size) {
    	for (int i = 1; i < size; i++) {
    		int t = arr[i];
    		int left = 0;
    		int right = i - 1;
    		while (left <= right) {
    			int mid = (left + right) / 2;
    			if (t < arr[mid]) {
    				right = mid - 1;
    			}
    			else{
    				left = mid + 1;
    			}
    		}
    		for (int j = i; j > left; j--) {
    			arr[j] = arr[j - 1];
    		}
    		arr[left] = t;
    	}
    }
    
    //希尔排序
    void shellSort(int arr[], int size) {
    	int gap = size / 2;//增量变量
    	while (gap > 0) {
    		for (int i = 0; i < gap; i++) {
    			for (int j = i + gap; j < size; j += gap) {
    				int t = arr[j];
    				int k = 0;
    				for (k = j; k >= 0; k--) {
    					if (t < arr[k-1]) {
    						arr[k] = arr[k - 1];
    					} 
    					else {
    						break;
    					}
    				}
    				arr[k] = t;
    			}
    		}
    		gap /= 2;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61

    3.1.2 判断一个数列是否是插入排序的结果

    例如给定初始序列,以及经过某种排序方法多次迭代后的序列,判断是否为插入排序

    3 1 2 8 7 5 9 4 6 0
    1 2 3 7 8 5 9 4 6 0

    int p = 0;
    while (p < n - 1 && v2[p] <= v2[p + 1]) {
    	p++;
    }
    p++;
    bool isInsert = true;
    //如果前半段有序,后半段两个数组都相同,那么一定是插入排序
    for (int i = p; i < n; i++) {
    	if (v1[i] != v2[i]) {
    		isInsert = false;
    		break;
    	}
    }
    //插入排序
    if (isInsert) {
    	cout << "Insertion Sort" << endl;
    	for (int i = p; i >= 0; i--) {
    		if (v2[i] >= v2[i - 1]) break;
    		swap(v2[i], v2[i - 1]);
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    3.2 归并排序

    void merge(int* arr, int left, int mid, int right) {
        //开辟左数组
        vector vleft(mid-left+1);
        //开辟右数组
        vector vright(right-mid);
        //将原先数组的左半部分copy到左数组
        for(int i = left; i <= mid; i++) vleft[i-left] = arr[i];
        //将原先数组的右半部分copy到右数组
        for(int i = mid + 1; i <= right; i++) vright[i-mid-1] = arr[i];
    
        //排序
        int k = left,i = 0, j = 0;
        //把小的放进去
        while(i < mid-left+1 && j < right-mid) {
            if(vleft[i] < vright[j]) arr[k++] = vleft[i++];
            else arr[k++] = vright[j++];
        }
    
        //剩余的放进去
        while(i < mid-left+1) arr[k++] = vleft[i++];
        while(j < right-mid) arr[k++] = vright[j++];
    }
    
    void mergeSort(int* arr, int left, int right) {
        if (left < right) {
            int mid = (left + right) / 2;
            //左递归
            mergeSort(arr, left, mid);
            //右递归
            mergeSort(arr, mid + 1, right);
            //合并
            merge(arr, left, mid, right);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    3.3 堆排序

    使用down递归写法

    int heap[100010];
    int Size = 0;
    int n, m;
    void down(int u) {
        int t = u;
        if (2 * u <= Size && heap[2 * u] < heap[t]) t = 2 * u;
        if (2 * u + 1 <= Size && heap[2 * u + 1] < heap[t]) t = 2 * u + 1;
        if (t != u) {
            swap(heap[u], heap[t]);
            down(t);
        }
    }
    
    void heap_sort() {
        for (int i = n / 2; i; i--) down(i);
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    使用迭代写法

       
    //堆排序
    void down() {
    	int p = n - 1;
    	while (p > 0 && heap[p] > heap[0]) {
    		p--;
    	}
    	swap(heap[0], heap[p]);
    	int child = 1;
    	//left_child = i * 2 + 1
    	//right_child = i * 2 + 2
    	//每次比较左右孩子取较大的那个下滤也就是著名的down操作
    	for (int i = 0; i * 2 + 1 <= p; i = child) {
    		int left_child = i * 2 + 1, right_child = i * 2 + 2;
    		//默认左孩子大
    		if (left_child < p) child = left_child;
    		//如果存在右孩子且比左孩子大那么更新为左孩子
    		if (right_child < p && heap[right_child] > heap[left_child]) child = right_child;
    		//下滤
    		if (heap[child] > heap[i]) swap(heap[i], heap[child]);
    		//左右孩子都比其小时说明排序成功break
    		else break;
    
    	}
    }
    
    void heap_sort() {
    	for (int i = 0; i < n; i++) down();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    3.4 快速排序

    void quick_sort(int* q, int l, int r)
    {
        if(l >= r) return;
        int i = l - 1;
        int j = r + 1;
        int x = q[l + (r-l)/2];
        while(i < j) {
            while(q[++i] < x);
            while(q[--j] > x);
            if(i < j) swap(q[i],q[j]);
        }
        quick_sort(q,l,j);
        quick_sort(q,j+1,r);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    4 其他常见算法

    4.1 二分

    //求左边界板子
    //【left,right】
    //mid = (left+right)/2
    //思考nums[mid]==target时,要求左边界所以将右边界左移right = mid-1;
    //左边界返回left
    int L(vector& nums,int target) {
        int left = 0;
        int right = nums.size() - 1;
        while(left <= right) {
            int mid = left + (right - left) / 2;
            if(nums[mid] < target) {
                left = mid + 1;
            }
            else right = mid-1;
        }
        if(left >= nums.size()) return -1;
        return nums[left] == target ? left : -1;
    }
    
    //求右边界板子
    //【left,right】
    //mid = (left+right)/2
    //思考nums[mid]==target时,要求右边界所以将左边界右移left = mid + 1;
    //右边界返回right
    int R(vector& nums,int target) {
        int left = 0;
        int right = nums.size() - 1;
        while(left <= right) {
            int mid = left + (right - left) / 2;
            if(nums[mid] <= target) {
                left = mid + 1;
            }
            else right = mid-1;
        }
        if(right < 0) return -1;
        return nums[right] == target ? right : -1;        
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    4.2 前缀和

    for(int i = 1; i <= n; i++) {
        nums[i] += nums[i-1];
    }
    
    • 1
    • 2
    • 3

    4.3 差分

    差分其实就是前缀和的逆过程

    void insert(vector& b,int l, int r, int c) {
        b[l] += c;
        b[r+1] -= c;
    }
    
    • 1
    • 2
    • 3
    • 4

    4.4 双指针

    这个概念比较大,两个指针的我都叫他双指针

    4.5 贪心

    这个完全玄学,太难了

    4.6 动规

    这个PAT考的比较少,主要考察

    1. 背包问题
    2. 计数类DP(整数划分)
    3. 记忆化搜索

    5 高精度

    因为c++的数字类型长度有限制比如int只能到2^32-1故一些数据量较大的数无法表示所以有了高精度的算法

    其实可以偷懒使用python

    5.1 高精度加法

    string add(string a, string b) {
        vector A, B;
        cin >> a >> b;
        for (int i = a.size() - 1; i >= 0; i--) {
            A.push_back(a[i] - '0');
        }
        for (int i = b.size() - 1; i >= 0; i--) {
            B.push_back(b[i] - '0');
        }
        vector C;
        int t = 0;
        for (int i = 0; i < A.size() || i <  B.size(); i++) {
            if (i < A.size()) t += A[i];
            if (i < B.size()) t += B[i];
            C.push_back(t % 10);
            t /= 10;
        }
        if (t != 0) C.push_back(t);
        string c = "";
        for (int i = C.size() - 1; i >= 0; i--) {
            c.append(C[i]);
        }
        return c;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    5.2 高精度减法

    vector sub(vector& A, vector& B) {
    	vector C;
    	int t = 0;
    	for (int i = 0; i < A.size(); i++) {
    		t += A[i];
    		if (i < B.size()) t -= B[i];
    		C.push_back((t + 10) % 10);
    		if (t < 0) t = -1;
    		else t = 0;
    	}
    
    	while (C.size() > 1 && C.back() == 0) C.pop_back();
    	return C;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    5.2 高精度乘法

    vector mul(vector& A, int b) {
        vector C;
        int t = 0;
        for (int i = 0; i < A.size() || t; i++) {
            if (i < A.size()) t += A[i] * b;
            C.push_back(t % 10);
            t /= 10;
        }
        while (C.size() > 1 && C.back() == 0) C.pop_back();
        return C;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    5.4 高精度除法

    vector div(vector& A, int b, int& r) {
        vector C(A.size());
        for (int i = 0; i < A.size(); i++) {
            r = r * 10 + A[i];
            C[i] = r / b;
            r %= b;
        }
    
        reverse(C.begin(), C.end());
        while (C.size() > 1 && C.back() == 0) C.pop_back();
        return C;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    6 进位制

    考察任意进制间的转换,下面以十进制和二进制之间的转换为例

    十进制->二进制:一直取余和除,直到0
    二进制->十进制:每次先乘以进制然后加上最小位

    typedef long long LL;
    
    using namespace std;
    //将r进制转为十进制
    LL toTen(string s, int r) {
    
        LL res = 0;
        for (int i = 0; i < s.length(); i++) {
            res = res * r + mp[s[i]];
        }
        return res;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    将十进制转换为r进制

        vector nums;
        while (n) {
            nums.push_back(n % r);
            n /= r;
        }
        int res = 0;
        for (int i = 0; i < nums.size(); i++) {
            res = res* r + nums[i];
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    7 数学常识

    7.1 判断一个数是否为质数

    bool isPrime(int r) {
        if (r <= 1) return false;
        for (int i = 2; i * i <= r; i++) {
            if (r % i == 0) return false;
        }
        return true;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    i*i可能会超出int的范围所以一般写成下面的那种写法

    bool isPrime(int r) {
        if (r <= 1) return false;
        for (int i = 2; i <= sqrt(r); i++) {
            if (r % i == 0) return false;
        }
        return true;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    7.2 最大公约数

    /*
    求两个正整数 a 和 b 的 最大公约数 d
    则有 gcd(a,b) = gcd(b,a%b)
    证明:
        设a%b = a - k*b 其中k = a/b(向下取整)
        若d是(a,b)的公约数 则知 d|a 且 d|b 则易知 d|a-k*b 故d也是(b,a%b) 的公约数
        若d是(b,a%b)的公约数 则知 d|b 且 d|a-k*b 则 d|a-k*b+k*b = d|a 故而d|b 故而 d也是(a,b)的公约数
        因此(a,b)的公约数集合和(b,a%b)的公约数集合相同 所以他们的最大公约数也相同 证毕#
    */
    int gcd(int a, int b){
        return b ? gcd(b,a%b):a; 
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    8 字符串处理

    主要考察string类常见操作,以及编码能力(做好一到算法题写一两百行代码的心里准备)

    9 模拟

    主要考察英语阅读理解能力和编码能力(做好一到算法题写一两百行代码的心里准备),读懂题目按照题目规则编写代码即可

  • 相关阅读:
    【华为OD机试真题 python】打印机队列【2022 Q4 | 100分】
    大厂程序员上班猝死成常态?
    操作系统——什么是程序和编译器
    python xml 解析
    C语言:指针(函数回调)
    C 风格文件输入/输出 (std::fflush)(std::fwide)(std::setbuf)(std::setvbuf)
    ant design pro git提交error; Angular 团队git提交规范
    手机木马远程控制复现
    Shiro-集成验证码
    StarRocks 的学习笔记
  • 原文地址:https://blog.csdn.net/woyaottk/article/details/128173097