2020年10月杂记

2020年10月杂记

关键字: 每日一题、操作系统

  • 二叉树的前序遍历——LeetCode 144
    一般情况下,二叉树的前序遍历的实现有递归和迭代两种方法。

递归实现:
递归实现比较简单,大致思路就是先访问当前节点,再对左子树和右子树调用该方法。主要需要注意的就是对空指针的判断。
参考代码如下:

/*
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
//递归
	void dfs(vector<int> &d,TreeNode* r){
	    if(r==nullptr){
	        return ;
	    }
	    d.push_back(r->val);
	    dfs(d,r->left);
	    dfs(d,r->right);
	}
	vector<int> preorderTraversal(TreeNode* root) {
	    vector<int> data;
	    dfs(data,root);
	    return data;
	}
};

递归实现思路很简单,但会消耗较多的时间和空间。
对于其他顺序遍历的递归实现,例如中序和后序遍历,其实现思路与前序遍历递归实现基本一致。

迭代实现:
迭代实现主要是利用了栈这个数据结构。
大致思路就是访问当前结点,并依次压入当前结点的右左结点,循环执行,直到栈为空,即遍历结束。同样需要注意的是对空指针的判断。
参考代码如下:

class Solution {
public:
    //迭代 
    vector<int> preorderTraversal(TreeNode* root) {
       stack<TreeNode*> node;
       vector<int> data;
       if(root==nullptr){
           return data;
       } 
       node.push(root);
       while(!node.empty()){
           data.push_back(node.top()->val);
           TreeNode* cur=node.top();
           node.pop();
           if(cur->right!=NULL){
               node.push(cur->right);
           }
           if(cur->left!=NULL){
               node.push(cur->left);
           }
       }
       return data;
    }
};

  • 死锁避免——银行家算法

避免死锁与预防死锁一样,都属于事先预防。但不同的是,预防死锁是破环产生死锁的四个必要条件,避免死锁是在系统在动态分配资源的时候,防止系统进入不安全的状态,从而避免发生死锁,优点就是限制条件弱,可以获得比较好的系统性能。

既然提到了安全状态,那么就简单介绍一下什么是安全状态。所谓安全状态就是,系统能按某种顺序推进进程执行,为每个进程分配资源,进程依次执行能正常获取所需的资源且能正常完成,则称此时的进程推进顺序为一个安全序列。如果系统找不到一个安全序列,则成为系统当前处于不安全的状态。
不是说不安全状态就一定是死锁状态,只是系统进入不安全状态后,很可能进入死锁状态。

银行家算法就是最著名的死锁避免算法。大致的思想就是:将操作系统当成银行家,进程申请资源就像是客户申请贷款,操作系统管理资源就像是银行家管理资金。操作系统按照银行家制定的规则对进程进行资源的分配。
分配规则如下:
进程运行之前先声明对各种资源的最大需求量,当进程执行时继续申请资源时,操作系统将测试其已有的资源与所申请的资源之和是否超过其声明的最大需求量,若超过则拒绝分配,否则继续测试系统已有资源是否可以满足进程所申请的资源,若满足,则按申请量进行资源的分配,否则推迟分配。

实现银行家算法的数据结构
可用资源(Available):系统有的数据结构,用一个数组来表示每种资源的数量。
最大需求矩阵(Max):一个二维矩阵,每一行表示一个进程对不同资源的最大需求量。
分配矩阵(Allocation):一个二维矩阵,表示已经分配给各进程的资源,每一行代表每一个进程已经分配到的各种资源的数量。
需求矩阵(Need):一个二维矩阵,表示每个进程接下来最多还需要多少资源,每一行代表每一个进程最多还需要各种资源的数量。

一般情况下,上述矩阵有以下关系:Need = Max - Allocation。

银行家算法具体描述:

  1. 若对资源申请的数量小于等于Need,则转向步骤2;否则认为出错,因为其所需要的资源数量大于所声明的最大资源数量。

  2. 若对资源申请的数量小于等于Available,则转向步骤3;否则,表示暂时无足够的资源进行分配,进程等待。

  3. 系统试探性地将资源分配给对应的进程,并修改相关的资源数值(Available、Allocation、Need)。

  4. 系统执行安全性算法,检查此次资源分配后,系统是否处于安全状态。若安全,才正式将资源分配给对应的进程;否则,将此次试探分配撤销并将对应的资源数值修改回原值,进程等待。

上面提到了安全性算法,安全性算法就是用来检查当前是否存在一个安全序列,具体实现步骤如下:

  1. 初始时,安全序列为空。
  2. 从Need矩阵中找出符合以下要求的行:该行进程不在当前安全序列中,而且其所有列的值均小于等于剩余资源的数量。找出符合要求的行,则将对应的进程加入当前安全序列,进入步骤3;否则,进入步骤4。
  3. 进程进入安全序列后,可以顺利执行完,直至完成,并释放进程占用的资源,更新对应的资源数值。返回步骤2。
  4. 若此时有进程都在安全序列中,则系统处于安全状态;否则,系统处于不安全的状态。

  • 死锁检测和解除
    死锁检测———资源分配图
    首先需要了解一下死锁的表示———资源分配图。
    死锁的条件是当前状态的资源分配图是不可完全简化的,该条件为死锁定理。
    死锁解除
    死锁解除的方法主要有以下三种:
  1. 资源剥夺法:挂起某些死锁进程,并抢占它所占用的资源,并将这些资源分配给其他的死锁进程。但是需要注意的是,要防止被挂起的进程长时间得不到资源。
  2. 撤销进程法:强制撤销部分甚至全部死锁进程并剥夺这些进程的资源。对于撤销的原则,可以按照进程优先级以及撤销进程的代价的高低进行。
  3. 进程回退法:让一个或多个进程回退到足以避免死锁的地步。进程回退时,会主动释放资源,但是要求系统保持进程的历史信息,设置还原点。

  • 内存管理——操作系统对内存的划分和动态分配

内存管理主要实现的功能
内存空间的分配与回收:由操作系统来完成主存储器空间的分配和管理。
地址转换:逻辑地址与物理地址的转换。
内存空间的保护:利用虚拟存储技术或自动覆盖技术,从逻辑上扩充内存。
存储保护:保证各道作业在各自的存储空间内运行,互不干扰。

首先了解一下程序运行的基本原理和要求
创建进程首先需要将程序(代码)和数据装入内存。将用户源程序变为在内存中执行的程序,通常需要以下几个步骤:

  1. 编译:由编译程序将用户的源代码编译成若干模块。
  2. 链接:由链接程序将编译后形成的一组模块目标及所需的库函数链接在一起,形成一个完整的装入模块。
  3. 装入:由装入程序将装入模块装入内存运行。

链接方式:

  1. 静态链接:程序运行之前,先将各目标模块及他们所需的库函数链接成一个完整的可执行程序,以后不再拆开。
  2. 装入时动态链接:在装入内存时,边装入边链接。
  3. 运行时动态链接:对某些目标模块的链接,是在程序执行中需要该目标模块时才进行的(便于修改和更新,便于实现对目标模块的共享)。

装入方式:

  1. 绝对装入:在编译时,若知道程序将驻留在内存的某个位置,则编译程序将产生绝对地址的目标代码。绝对装入程序按照装入模块中的地址,将程序和数据装入内存。由于程序中的逻辑地址与实际内存地址完全相同,因此不需要对程序和数据的地址进行修改。这种方式只适用于单道程序环境。绝对地址可以由程序员给出也可在编译或汇编时给出。通常情况下,程序中采用的是符号地址,编译或汇编时才转换成绝对地址。

  2. 可重定位装入:在多道程序环境下,多个目标模块的地址都是从0开始,程序中的其他地址都是相对于初始地址的,此时可以采用重定位装入方式。根据内存的使用情况,将装入模块装入内存的适当位置。装入时对目标程序中的指令和数据的修改过程称为重定位,地址变换通常是在装入时一次完成的,因此也称为静态重定位。其特点就是,一个作业装入内存时,必须给它分配要求的全部内存空间,若没有足够的内存空间,则不能装入该作业。此外,作业一旦进入内存,整个运行期间就不能在内存中移动,也不能再申请内存空间。

  3. 动态运行时装入:这种方式也称为动态重定位。程序在内存中若发生移动,则需要采用动态的装入方式。装入程序把装入内存后,并不会将装入模块中的地址转换为绝对地址,而是将地址转换的工作推迟到程序真正执行到的时候才进行。所以,装入内存的地址都是相对地址,这种方式需要一个重定位寄存器的支持。其特点就是,可以将程序分配到不连续的内存空间,在程序运行之前,可以装入部分代码即可投入运行,而且可以根据需求动态申请内存空间,便于程序段的共享,可以提供较大的内存空间。


  • 内存保护
    内存分配前需要保护操作系统不受用户进程的影响,同时保护用户进程不受其他用户进程的影响。主要有以下两个方法:
  1. 在CPU中设置一对上、下限寄存器,存放用户作业在主存中的下限和上限地址,每当CPU要访问一个地址时,分别和两个寄存器的值相比,判断是否越界。
  2. 采用重定位寄存器(基址寄存器)和界地址寄存器(限长寄存器)来实现这种保护。重定位寄存器含最小的物理地址值,界地址寄存器含逻辑地址的最大值。每个逻辑地址的地址值必须小于界地址寄存器,则加上重定位寄存器的值后映射成物理地址,再送交内存单元。

10月27日,暂时先写到这里。
10月28日,再来。

  • 独一无二的出现次数——LeetCode 1207

给你一个整数数组 arr,请你帮忙统计数组中每个数的出现次数。如果每个数的出现次数都是独一无二的,就返回 true;否则返回 false。

大致的思路:首先利用哈希表,存下每个数字出现的次数;然后再用已有的每个数字出现的次数去新建一个集合Set(利用集合会去掉重复元素的特点),最后比较哈希表和集合的Size就可以得出是否所有的数字出现的次数都是独一无二的。参考代码如下(LeetCode官方解答):

class Solution {
public:
    bool uniqueOccurrences(vector<int>& arr) {
        unordered_map<int, int> occur;
        for (const auto& x: arr) {
            occur[x]++;
        }
        unordered_set<int> times;
        for (const auto& x: occur) {
            times.insert(x.second);
        }
        return times.size() == occur.size();
    }
};

这里想重新再复习以下STL中的unordered_map、map、unordered_set、set、pair等数据结构。

map:
元素一对一的映射(容器),无重复值元素,实现了基于关键字的快速查找。底层是利用红黑树进行实现的。红黑树(非严格平衡二叉树),红黑树有自动排序的功能,所以map内部的所有元素都是有序的,红黑树的每一个节点都代表这map的一个元素。因此对map进行插入、删除、查找等操作都是相当于对红黑树进行操作。最后根据树的中序遍历可以将键值按照大小顺序遍历出来。

unordered_map:
unordered_map内部是一个哈希表(散列表,把关健值映射到哈希表中的某一个位置,这样查找时间复杂度是O(1)),元素的排列顺序是无序的。

优缺点:
(参考博客https://blog.csdn.net/lxq19980430/article/details/102939655)
map:

  1、优点:
       (1)有序、
       (2)时间复杂度低:内部结构时红黑树,红黑树很多操作都是在
       O(logn)的时间复杂度下实现的,因此效率高。
  2、缺点:
      空间占有率高,因为map内部实现是红黑树,虽然它时间复杂度低,
  运行效率高,但是因为每一个节点都需要额外保存父节点、孩子节点和红黑树性质,
  这样使得每一个节点都占用大量的空间。
  3、应用场景:应用于对顺序有要求的问题,用map会更高效。

unordered_map:

 1、优点:内部结构是哈希表,查找为O(1),效率高。
 2、缺点:哈希表的建立耗费时间。
 3、应用场景:对于频繁查找的问题,用unordered_map更高效。

pair:
可以将两个元素关联起来,两个成员分别为first和second,可以像使用结构体一样来使用pair。map和unordered_map的实现就利用了pair。

set:
集合(容器)。无重复值的元素,可以快速查找。
unordered_set:
set与unordered_set区别和map与unordered_map区别类似,set基于红黑树实现,红黑树具有自动排序的功能,因此map内部所有的数据,在任何时候,都是有序的。unordered_set基于哈希表,数据插入和查找的时间复杂度很低,几乎是常数时间,而代价是消耗比较多的内存,无自动排序功能。底层实现上,使用一个下标范围比较大的数组来存储元素,形成很多的桶,利用hash函数对key进行映射到不同区域进行保存。

总之,底层使用hash方法的,进行任意元素的读取、修改、插入的时间复杂度很低,而占有空间较大;底层使用红黑树的,所有元素都是有序的,相对来说,访问元素的时间复杂度较高。


  • 求根到叶子节点数字之和——LeetCode 129
    给定一个二叉树,它的每个结点都存放一个 0-9 的数字,每条从根到叶子节点的路径都代表一个数字。例如,从根到叶子节点路径 1->2->3 代表数字 123。计算从根到叶子节点生成的所有数字之和。

谈谈我看到这题的思路,很直观,脑子里一瞬间就会想到直接用深度优先搜索来遍历二叉树,一般会用递归实现(简单),只需考虑清楚函数出口和递归调用就可以了,这里出口无非就是遇到了叶子节点,就可生成一个数字,存起来,最后得到所有的数字累加就可以了。当然也可以用迭代实现,大致的思路也差不多。参考代码如下:

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
//shen du you xian
    vector<int> data;
    void dfs(TreeNode* r,int cur){
    //其实也可以修改函数出口和递归调用的方式,
    //直接返回累加值,不用得到所有的数字
        if(r==nullptr){
            return ;
        }
        cur = cur*10 + r->val;
        if(r->left==nullptr&&r->right==nullptr){
            data.push_back(cur);
            return ;
        }
        if(r->left!=nullptr){
            dfs(r->left,cur);
        }
        if(r->right!=nullptr){
            dfs(r->right,cur);
        }
    }
    int sumNumbers(TreeNode* root) {
        if(root==nullptr){
            return 0;
        }
        dfs(root,0);
        int len=data.size(),count=0;
        for(int i=0;i<len;i++){
            count += data[i];
        }
        return count;
    }
};

除了深度优先,宽度优先当然也是可以的,只不过思路上可能会复杂一点点,但其实在时空复杂度上并没有带来好处,因此这里也不作过多的介绍了(其实是自己也懒得去看了)。这里放以下LeetCode官方的参考代码:

class Solution {
public:
    int sumNumbers(TreeNode* root) {
        if (root == nullptr) {
            return 0;
        }
        int sum = 0;
        queue<TreeNode*> nodeQueue;
        queue<int> numQueue;
        nodeQueue.push(root);
        numQueue.push(root->val);
        while (!nodeQueue.empty()) {
            TreeNode* node = nodeQueue.front();
            int num = numQueue.front();
            nodeQueue.pop();
            numQueue.pop();
            TreeNode* left = node->left;
            TreeNode* right = node->right;
            if (left == nullptr && right == nullptr) {
                sum += num;
            } else {
                if (left != nullptr) {
                    nodeQueue.push(left);
                    numQueue.push(num * 10 + left->val);
                }
                if (right != nullptr) {
                    nodeQueue.push(right);
                    numQueue.push(num * 10 + right->val);
                }
            }
        }
        return sum;
    }
};

  • C++11新特性

nullptr

auto、decltype

迭代器

初始化列表

模板增强

构造函数

Lambda

新增容器

正则表达式

语言级线程支持

右值引用和move语义


  • 岛屿的周长——LeetCode 463

给定一个包含 0 和 1 的二维网格地图,其中 1 表示陆地 0 表示水域。
网格中的格子水平和垂直方向相连(对角线方向不相连)。整个网格被水完全包围,但其中恰好有一个岛屿(或者说,一个或多个表示陆地的格子相连组成的岛屿)。
岛屿中没有“湖”(“湖” 指水域在岛屿内部且不和岛屿周围的水相连)。格子是边长为 1 的正方形。网格为长方形,且宽度和高度均不超过 100 。计算这个岛屿的周长。

大致的思路:遍历整个网格,遇到陆地就遍历它的四邻域,如果有水域,就说明对应的边是岸线,周长加1。
参考代码:

class Solution {
public:
    int islandPerimeter(vector<vector<int>>& grid) {
        int length=0;
        int row=grid.size(),col=grid[0].size();
        int dir[2][4]={{0,0,-1,1},{-1,1,0,0}};
        for(int i=0;i<row;i++){
            for(int j=0;j<col;j++){
                if(grid[i][j]==1){
                    for(int k=0;k<4;k++){
                        int x = i + dir[0][k];
                        int y = j + dir[1][k];
                        if(x<0||y<0||x>=row||y>=col){
                            length++;continue;
                        }
                        if(grid[x][y]==0){
                            length++;
                        }
                    }
                }
            }
        }
        return length;
    }
};
  • O(1)时间插入、删除和获得随即元素,且允许重复元素——LeetCode 381

设计一个支持在平均 时间复杂度 O(1) 下, 执行以下操作的数据结构。
注意: 允许出现重复元素。
insert(val):向集合中插入元素 val。
remove(val):当 val 存在时,从集合中移除一个 val。
getRandom:从现有集合中随机获取一个元素。每个元素被返回的概率应该与其在集合中的数量呈线性相关。

主要思路:
首先用vector存元素,可以满足插入元素的O(1)。
删除就需要使用索引了,因为允许重复元素,所以不能只用unordered_map<int,int>,可以考虑到元素下标是不重复的,所以可以使用nordered_map<int,unordered_set< int>>,这样就可以对同一个值,存多个下标了。有了索引之后,元素的顺序就显得不那么重要,因此在删除元素的时候,可以考虑使用末尾元素与所删除的元素(下标从集合中随意选取一个)进行交换,再将末尾元素删除。而对于索引,则是将删除值对应的下标集合中删除所选取的下标,并且,将原来末尾值对应的下标集合更新(将末尾下标删除、添加之前选中的删除值的下标)。
产生随机元素就比较容易了。
参考代码如下:

class RandomizedCollection {
public:
    vector<int> data;
    unordered_map<int,unordered_set<int>> index;
    int length;
    /** Initialize your data structure here. */
    RandomizedCollection() {
        length = 0;
    }
    /** Inserts a value to the collection. Returns true if the collection did not already contain the specified element. */
    bool insert(int val) {
        int size=data.size();
        data.push_back(val);
        index[val].insert(length);
        length++;
        return index[val].size()==1;
    }
    
    /** Removes a value from the collection. Returns true if the collection contained the specified element. */
    bool remove(int val) {
        if(index.find(val) == index.end()){
            return false;
        }
        //xia mian zhu yao shi jiang shu ju he suo yin jin xing le geng xin
        //mo wei yuan su yu suo shan chu de yuan su jin xing jiao huan,
        //ran hou,shan chu mo wei yuan su.
        int i = *(index[val].begin());//i ke neng shi mo wei yuan su
        data[i] = data.back();
        index[val].erase(i);
        index[data[i]].erase(data.size() - 1);//ruo yi jing shan le ye bu hui bao cuo
        if (i < data.size() - 1) {
            index[data[i]].insert(i);
        }
        if (index[val].size() == 0) {
            index.erase(val);
        }
        data.pop_back();
        length--;
        return true;
    }
    /** Get a random element from the collection. */
    int getRandom() {
        return data[rand() % data.size()];
    }
};

/**
 * Your RandomizedCollection object will be instantiated and called as such:
 * RandomizedCollection* obj = new RandomizedCollection();
 * bool param_1 = obj->insert(val);
 * bool param_2 = obj->remove(val);
 * int param_3 = obj->getRandom();
 */
  • 内存扩充技术——覆盖与交换

  • 红黑树、B树、B+树、B-树

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值