zoukankan      html  css  js  c++  java
  • [基础数据结构]二叉树完全总结

      树形结构是数据结构中一种非常常用的非线性结构。通常用来表示具备分支关系的层次结构。其中二叉树又是树形结构中最简单最常见的一种。

    一、定义 

      二叉树,顾名思义,只有两个分叉的树,他的特点就是每个节点至多只有两颗子树(即二叉树中不存在度大于2的节点),通常情况下,我们称二叉树的两颗子树为左子树和右子树。二叉树又可递归定义为:①一个空树;②左子树和右子树均为二叉树。换句话说,二叉树有三个部分组成,根节点和同为二叉树的左右子树。这种递归定义非常有趣,它将帮助我们解决很多二叉树的操作问题。

      二叉树中有两种特殊的形态,分别是完全二叉树和满二叉树。

      满二叉树是指,除叶子节点外,每个节点都有两个子节点,从视觉上讲,就是这颗二叉树被完全填充。

      完全二叉树形象上讲是指“最右下角”的节点可能空缺,即只能是最右边且是最大层次的节点空缺。他有两个重要的特点:

      ①、叶子节点只可能出现在最大的两层上。

      ②、对任意节点,若其右子树的最大层次为l,那么其左子树的最大层次必定为l或l+1。

    二、性质

      性质1:在二叉树的第i层至多有2i-1个节点(i>=1),这点有数学归纳法很容易证得。

      性质2:深度为k的二叉树至多有2k-1个节点(k>=1),显然,节点最多的情形即为满二叉树。

      性质3:对任意一颗二叉树,若其叶子节点(其度为0)数为n0,度为2的节点数为n2,则有n0=n2+1。

      证明:设度为1的节点数为n1,那么总结点数n = n0+n1+n2;

          设二叉树的分支数为B,由于除了根节点外,每个节点都由一个分支射入,即对应一个分支,即n = B+1。

          而每个分支都是有度为1或2的节点射出的,即B = n1+2*n2。

          则有n = n1+2*n2+1

      综上,有

          n0 = n2+1

    三、二叉树的遍历

      遍历操作是二叉树最重要的操作,其他的操作大都围绕遍历来完成。根据二叉树的递归定义我们可以知道,只要分别完成对二叉树的三个部分的遍历,即可完成对整个二叉树的遍历。

      二叉树的遍历通常根据对根节点的访问顺序,分为先序遍历、中序遍历和后续遍历,即先(根)序遍历、中(根)序遍历和后(根)序遍历。以下将详细阐述三种遍历方式。

      PreOder(T) = T+PreOder(T的左子树)+PreOder(T的右子树)

      InOder(T)=InOder(T的左子树)+T+InOder(T的右子树)

           PostOder(T) = PostOder(T的左子树)+PostOder(T的右子树)+T

      1、二叉树的链式存储结构:

      由二叉树的定义可知,每个节点至少要包含三个部分,即数据、左节点指针、右节点指针。当然,在一些特殊的场合,可能还会有一些其他数据域,如父节点指针、层次、访问标记等。目前暂时考虑最简单的情形。  

    1 typedef char TElemType;
    2 /****二叉树的节点的存储结构****/
    3 typedef struct BiTNode
    4 {
    5     TElemType data;
    6     struct BiTNode *lchild, *rchild;
    7 }BiTNode,*BiTree;
    View Code

      2、先序遍历:

        递归式先序遍历:

        根据先序遍历的定义,先访问根节点,再依次访问左右子树,不难得出先序遍历的递归代码如下:    

     1 bool PreOrderTraverse_rec( BiTree T ,bool (* PrintElem)(TElemType e)) 
     2 {
     3     if(T)
     4     {
     5         if (PrintElem(T->data))    
     6         {    
     7             if (PreOrderTraverse_rec( T->lchild,PrintElem))
     8             {
     9                 if (PreOrderTraverse_rec( T->rchild ,PrintElem)) 
    10                     return true;
    11             }
    12         }
    13         return false;
    14     }
    15     else 
    16         return true;
    17 }
    View Code

        PrintElement是每个元素的操作函数,最简单的操作就是打印了。    

    1 bool PrintElem( TElemType e)
    2 {
    3     cout<<e<<" ";
    4     return true;
    5 }
    View Code

        非递归式先序遍历:

        采用栈模拟递归过程实现非递归。对栈中的每一个节点,执行操作,并压栈,沿着左路走到底,再弹出,将右节点压栈,重复上述操作。

           算法步骤:

          a.从根开始,访问栈顶元素,不断将左子树指针压栈,直到指针为空为止;

           b.弹栈,对栈顶元素。

           如果,该节点的右结点不为空,则转到a;

           否则,转到b。

          c.栈空,则结束

    版本一:   

     1 ///////////////////////////////////////////////////////////////
     2 //用非递归的方式先序遍历二叉树,若失败则返回false
     3 ///////////////////////////////////////////////////////////////
     4 bool PreOrderTraverse( BiTree T ,bool (*PrintElem)(TElemType e)) 
     5 {
     6     SqStack S; BiTree p;
     7     InitStack(S); Push(S,T);
     8     while (!StackEmpty(S))
     9     {
    10         while(GetTop(S,p)&&p)
    11         {
    12             if (!PrintElem(p->data)) return false;
    13             Push(S,p->lchild);
    14         }
    15         Pop(S,p);//弹出压入的空字符
    16         if(!StackEmpty(S))
    17         {
    18             Pop(S,p);
    19             Push(S,p->rchild);
    20         }
    21     }
    22     cout<<endl;
    23     return true;
    24 }
    View Code 

    版本二:

     1 ///////////////////////////////////////////////////////////////
     2 //用非递归的方式先序遍历二叉树第二种方法,若失败则返回false
     3 ///////////////////////////////////////////////////////////////
     4 bool PreOrderTraverse2( BiTree T ,bool (*PrintElem)(TElemType e)) 
     5 {
     6     SqStack S; BiTree p;
     7     InitStack(S); 
     8     p = T;
     9     while(p||!StackEmpty(S))
    10     {
    11         if(p)
    12         {
    13             Push(S,p);
    14             if (!PrintElem(p->data)) return false;
    15             p = p->lchild;
    16         }
    17         else
    18         {
    19             Pop(S,p);
    20             p = p->rchild;
    21         }
    22     }
    23     
    24     cout<<endl;
    25     return true;
    26 }
    View Code

      3、中序遍历:

        递归式中序遍历:

     1 ///////////////////////////////////////////////////////////////
     2 //用递归的方式中序遍历二叉树,若失败则返回false
     3 ///////////////////////////////////////////////////////////////
     4 bool InOrderTraverse_rec( BiTree T,bool (*PrintElem)(TElemType e)) //递归
     5 {
     6     if(T)
     7     {
     8         if (InOrderTraverse_rec( T->lchild ,PrintElem))
     9             if (PrintElem(T->data))
    10                 if (InOrderTraverse_rec( T->rchild, PrintElem)) 
    11                     return true;
    12         return false;
    13     }
    14     else 
    15         return true;
    16 }
    View Code

        非递归式中序遍历:

        与先序遍历类似。先将左子树节点压栈,直到尽头,弹出时,执行操作,再将右子树节点压栈,重复上述操作。

        算法步骤:

          a.从根开始,不断将左子树指针压栈,直到指针为空为止。 

           b.访问栈顶元素。

           如果,该节点的右结点不为空,则转到a;

           否则,转到b。

          c.栈空,则结束。

    版本一:

     1 ///////////////////////////////////////////////////////////////
     2 //用非递归的方式中序遍历二叉树,若失败则返回false
     3 ///////////////////////////////////////////////////////////////
     4 bool InOrderTraverse( BiTree T ,bool(*PrintElem)(TElemType e)) //非递归1
     5 {
     6     SqStack S; BiTree p;
     7     InitStack(S); Push(S,T);
     8     while(!StackEmpty(S))
     9     {
    10         while(GetTop(S,p)&&p)
    11         {
    12             Push(S,p->lchild);
    13             p=p->lchild;
    14         }
    15         Pop(S,p);
    16         if(!StackEmpty(S))
    17         {
    18             Pop(S,p);
    19             if (!PrintElem(p->data)) return false;
    20             Push(S,p->rchild);
    21         }
    22     }
    23     cout<<endl;
    24     return true;
    25 }
    View Code

    版本二:

     1 ///////////////////////////////////////////////////////////////
     2 //用非递归的方式中序遍历二叉树,若失败则返回false
     3 ///////////////////////////////////////////////////////////////
     4 bool InOrderTraverse2( BiTree T ,bool(*PrintElem)(TElemType e)) //非递归1
     5 {
     6     SqStack S; BiTree p;
     7     InitStack(S); 
     8     p = T;
     9     while(p||!StackEmpty(S))
    10     {
    11         if(p)
    12         {
    13             Push(S,p);
    14             p = p->lchild;
    15         }
    16         else
    17         {
    18             Pop(S,p);
    19             if (!PrintElem(p->data)) return false;
    20             p = p->rchild;
    21         }
    22     }
    23     
    24     cout<<endl;
    25     return true;
    26 }
    View Code

      可以发现,非递归式的先序遍历和中序遍历代码非常相似,仅是改变了操作函数的位置。

      4、后序遍历:

        递归式后序遍历:

     1 ///////////////////////////////////////////////////////////////
     2 //用递归的方式后序遍历二叉树,若失败则返回false
     3 ///////////////////////////////////////////////////////////////
     4 bool PostOrderTraverse_rec( BiTree T ,bool (* PrintElem)(TElemType e))
     5 {
     6     if(T)
     7     {
     8         if (PostOrderTraverse_rec( T->lchild ,PrintElem))
     9             if (PostOrderTraverse_rec( T->rchild, PrintElem))
    10                 if (PrintElem(T->data))
    11                     return true;
    12         return false;
    13     }
    14     else 
    15         return true;
    16 }
    View Code

        非递归式后序遍历:

        后续遍历的非递归方式相比前面两种有点复杂。主要原因在于前面两种遍历,每个结点仅有一次出入栈,而后序遍历中,每个节点会有两次出入栈.

        算法步骤:

          a.从根开始,不断将左子树指针压栈,直到指针为空为止;

           b.看栈顶元素

            如果:它没有节点,或者它的右结点被访问过,访问该结点,弹栈;转到b;

            否则:它的节点不空且未被访问过,则必须将它的右结点继续压栈;因为可能他的右结点是另一颗子树的根节点。转到a;

          c.栈空,则结束。

    版本一:

    根据算法步骤,在原结点结构体中加入一个字段代表是否访问过。

    1 typedef struct BiTNode
    2 {
    3     TElemType data;
    4     struct BiTNode *lchild, *rchild;
    5     int mark ; //初始化为未被访问过,0
    6 }BiTNode,*BiTree;
     1 ///////////////////////////////////////////////////////////////
     2 //用非递归的方式后序遍历二叉树,若失败则返回false
     3 ///////////////////////////////////////////////////////////////
     4 bool PostOrderTraverse( BiTree T ,bool (* PrintElem)(TElemType e))
     5 {
     6     BiTree p,q;
     7     SqStack S;
     8 
     9     p = T;
    10     q = NULL;
    11     InitStack(S);
    12 
    13     while (p || !StackEmpty(S))
    14     {
    15         if (p && p->mark==0)
    16         {
    17             Push(S,p);
    18             p = p->lchild;
    19         }
    20         else
    21         {
    22             Pop(S,p);
    23 
    24             if (p->rchild && p->rchild->mark==0)  //存在右孩子,则还把该节点压栈,顺着其左子树继续
    25             {
    26                 q = p;
    27                 p = p->rchild;//讨论p的右结点,现在右结点还没有被压栈
    28                 Push(S,q);//把原来的p重新压回栈
    29                 continue;
    30             }
    31 
    32             if (!PrintElem(p->data)) return false;
    33             p->mark = 1;//标志为已访问过
    34             if (p == T) break;//已经访问到了头结点
    35         }//else
    36     }//while
    37     cout<<endl;
    38     return true;
    39 }
    View Code

    版本二:

    算法步骤中,要访问某节点,必须确认其右结点已经访问过,即右结点必须是上一个访问的节点,所以版本二中通过保存上次访问节点来实现。不需要添加额外字段。

     1 ///////////////////////////////////////////////////////////////
     2 //用非递归的方式后序遍历二叉树,若失败则返回false
     3 ///////////////////////////////////////////////////////////////
     4 bool PostOrderTraverse2( BiTree T ,bool (* PrintElem)(TElemType e))
     5 {
     6     BiTree p,q;
     7     SqStack S;
     8 
     9     p = T;
    10     q = NULL;
    11     InitStack(S);
    12     Push(S,p);
    13     while(!StackEmpty(S))
    14     {
    15         while(GetTop(S,p) && p)
    16         {
    17             Push(S,p->lchild);
    18             p=p->lchild;
    19         }
    20         Pop(S,p);
    21         q = NULL;//代表刚刚访问的节点
    22         while(!StackEmpty(S)) 
    23         {
    24             GetTop(S,p);
    25             if (p->rchild==NULL || p->rchild==q)//q表示刚访问过的结点
    26             {
    27                 if (!PrintElem(p->data)) return false;  
    28                 q=p; //记录访问过的结点
    29                 Pop(S,p);
    30             }
    31             else
    32             {
    33                 p=p->rchild;
    34                 Push(S,p);
    35                 break;
    36             }
    37         }
    38 
    39     }
    40     cout<<endl;
    41     return true;
    42 }
    View Code

    版本三:

    在原结构体中加入字段标记左右子树,当为右子树时,说明右子树访问过,可以访问该节点。

    1 typedef enum{L,R} TagType;
    2 
    3 typedef struct BiTNode
    4 {
    5     TElemType data;
    6     struct BiTNode *lchild, *rchild;
    7     TagType tag;
    8 }BiTNode,*BiTree;
     1 ///////////////////////////////////////////////////////////////
     2 //用非递归的方式后序遍历二叉树,若失败则返回false
     3 ///////////////////////////////////////////////////////////////
     4 bool PostOrderTraverse3( BiTree T ,bool (* PrintElem)(TElemType e))
     5 {
     6     BiTree p;
     7     SqStack S;
     8 
     9     p = T;
    10     InitStack(S);
    11     
    12     do
    13     {
    14         while(p!=NULL)
    15         {
    16             p->tag = L;//标记为左子树
    17             Push(S,p);
    18             p = p->lchild;
    19         }
    20         while(!StackEmpty(S) && GetTop(S,p) && p->tag==R)//表示右子树访问结束,可以访问该节点
    21         {
    22             Pop(S,p);
    23             if (!PrintElem(p->data)) return false;  
    24         }
    25 
    26         if(!StackEmpty(S))
    27         {
    28             GetTop(S,p);
    29             p->tag = R;//标记为右子树
    30             p = p->rchild;
    31         }
    32     }while(!StackEmpty(S));
    33     cout<<endl;
    34     return true;
    35 }
    View Code

    版本四:

    前面所有的方式,本质上都是通过栈记录历史信息来模拟递归,版本四提供了一种巧妙的方法,可以不用栈,实现非递归式后序遍历。具体的实现方法是在节点中加入一个状态域来保存当前状态,是该访问左子树、右子树,还是访问节点。

    1 typedef enum{L,R,V} TagType;
    2 
    3 typedef struct BiTNode
    4 {
    5     TElemType data;
    6     struct BiTNode *lchild, *rchild;
    7     TagType tag;
    8 }BiTNode,*BiTree;
     1 ///////////////////////////////////////////////////////////////
     2 //用非递归的方式后序遍历二叉树,若失败则返回false
     3 ///////////////////////////////////////////////////////////////
     4 bool PostOrderTraverse4( BiTree T ,bool (* PrintElem)(TElemType e))
     5 {
     6     BiTree p,q;
     7     p = T;
     8     
     9     while(p)
    10     {
    11         switch(p->tag)
    12         {
    13         case L:
    14             p->tag = R;//接下来要访问右子树
    15             if(p->lchild)
    16                 p = p->lchild;
    17             break;
    18         case R:
    19             p->tag = V;//右子树访问完毕,接下来可以访问根了
    20             if(p->rchild)
    21                 p = p->rchild;
    22             break;
    23         case V:
    24             if (!PrintElem(p->data)) return false;
    25             p->tag = L;
    26             p = GetParent(T,p);//指向父节点
    27             break;
    28         }
    29     }
    30     cout<<endl;
    31     return true;
    32 }
    View Code

    以上代码中有一部分是指向某节点的父节点,一种做法是节点数据域中加入一个指针指向父节点,在创建二叉树时就做好这项工作。另一种是每次都从二叉树中找某节点的父节点。上述代码用的是第二种方法:编写了从二叉树T中找到节点child的父节点的函数GetParent。

     1 BiTree GetParent(BiTree T,BiTree child)
     2 {
     3     LinkQueue Q; 
     4     BiTree p;
     5     Q.EnQueue(T);
     6     while(!Q.QueueEmpty())
     7     {
     8         Q.DeQueue(p);
     9         if (p->lchild == child || p->rchild == child)
    10             return p;
    11 
    12         if(p->lchild)
    13         {
    14             Q.EnQueue(p->lchild);
    15         }
    16 
    17         if(p->rchild)
    18         {
    19             Q.EnQueue(p->rchild);
    20         }
    21     }
    22     return NULL;
    23 }
    View Code 

      5、层序遍历:

      层序遍历形象的就是按层次访问二叉树。上面的节点永远都是先访问的,很容易联想到“先进先出”的队列。层序遍历就是借助队列来实现的。

      算法步骤:

          a.将根节点移进队列;

            b.访问队列头节点,若其有左右节点,分别将其左右节点也移进队列,转到b;

          c.队列空,则操作结束。

     1 ///////////////////////////////////////////////////////////////
     2 //层序遍历二叉树,若失败则返回false
     3 ///////////////////////////////////////////////////////////////
     4 bool LevelOrderTraverse( BiTree T, bool (* PrintElem)(TElemType e) )
     5 {
     6     LinkQueue Q; 
     7     BiTree p;
     8     Q.EnQueue(T);
     9     while(!Q.QueueEmpty())
    10     {
    11         Q.DeQueue(p);
    12         if (!PrintElem(p->data)) return false;
    13 
    14         if(p->lchild)
    15         {
    16             Q.EnQueue(p->lchild);
    17         }
    18 
    19         if(p->rchild)
    20         {
    21             Q.EnQueue(p->rchild);
    22         }
    23     }
    24     cout<<endl;
    25     return true;
    26 }
    View Code

    练习题:

    1、Binary Tree Preorder Traversal

    四、二叉树的其他操作

      1、递归创建二叉树

     1 bool CreateBiTreeFromStdin( BiTree &T )
     2 {
     3     TElemType ch;
     4     
     5     ch = getchar();
     6     if( ch=='#' ) T = NULL;
     7     else
     8     {
     9         if( !(T = (BiTNode *)malloc(sizeof(BiTNode))) ) 
    10             exit(OVERFLOW);
    11         T->data = ch;
    12         CreateBiTreeFromStdin( T->lchild);
    13         CreateBiTreeFromStdin( T->rchild);
    14     }
    15     return OK;
    16 }
    View Code

       2、递归复制二叉树

     1 /////////////////////////////////////////////////////////////////////////////
     2 //递归复制二叉树,若成功则返回true
     3 //////////////////////////////////////////////////////////////////////////////
     4 bool CopyTree(BiTree ST, BiTree &DT)
     5 {
     6     if (!ST) 
     7         DT = NULL;
     8     else
     9     {
    10         DT = (BiTree)malloc(sizeof(BiTNode));
    11         DT->data = ST->data;
    12 
    13         CopyTree(ST->lchild,DT->lchild);
    14         CopyTree(ST->rchild,DT->rchild);
    15     }
    16     return true;
    17 }
    View Code

         3、判断一个二叉树是否为完全二叉树

        利用层序遍历的思想

     1 ////////////////////////////////////////////////////////////////////////////
     2 //判断一个二叉树是否为完全二叉树,若是则返回true,否则返回false
     3 ////////////////////////////////////////////////////////////////////////////
     4 bool IsFullBiTree( BiTree T )
     5 {
     6     LinkQueue Q;
     7     BiTree p;
     8         int flag=0;
     9         Q.EnQueue(T); 
    10 
    11     while(!Q.QueueEmpty())
    12     {
    13         Q.DeQueue(p);
    14         if(!p) 
    15             flag=1;
    16         else 
    17             if(flag) 
    18             {
    19                 cout<<"该树不是完全二叉树!
    ";
    20                 return false;
    21             }
    22             else
    23             {
    24                 Q.EnQueue(p->lchild);
    25                 Q.EnQueue(p->rchild); 
    26             }
    27      }
    28     cout<<"该树是完全二叉树!
    ";
    29     return true;
    30 }
    View Code

       4、交换左右子树

     1 /////////////////////////////////////////////////////////////////////////////
     2 //递归交换二叉树的左右子树,若成功则返回true
     3 //////////////////////////////////////////////////////////////////////////////
     4 bool Revolute_BT( BiTree &T )
     5 {
     6     
     7     BiTree temp;
     8     //交换
     9     temp = T->lchild;
    10     T->lchild = T->rchild;
    11     T->rchild = temp;
    12 
    13     if(T->lchild) 
    14         Revolute_BT(T->lchild);
    15     if(T->rchild) 
    16         Revolute_BT(T->rchild);
    17 
    18     return true;
    19 }
    View Code

         5、求二叉树叶子节点数

     1 /////////////////////////////////////////////////////////////////////////////
     2 //递归求二叉树的叶子节点的个数,参数为二叉树的头结点指针和个数count的引用,若成功返回true,
     3 //若为空树,返回false
     4 ///////////////////////////////////////////////////////////////////////////
     5 bool CountLeaf( BiTree T ,int &count)
     6 {
     7     if (!T) return false;
     8 
     9     if ( (!T->lchild)&&(!T->rchild) )//既无左孩子,也无右孩子
    10         count++;
    11     CountLeaf( T->lchild,count );
    12     CountLeaf( T->rchild,count );
    13 
    14     return true;
    15 }
    View Code

        6、求二叉树的繁茂度(高度*单层最多节点数)

     1 ///////////////////////////////////////////////
     2 //求二叉树的繁茂度(高度*单层最多节点数),参数为二叉树的头节点,返回繁茂度值
     3 //////////////////////////////////////////////
     4 int GetFanMao(BiTree T)
     5 {
     6   int count[100];  //用来存储每一层的节点数,0号单元未用
     7   memset(count,0,sizeof(count));
     8   int h; //树的高度
     9   int maxn; //单层最大节点数
    10   int i;
    11   int fm;//繁茂度
    12   BiTree p;
    13   if (!T) return 0;
    14   LinkQueue Q; 
    15   Q.EnQueue(T);
    16   while(!Q.QueueEmpty())
    17   {
    18     Q.DeQueue(p);
    19     count[p->layer]++;
    20 
    21     if(p->lchild) 
    22     {
    23         Q.EnQueue(p->lchild);
    24         p->lchild->layer = p->layer+1;  //可以求得该节点的层数
    25     }
    26     if(p->rchild) 
    27     {    
    28         Q.EnQueue(p->rchild);
    29         p->rchild->layer = p->layer+1;
    30     }
    31   } 
    32   h=p->layer;//高度
    33 
    34   for(maxn=count[1],i=1;count[i];i++)//求层最大结点数
    35     if(count[i]>maxn) 
    36         maxn=count[i]; 
    37 
    38   fm = h*maxn; //计算繁茂度
    39   return fm;
    40 }
    View Code

        7、求二叉树的高度

     1 ///////////////////////////////////////////////////////////////
     2 //求二叉树的高度,参数为二叉树的头节点,返回高度值
     3 ///////////////////////////////////////////////////////////////
     4 int GetHeight( BiTree T )
     5 {
     6     int lheight,rheight,max,h;
     7 
     8     if ( !T )     return 0;
     9     else
    10     {
    11         lheight = GetHeight( T->lchild );
    12         rheight = GetHeight( T->rchild );
    13         max = lheight > rheight ? lheight : rheight;
    14         h = max+1;
    15         return h;
    16     }
    17 }
    View Code

        8、 求完全二叉树的节点数(递归版本和非递归版本)  

         http://www.cnblogs.com/codershell/p/3306676.html

         9、递归释放一颗二叉树 

    1 void Remove(BiTNode* u)
    2 {
    3       if(u==NULL) return;
    4       Remove(u->left);
    5       Remove(u-right);
    6       free(u);        
    7 }
    View Code 

        10、二叉树的重建

            11、二叉树的旋转

      12、二叉树中两结点的最低公共祖先  

    后续将会补充更多与二叉树相关的操作,如果文中有任何错误或表述不清楚的,欢迎大家及时指出。

  • 相关阅读:
    P3834 【模板】可持久化线段树 (静态主席树)
    P3834 【模板】可持久化线段树 (静态主席树)
    2019 南昌网络赛 I. Max answer
    Linux从入门到精通——系统的进程
    Linux从入门到精通——文件权限
    Linux从入门到精通——命令行使用技巧
    Linux从入门到精通——Linux系统的文件及其管理
    Linux从入门到精通——vim及管理输入输出
    Linux从入门到精通——用户管理
    thiny mission 2021 10 15
  • 原文地址:https://www.cnblogs.com/codershell/p/3291601.html
Copyright © 2011-2022 走看看