AIRobot

AIRobot quick note


  • 首页

  • 关于

  • 标签

  • 分类

  • 归档

  • 搜索

红黑树详解

发表于 2019-04-02 更新于 2019-04-13 分类于 algorithm
本文字数: 15k 阅读时长 ≈ 13 分钟

红黑树详解

红黑树是一种相当复杂的数据结构,我仔细研究并亲手实现了它,这是一个多月来阅读「算法导论」给我带来成就感最大的一次。

概述

红黑树有以下五条性质:

  1. 节点是红色或者黑色的。
  2. 根节点的黑色的。
  3. nil 节点时黑色的。
  4. 每个红节点的左子节点和右子节点必定是黑色的。
  5. nil 节点在任意位置黑深度都相等。

nil 节点就是空节点,在红黑树的实现中,nil 节点代替二叉树中的 NULL:叶子节点的左节点和右节点指针都指向 nil 节点;只一个子树的节点,其另外一个子节点指针也指向 nil 节点;根节点的父节点也指向 nil 节点。nil节点的父节点和右节点都是自己,左节点为红黑树的根节点。如果红黑树为空(没有根节点),那么nil节点的左节点就也是自己。nil节点的存在大大方便了很多操作。

红黑树

红黑树的这5条性质中,第4和第5条性质保证了红黑树是相对“平衡”的。

  • 第4条保证了:两个红节点不可能直接相连。
  • 第5条保证了:从 nil 节点开始,从上至下沿任意一条道路,再回到 nil 节点,经过的黑节点的数量是一样的。

这样,红黑树中的所有叶子节点到根节点的长度,在最坏情况下也不会大于最好情况下的两倍,所以红黑树就能保持「大致」的平衡。

将黑节点视为框架(骨骼),红节点视为填充物(血肉)。黑节点控制着红黑树的平衡,一棵黑高度是n的红黑树,根节点的左右子树的黑高度就都很整齐地是 n 而不会不一样,这样红黑树就不会失去平衡;红节点使红黑树能够容纳有限程度的不平衡,固定黑高度的子树,能够容纳的节点个数是有限的。

如果一棵子树中已经塞满了红节点,再试图向其中插入节点,就会导致树失去平衡,这是就要进行一些额外的处理(包括旋转根节点,将一棵子树中的节点移到另一棵子树中去)。红黑树的复杂就在于此:如何在插入节点(还有)删除节点后,保证树仍然具有上述五条性质,这其中包含一些精妙的设计。

二叉查找树部分

红黑树在二叉查找树的基础上修改而来。首先看一下节点类 node 的定义:

class node
{
public:
    node (int val): value(val), isRed(true) {};
    node (int val, node *le, node *ri, node *pa): value(val), left(le), 
        right(ri), parent(pa), isRed(true) {};
    int value;
    node *left;
    node *right;
    node *parent;
    bool isRed;
};

节点有四个属性,value 表示节点值,isRed 表示红色还是黑色,left,right,parent指向左子结点、右子结点和父节点,构造函数用来构造节点,节点构造出来默认是红色的。

二叉查找树的特点是,根节点左子树中的节点全部小于根节点,右子树中的节点全部大于根节点。二叉查找树的插入和删除操作都有所体现。红黑树 rbtree 类的定义如下,先看 public 部分

class rbtree
{
public:
    rbtree();
    void insertValue(int val);
    node *searchValue(int val);
    void removeValue(int val);
    void removeNode(node *n);
    void printTree();
private:
    node *nil;
    // 省略
};

rbtree 类提供这样一些函数,插入一个值 insertValue ,查询一个值 searchValue ,删除一个节点 removeNode 或删除一个值 removeValue (即先查询,然后再删除查询到的第一个节点),在控制台输出树 printTree。

由于操作红黑树的函数经常用到递归,需要传入当前节点(从 nil 节点开始),所以上述函数都是通过调用 private 函数(调用这些函数是都要传入一个“当前节点”,以方便函数在当前节点的子结点上继续调用自己)来进行完成任务的:

rbtree::rbtree()
{
    // 构造空的红黑树,生成nil节点,其left,right,parent都指向自己
    nil = new node(0);
    nil->isRed = false;
    nil->left = nil->right = nil->parent = nil;
}

构造函数构造一个空的红黑树,只有 nil 节点,nil 节点的左子结点还是 nil 节点自己,如果非空,则是树的根节点。

void rbtree::insertValue(int val)
{
    // 从nil节点开始,插入一个值
    insert(nil, val);
}

insertValue 插入一个数值,它调用 insert 函数并将 nil 传递作为当前节点传入。任何插入操作都会从nil节点开始,沿着红黑树向下搜寻合适的位置。如果是空树,那就直接插在nil->left(根节点)上。

node *rbtree::searchValue(int val)
{
    node *res = search(nil->left, val);
    return (res == nil) ? NULL : res;
}

searchValue 在红黑树中查询一个值,如果查到了,则返回查到的节点,如果没有查到,返回NULL(而不是nil,因为该函数是对外的接口,外面不知道红黑树中有一个 nil 节点这回事)。

void rbtree::removeNode(node *n)
{
    remove(n);
}

删除节点很简单,就是调用私有的 remove 方法。删除节点不需要从 nil 节点开始遍历,直接从待删除的节点开始操作就可以了,所以 remove 方法并不接受一个 「当前节点」参数。

void rbtree::removeValue(int val)
{
    node *tn = search(nil->left, val);
    if (tn == nil)
    {
        return;
    }
    else
    {
        removeNode(tn);
    }
}

删除一个值,就是先根据值查找节点,如果查到了,就删除该节点。

void rbtree::printTree()
{
    // 将红黑树打印出来:如果为空,则显示一条消息;
    // 如果不为空,则打印出来
    if (nil->left == nil)
    {
        cout << "Empty Tree";
    }
    else
    {
        output(nil->left, 1, 0);
    }
    cout << endl;
}

输出红黑树也很简单,先检查是不是空树,如果是,输出一条消息,如果不是,调用私有函数 output() 并传入根节点。

现在看一下上面用到的几个私有函数。先看声明:

class rbtree
{
public:
    // 省略
private:
    node *nil;
    void insert(node *n, int val);
    void remove(node *n);
    node *search(node *n, int val);
    void output(node *n, int level, int blevel);
    // 省略
};

几个方法的实现如下所示:

void rbtree::insert(node *n, int val)
{
    // 在以节点n为根的子树中插入值val,从根部开始查找,
    if (n == nil)   /*从nil开始,说明是一次插入操作的第一次调用*/
    {
        if (nil->left != nil)   /*非空树*/
        {
            insert(nil->left, val);
        }
        else    /*空树*/
        {
            node *tn = new node(val, nil, nil, nil);
            nil->left = tn;
            fixInsert(tn);
        }
    }
    else    /*n为普通节点(而不是nil)*/
    {
        node **pos = (val < n->value) ? &(n->left) : &(n->right);
        if (*pos != nil) /*待插入位置非空,需要在子树中寻找更具体的位置*/
        {
            insert(*pos, val);
        }
        else /*待插入的位置为空,将新节点插入到该位置上*/
        {
            node *tn = new node(val, nil, nil, n);
            *pos = tn;
            fixInsert(tn);
        }
    }
}

插入操作,首先检查传入的当前节点n是不是nil节点:

  1. 如果是,说明是插入操作的第一次调用,传入了nil节点。检查红黑树是不是空树:
    1. 如果是空树,就将其作为根节点,插入在nil->left处。
    2. 如果不是,就在根节点上调用自己。
  2. 如果不是,就说明已经下放到普通节点了,将待插入的值与自己比较,并选择合适的位置(左子树或右子树)。检查这个合适的位置是不是空:
    1. 如果不是空,就在这个子节点上继续调用自己。
    2. 如果是空,就使用这个值构造一个新节点,插入在这个位置上。

你可能注意到,在真正插入一个节点(而不是继续调用自己)后,我们都执行了一个fixInsert(),这是这篇博文的重点之一,稍后再讲。

node *rbtree::search(node *n, int val)
{
    if (n == nil)
    {
        return nil;
    }
    else if (n->value == val)
    {
        return n;
    }
    else
    {
        return (val < n->value) ? search(n->left, val) : search(n->right, val);
    }
}

查询操作相对简单,就是比较查询值和当前节点值是否相等,如果相等,就返回当前节点,否则,就根据查询值与当前值的大小关系,在左子树或右子树中继续查询。

void rbtree::remove(node *n)
{
    // 如果有两个非空子树,就将待删除节点与左子树中最大节点交换值,
    // 然后删除左子树中的那个“最大节点”
    if (n->left != nil && n->right != nil)
    {
        node *target = n->right;
        while (target->left != nil)
        {
            target = target->left;
        }
        switchValue(n, target);
        remove(target);
    }
    else /*至少有一个子树为空*/
    {
        node *c = (n->left == nil) ? n->right : n->left;
        (*getSelfFromParent(n)) = c;
        if (c != nil)
        {
            c->parent = n->parent;
        }
        // 如果删掉的是红节点,什么都不做
        if (n->isRed == true)
        {
            return;
        }
        // 如果删除的节点是黑色,而删除节点的子节点是红色,那就将子节点染黑
        if (c->isRed == true)
        {
            c->isRed = false;
        }
        else
        {
            // 如果删除节点是黑色,删除节点的子节点也是黑色,麻烦来了
            fixRemove(c, c->parent);
        }
    }
}

删除操作,稍微复杂一些。二叉查找树中删除一个节点是这样的。

  1. 如果待删除的节点没有子节点或者只有一个子节点,那就直接将其删除,将子节点(如果有的话)代替自己接在父节点上。
  2. 如果待删除的节点有两个子节点(就是有两个非空子树),那就将该节点和左子树中的最大节点交换节点值,然后将左子树中的最大节点删除(该节点既然是左子树中的最大节点,就不可能有右子结点,所以不会有两个非空子树)。而且,左子树中的最大节点,一定比左子树中的其余所有节点都大(因为它是最大节点),又比右子树中的所有节点都小(它在左子树中),所以让该节点坐在原来待删除节点的位置上,不会破坏二叉查找树的属性。

你可能注意到,在真正删除一个节点后,在某些特定条件下,我们需要进行一些额外的处理,甚至调用 fixRemove() 函数。这是本篇博文的另一个重点,将在后面详细解释。

void rbtree::output(node *n, int level, int blevel)
{
    if (n == nil)
    {
        return;
    }
    int tb = (n->isRed) ? 0 : 1;
    output(n->left, level + 1, blevel + tb);
    for (int i = 1; i < level; i++)
    {
        cout << "   ";
    }
    cout << n->value << ((n->isRed) ? "R" : "");
    if (n->left == nil || n->right == nil)
    {
        cout << "(" << blevel + tb << ")";
    }
    cout << endl;
    output(n->right, level + 1, blevel + tb);
}

输出一个子树,传入的是当前节点,其深度与黑深度。printTree()函数调用该函数。该函数负责控制缩进,体现节点的深度;负责用字符R标记红节点,还负责输出叶子节点的黑深度。如下就是一个简单地五个元素的红黑树的输出。

>> g++ rbtree.cpp
>> ./a.out
   5(2)
      8R(2)
10
      15R(2)
   17(2)
>>

红节点打上了字符R标记,而且所有具有指向nil节点指针的节点的黑深度也输出在括号中(本例中为2)。

旋转

为了进行 fixInsert() 和 fixRemove() ,我们定义旋转操作还有一些其他的辅助函数。先看类中的函数声明:

class rbtree
{
public:
    // 省略
private:
    // 省略
    bool rotateLeft(node *n);
    bool rotateRight(node *n);
    node **getSelfFromParent(node *n);
    node **getSiblingFromParent(node *n);
    void switchColor(node *n1, node *n2);
    void switchValue(node *n1, node *n2);
    bool isLeft(node *n);
    // 省略
};

它们的实现如下所示:

bool rbtree::rotateLeft(node *n)
{
    if (n->right == nil)
    {
        return false;
    }
    else
    {
        node *x = n;
        node *y = n->right;
        node *p = y->left;
        (*getSelfFromParent(x)) = y;
        y->parent = x->parent;
        y->left = x;
        x->parent = y;
        x->right = p;
        if (p != nil)
        {
            p->parent = x;
        }
        return true;
    }
}

rotateLeft 左旋节点n。左旋的示意图如下所示:节点X左旋之后,由X的右子节点Y代替X成为X的父亲的子节点,X成为Y的左子结点,原先Y的左子结点成为了X的右子结点。旋转后,Y占据了X的位置,原先该位置上的左子树和右子树中的节点个数得到了调整,左子树中新加入了X节点,右子树中去除了Y节点,而β子树也离开了右子树,转到左子树中。实际上,红黑树就是通过这种调整来保持本身的大致平衡的。

旋转

bool rbtree::rotateRight(node *n)
{
    if (n->left == nil)
    {
        return false;
    }
    else
    {
        node *y = n;
        node *x = y->left;
        node *p = x->right;
        (*getSelfFromParent(y)) = x;
        x->parent = y->parent;
        x->right = y;
        y->parent = x;
        y->left = p;
        if (p != nil)
        {
            p->parent = y;
        }
        return true;
    }
}

右旋与左旋对称。

还有一些其他的辅助函数,可以使后面修复插入和删除的逻辑代码更加易读:

node **rbtree::getSelfFromParent(node *n)
{
    node *p = n->parent;
    return p->left == n ? (&(p->left)) : (&(p->right));
}
node **rbtree::getSiblingFromParent(node *n)
{
    node *p = n->parent;
    return p->left == n ? (&(p->right)) : (&(p->left));
}

getSelfFromParent 和 getSiblingFromParent 函数分别获取一个指针,该指针指向父节点中指向自己的指针和指向兄弟的指针。函数返回的是指针的指针,而不是直接指向自己或兄弟的指针,是因为这样可以用来改变指向的指针所指的内容。比如,*getSelfFromParent(n1)=n2 就可以将节点n1的父亲节点中,指向n1的指针(left 或 right)改成指向n2。

bool rbtree::isLeft(node *n)
{
    // 判断节点是否为父节点的左子结点
    return n->parent->left == n;
}
void rbtree::switchColor(node *n1, node *n2)
{
    bool tmp = n1->isRed;
    n1->isRed = n2->isRed;
    n2->isRed = tmp;
}
void rbtree::switchValue(node *n1, node *n2)
{
    int tmp = n1->value;
    n1->value = n2->value;
    n2->value = tmp;
}

这三个函数都比较简单实用,isLeft() 函数用来判断某个节点它是父亲的左子结点还是右子结点,switchColor() 函数用来交换两个节点的颜色,switchValue 用来交换两个节点的值。

修复插入

交代了这么多,总算进入重点了。

那么,为什么要修复插入呢?之前说到,在插入一个节点之后,我们执行 fixInsert() 。实际上,新插入了节点,该节点可能会破坏红黑树的性质1或5。

我们知道,新插入的节点n是红色的:

  1. 如果n的父节点p是黑色的
    1. 如果p是nil(也就是说,n是根节点),那么将n染成黑色,插入完成了(case1)。
    2. 如果p不是nil,而那么什么也不用做,插入就完成了(case2)。——如果它的父亲在插入前是叶子节点,那么插入之后n成为叶子节点,n本身是红色的,所以它的黑深度和p一样。如果在插入之前,p就有一个子节点s,那么s一定是红色的叶子节点(如果s是黑色的,而p的另一个子树为空,就违反了性质5),n插入之后,s就是n的兄弟节点。s和n都会具有与p一致的黑深度,也不会破坏性质。
  2. 如果n的父节点p是红色的,再插入一个红色子节点就违反了性质4,所以需要进行处理:
    1. 如果n的叔叔u是红色的,那么将u和p染成黑色,将n的爷爷节点g染成红色,然后执行fixInsert(g)(case3)。——如图所示,叔叔u是红色的,爷爷g一定是黑色的。这是p和n是连续的红节点,违反了性质4。处理之后,g变成了红色,又有可能违反性质4(g的父节点又是红色的)或者性质1(g就是根节点了)。这是再次对g调用fixInsert()。
    2. 如果n的叔叔u是黑色:
      1. 如果n为左子节点且p为左子节点(如图) / n为右子节点且p为右子结点(与图对称),那么右旋/左旋g并交换p和g的颜色(case3)。
      2. 如果n为右子结点且p为左子结点(如图) / n为左子结点且p为右子结点(与图对称),那么左旋/右旋p转化为上述情形(case4)。

修复插入case1:

修复插入Case2

修复插入case2:

修复插入Case3

修复插入case3:

修复插入Case4

代码如下所示:

void rbtree::fixInsert(node *n)
{
    if (n->parent == nil)
    {
        fixInsertCase1(n);
        return;
    }
    // 父节点P为黑色,则什么都不做
    if (n->parent->isRed == false)
    {
        fixInsertCase2(n);
    }
    else // 父节点P为红色
    {
        node *p = n->parent;
        node *u = *getSiblingFromParent(p);

        if (u->isRed == true)   /*叔叔节点U为红色*/
        {
            fixInsertCase3(n);
        }
        else    /*叔叔节点U为黑色*/
        {
            fixInsertCase4(n);
        }
    }
}
void rbtree::fixInsertCase1(node *n)
{
    n->isRed = false;
}
void rbtree::fixInsertCase2(node *n)
{
    return;
}
void rbtree::fixInsertCase3(node *n)
{
    node *p = n->parent;
    node *u = *getSiblingFromParent(p);
    node *g = p->parent;
    p->isRed = u->isRed = false;
    g->isRed = true;
    fixInsert(g);
}
void rbtree::fixInsertCase4(node *n)
{
    node *p = n->parent;
    node *u = *getSiblingFromParent(p);
    node *g = p->parent;
    if (isLeft(n) && isLeft(p))
    {
        rotateRight(g);
        switchColor(p, g);
    }
    else if (!isLeft(n) && !isLeft(p))
    {
        rotateLeft(g);
        switchColor(p, g);
    }
    else if (!isLeft(n) && isLeft(p))
    {
        rotateLeft(p);
        rotateRight(g);
        switchColor(n, g);
    }
    else // isLeft(n) && !isLeft(p)
    {
        rotateRight(p);
        rotateLeft(g);
        switchColor(n, g);
    }
}

修复删除

按照二叉查找树的删除方法,删除节点的操作都能转化为删除至多具有一个子节点的节点。如果被删除节点是红色的,那肯定不会对红黑树的性质造成影响,所以,完全不用在意。我们在意的是被删除节点是黑色的情况:如果被删除节点的子节点是红色的,那也好办,将子节点直接染黑就行,因为子节点代替了被删除节点的位置,它原来是红色的,对黑深度就没有贡献,染黑它,就可以起到被删除节点的作用。

最难办的是,如果被删除节点是黑色的,而且他的子节点还是黑色的(其实这时子节点只能是nil节点了),这时这条路径上就少了一个黑节点,此时就需要调用 fixRemove() 来修复。实际上,调用该函数的时机是,某一个节点由于某种变化(这里就是删除了一个黑节点且接上来的子节点也是黑色),导致通过该节点的路径少了一个黑节点。

fixRemove() 的逻辑是这样的(实际上,第一次删除节点后,节点的子节点n都是 nil ,只有开始递归调用自己之后,n才是正常的节点):

  1. 如果被删除节点的子节点n就是根节点(case1),那么结束。
  2. 如果n不是根节点:
    1. n的兄弟节点s是红节点(case2),那么N的父节点p和s互换颜色,并左旋(如图)/右旋(与图对称)p,转入case4,case5或case6。
    2. n的兄弟节点s为黑节点,s的左右节点都是黑节点(case3),那就将s染成红色,再对p执行 fixRemove() 。将s染成红色导致所有通过s的路径少了一个黑节点,这样就与通过n的路径一致了。现在通过p节点的所有路径都少了一个黑节点,所以要继续对p做 fixRemove()。
    3. s与s的两个子节点都是黑色,但是p是红色(case4)。这时只需要交换p和s的颜色即可。原先通过p的路径上的黑节点数目没有变,但是通过n的节点数目补回来了。
    4. n是左节点(否则对称)的情况下,s是黑,s的左子节点是红,右子结点是黑(case5)。这时右旋s,这样n就有了一个右子节点为红色的兄弟,转入case6。
    5. n是左节点(否则对称)的情况下,s是黑,s的右子节点是红(case6)。这时左旋p,然后交换s和p的颜色,并且将是s的右节点染黑。这样,不管p原先是红色还是黑色,通过n的路径都增加了一个黑节点,而通过sr的路径保持不变。

修复删除case2:

修复删除Case2

修复删除case3:

修复删除Case3

修复删除case4:

修复删除Case4

修复删除case5:

修复删除Case5

修复删除case6:

修复删除Case6

代码如下所示:

void rbtree::fixRemove(node *n, node *p)
{
    if (p == nil)
    {
        fixRemoveCase1(n, p);
        return;
    }
    node *s = *getSiblingFromParent(n);
    if (s->isRed == true)
    {
        fixRemoveCase2(n, p);
        return;
    }
    // s为黑色
    if (p->isRed == false && s->left->isRed == false && s->right->isRed == false)
    {
        fixRemoveCase3(n, p);
        return;
    }
    if (p->isRed == true && s->left->isRed == false && s->right->isRed == false)
    {
        fixRemoveCase4(n, p);
        return;
    }
    if ((isLeft(n) && s->left->isRed == true && s->right->isRed == false) ||
            (!isLeft(n)  && s->right->isRed == true && s->left->isRed == false))
    {
        fixRemoveCase5(n, p);
        return;
    }
    if ((isLeft(n) &&  s->right->isRed == true) ||
            (!isLeft(n) && s->left->isRed == true))
    {
        fixRemoveCase6(n, p);
    }
}
void rbtree::fixRemoveCase1(node *n, node *p)
{
    return;
}
void rbtree::fixRemoveCase2(node *n, node *p)
{
    node *s = *getSiblingFromParent(n);
    if (isLeft(n))
    {
        rotateLeft(p);
    }
    else
    {
        rotateRight(p);
    }
    switchColor(p, s);
    fixRemove(n, p);
}
void rbtree::fixRemoveCase3(node *n, node *p)
{
    node *s = *getSiblingFromParent(n);
    s->isRed = true;
    fixRemove(p, p->parent);
}
void rbtree::fixRemoveCase4(node *n, node *p)
{
    node *s = *getSiblingFromParent(n);
    switchColor(s, p);
}
void rbtree::fixRemoveCase5(node *n, node *p)
{
    node *s = *getSiblingFromParent(n);
    if (isLeft(n))
    {
        rotateRight(s);
    }
    else
    {
        rotateLeft(s);
    }
    fixRemove(n, p);

}
void rbtree::fixRemoveCase6(node *n, node *p)
{
    node *s = *getSiblingFromParent(n);
    if (isLeft(n))
    {
        s->right->isRed = false;
        rotateLeft(p);
    }
    else
    {
        s->left->isRed = false;
        rotateRight(p);
    }
    switchColor(p, s);
}

这样就实现了红黑树。

原文链接

原文

[repost]教你透彻了解红黑树红黑树

发表于 2019-04-02 分类于 algorithm
本文字数: 13k 阅读时长 ≈ 12 分钟

教你透彻了解红黑树

二叉查找树

由于红黑树本质上就是一棵二叉查找树,所以在了解红黑树之前,咱们先来看下二叉查找树。

二叉查找树(Binary Search Tree),也称有序二叉树(ordered binary tree),排序二叉树(sorted binary tree),是指一棵空树或者具有下列性质的二叉树:

  • 若任意结点的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
  • 若任意结点的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
  • 任意结点的左、右子树也分别为二叉查找树。
  • 没有键值相等的结点(no duplicate nodes)。

因为,一棵由n个结点,随机构造的二叉查找树的高度为lgn,所以顺理成章,一般操作的执行时间为O(lgn).(至于n个结点的二叉树高度为lgn的证明,可参考算法导论 第12章 二叉查找树 第12.4节)。

但二叉树若退化成了一棵具有n个结点的线性链后,则此些操作最坏情况运行时间为O(n)。后面我们会看到一种基于二叉查找树-红黑树,它通过一些性质使得树相对平衡,使得最终查找、插入、删除的时间复杂度最坏情况下依然为O(lgn)。

红黑树

前面我们已经说过,红黑树,本质上来说就是一棵二叉查找树,但它在二叉查找树的基础上增加了着色和相关的性质使得红黑树相对平衡,从而保证了红黑树的查找、插入、删除的时间复杂度最坏为O(log n)。

但它是如何保证一棵n个结点的红黑树的高度始终保持在h = logn的呢?这就引出了红黑树的5条性质:

1)每个结点要么是红的,要么是黑的。  
2)根结点是黑的。  
3)每个叶结点(叶结点即指树尾端NIL指针或NULL结点)是黑的。  
4)如果一个结点是红的,那么它的俩个儿子都是黑的。  
5)对于任一结点而言,其到叶结点树尾端NIL指针的每一条路径都包含相同数目的黑结点。  

正是红黑树的这5条性质,使得一棵n个结点是红黑树始终保持了logn的高度,从而也就解释了上面我们所说的“红黑树的查找、插入、删除的时间复杂度最坏为O(log n)”这一结论的原因。

如下图所示,即是一颗红黑树(下图引自wikipedia:http://t.cn/hgvH1l):

upload successful

上文中我们所说的 “叶结点” 或”NULL结点”,它不包含数据而只充当树在此结束的指示,这些结点以及它们的父结点,在绘图中都会经常被省略。

树的旋转知识

当我们在对红黑树进行插入和删除等操作时,对树做了修改,那么可能会违背红黑树的性质。

为了继续保持红黑树的性质,我们可以通过对结点进行重新着色,以及对树进行相关的旋转操作,即修改树中某些结点的颜色及指针结构,来达到对红黑树进行插入或删除结点等操作后,继续保持它的性质或平衡。

树的旋转,分为左旋和右旋,以下借助图来做形象的解释和介绍:

1.左旋

upload successful

如上图所示:

当在某个结点pivot上,做左旋操作时,我们假设它的右孩子y不是NIL[T],pivot可以为任何不是NIL[T]的左孩子结点。

左旋以pivot到y之间的链为“支轴”进行,它使y成为该孩子树新的根,而y的左孩子b则成为pivot的右孩子。

左旋操作的参考代码如下所示(以x代替上述的pivot):

1
2
3
4
5
6
7
8
9
10
11
12
 LEFT-ROTATE(T, x)  
1 y ← right[x] ▹ Set y.
2 right[x] ← left[y] ▹ Turn y's left subtree into x's right subtree.
3 p[left[y]] ← x
4 p[y] ← p[x] ▹ Link x's parent to y.
5 if p[x] = nil[T]
6 then root[T] ← y
7 else if x = left[p[x]]
8 then left[p[x]] ← y
9 else right[p[x]] ← y
10 left[y] ← x ▹ Put x on y's left.
11 p[x] ← y

2.右旋

右旋与左旋差不多,再此不做详细介绍。

upload successful

对于树的旋转,能保持不变的只有原树的搜索性质,而原树的红黑性质则不能保持,在红黑树的数据插入和删除后可利用旋转和颜色重涂来恢复树的红黑性质。

红黑树的插入

要真正理解红黑树的插入和删除,还得先理解二叉查找树的插入和删除。磨刀不误砍柴工,咱们再来分别了解下二叉查找树的插入和删除。

二叉查找树的插入

如果要在二叉查找树中插入一个结点,首先要查找到结点插入的位置,然后进行插入,假设插入的结点为z的话,插入的伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
TREE-INSERT(T, z)
1 y ← NIL
2 x ← root[T]
3 while x ≠ NIL
4 do y ← x
5 if key[z] < key[x]
6 then x ← left[x]
7 else x ← right[x]
8 p[z] ← y
9 if y = NIL
10 then root[T] ← z ⊹ Tree T was empty
11 else if key[z] < key[y]
12 then left[y] ← z
13 else right[y] ← z

可以看到,上述第3-7行代码即是在二叉查找树中查找z待插入的位置,如果插入结点z小于当前遍历到的结点,则到当前结点的左子树中继续查找,如果z大于当前结点,则到当前结点的右子树中继续查找,第9-13行代码找到待插入的位置,如果z依然比此刻遍历到的新的当前结点小,则z作为当前结点的左孩子,否则作为当前结点的右孩子。

红黑树的插入和插入修复

现在我们了解了二叉查找树的插入,接下来,咱们便来具体了解红黑树的插入操作。红黑树的插入相当于在二叉查找树插入的基础上,为了重新恢复平衡,继续做了插入修复操作。

假设插入的结点为z,红黑树的插入伪代码具体如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
RB-INSERT(T, z)  
1 y ← nil[T]
2 x ← root[T]
3 while x ≠ nil[T]
4 do y ← x
5 if key[z] < key[x]
6 then x ← left[x]
7 else x ← right[x]
8 p[z] ← y
9 if y = nil[T]
10 then root[T] ← z
11 else if key[z] < key[y]
12 then left[y] ← z
13 else right[y] ← z
14 left[z] ← nil[T]
15 right[z] ← nil[T]
16 color[z] ← RED
17 RB-INSERT-FIXUP(T, z)

我们把上面这段红黑树的插入代码,跟我们之前看到的二叉查找树的插入代码,可以看出,RB-INSERT(T, z)前面的第1-13行代码基本就是二叉查找树的插入代码,然后第14-16行代码把z的左孩子、右孩子都赋为叶结点nil,再把z结点着为红色,最后为保证红黑性质在插入操作后依然保持,调用一个辅助程序RB-INSERT-FIXUP来对结点进行重新着色,并旋转。

换言之

  • 如果插入的是根结点,因为原树是空树,此情况只会违反性质2,所以直接把此结点涂为黑色。
  • 如果插入的结点的父结点是黑色,由于此不会违反性质2和性质4,红黑树没有被破坏,所以此时也是什么也不做。

但当遇到下述3种情况时:

  • 插入修复情况1:如果当前结点的父结点是红色且祖父结点的另一个子结点(叔叔结点)是红色
  • 插入修复情况2:当前结点的父结点是红色,叔叔结点是黑色,当前结点是其父结点的右子
  • 插入修复情况3:当前结点的父结点是红色,叔叔结点是黑色,当前结点是其父结点的左子

又该如何调整呢?答案就是根据红黑树插入代码RB-INSERT(T, z)最后一行调用的RB-INSERT-FIXUP(T,z)所示操作进行,具体如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
RB-INSERT-FIXUP(T,z)
1 while color[p[z]] = RED
2 do if p[z] = left[p[p[z]]]
3 then y ← right[p[p[z]]]
4 if color[y] = RED
5 then color[p[z]] ← BLACK ▹ Case 1
6 color[y] ← BLACK ▹ Case 1
7 color[p[p[z]]] ← RED ▹ Case 1
8 z ← p[p[z]] ▹ Case 1
9 else if z = right[p[z]]
10 then z ← p[z] ▹ Case 2
11 LEFT-ROTATE(T, z) ▹ Case 2
12 color[p[z]] ← BLACK ▹ Case 3
13 color[p[p[z]]] ← RED ▹ Case 3
14 RIGHT-ROTATE(T, p[p[z]]) ▹ Case 3
15 else (same as then clause
with "right" and "left" exchanged)
16 color[root[T]] ← BLACK

下面,咱们来分别处理上述3种插入修复情况。

插入修复情况1:当前结点的父结点是红色且祖父结点的另一个子结点(叔叔结点)是红色。

即如下代码所示:

1
2
3
4
1 while color[p[z]] = RED  
2 do if p[z] = left[p[p[z]]]
3 then y ← right[p[p[z]]]
4 if color[y] = RED

此时父结点的父结点一定存在,否则插入前就已不是红黑树。
与此同时,又分为父结点是祖父结点的左子还是右子,对于对称性,我们只要解开一个方向就可以了。

在此,我们只考虑父结点为祖父左子的情况。
同时,还可以分为当前结点是其父结点的左子还是右子,但是处理方式是一样的。我们将此归为同一类。

对策:将当前结点的父结点和叔叔结点涂黑,祖父结点涂红,把当前结点指向祖父结点,从新的当前结点重新开始算法。即如下代码所示:

1
2
3
4
5                   then color[p[z]] ← BLACK                    ▹ Case 1  
6 color[y] ← BLACK ▹ Case 1
7 color[p[p[z]]] ← RED ▹ Case 1
8 z ← p[p[z]] ▹ Case 1

针对情况1,变化前(图片来源:saturnman)[当前结点为4结点]:

upload successful

变化后:

upload successful

插入修复情况2:当前结点的父结点是红色,叔叔结点是黑色,当前结点是其父结点的右子
对策:当前结点的父结点做为新的当前结点,以新当前结点为支点左旋。即如下代码所示:

1
2
3
 9                   else if z = right[p[z]]
10 then z ← p[z] ▹ Case 2
11 LEFT-ROTATE(T, z) ▹ Case 2

如下图所示,变化前[当前结点为7结点]:

upload successful

变化后:

upload successful

插入修复情况3:当前结点的父结点是红色,叔叔结点是黑色,当前结点是其父结点的左子
解法:父结点变为黑色,祖父结点变为红色,在祖父结点为支点右旋,操作代码为:

1
2
3
12                           color[p[z]] ← BLACK                 ▹ Case 3
13 color[p[p[z]]] ← RED ▹ Case 3
14 RIGHT-ROTATE(T, p[p[z]]) ▹ Case 3

最后,把根结点涂为黑色,整棵红黑树便重新恢复了平衡。

如下图所示[当前结点为2结点]

upload successful

变化后:

upload successful

红黑树的删除

ok,接下来,咱们最后来了解,红黑树的删除操作。

“我们删除的结点的方法与常规二叉搜索树中删除结点的方法是一样的,如果被删除的结点不是有双非空子女,则直接删除这个结点,用它的唯一子结点顶替它的位置,如果它的子结点都是空结点,那就用空结点顶替它的位置,如果它的双子全为非空,我们就把它的直接后继结点内容复制到它的位置,之后以同样的方式删除它的后继结点,它的后继结点不可能是双子非空,因此此传递过程最多只进行一次。”

二叉查找树的删除

继续讲解之前,补充说明下二叉树结点删除的几种情况,待删除的结点按照儿子的个数可以分为三种:

  1. 没有儿子,即为叶结点。直接把父结点的对应儿子指针设为NULL,删除儿子结点就OK了。
  2. 只有一个儿子。那么把父结点的相应儿子指针指向儿子的独生子,删除儿子结点也OK了。
  3. 有两个儿子。这是最麻烦的情况,因为你删除结点之后,还要保证满足搜索二叉树的结构。其实也比较容易,我们可以选择左儿子中的最大元素或者右儿子中的最小元素放到待删除结点的位置,就可以保证结构的不变。当然,你要记得调整子树,毕竟又出现了结点删除。习惯上大家选择左儿子中的最大元素,其实选择右儿子的最小元素也一样,没有任何差别,只是人们习惯从左向右。这里咱们也选择左儿子的最大元素,将它放到待删结点的位置。左儿子的最大元素其实很好找,只要顺着左儿子不断的去搜索右子树就可以了,直到找到一个没有右子树的结点。那就是最大的了。

二叉查找树的删除代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
TREE-DELETE(T, z)
1 if left[z] = NIL or right[z] = NIL
2 then y ← z
3 else y ← TREE-SUCCESSOR(z)
4 if left[y] ≠ NIL
5 then x ← left[y]
6 else x ← right[y]
7 if x ≠ NIL
8 then p[x] ← p[y]
9 if p[y] = NIL
10 then root[T] ← x
11 else if y = left[p[y]]
12 then left[p[y]] ← x
13 else right[p[y]] ← x
14 if y ≠ z
15 then key[z] ← key[y]
16 copy y's satellite data into z
17 return y

红黑树的删除和删除修复

OK,回到红黑树上来,红黑树结点删除的算法实现是:

RB-DELETE(T, z) 单纯删除结点的总操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 1 if left[z] = nil[T] or right[z] = nil[T]  
2 then y ← z
3 else y ← TREE-SUCCESSOR(z)
4 if left[y] ≠ nil[T]
5 then x ← left[y]
6 else x ← right[y]
7 p[x] ← p[y]
8 if p[y] = nil[T]
9 then root[T] ← x
10 else if y = left[p[y]]
11 then left[p[y]] ← x
12 else right[p[y]] ← x
13 if y ≠ z
14 then key[z] ← key[y]
15 copy y's satellite data into z
16 if color[y] = BLACK
17 then RB-DELETE-FIXUP(T, x)
18 return y

“在删除结点后,原红黑树的性质可能被改变,如果删除的是红色结点,那么原红黑树的性质依旧保持,此时不用做修正操作,如果删除的结点是黑色结点,原红黑树的性质可能会被改变,我们要对其做修正操作。那么哪些树的性质会发生变化呢,如果删除结点不是树唯一结点,那么删除结点的那一个支的到各叶结点的黑色结点数会发生变化,此时性质5被破坏。如果被删结点的唯一非空子结点是红色,而被删结点的父结点也是红色,那么性质4被破坏。如果被删结点是根结点,而它的唯一非空子结点是红色,则删除后新根结点将变成红色,违背性质2。”

RB-DELETE-FIXUP(T, x) 恢复与保持红黑性质的工作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
 1 while x ≠ root[T] and color[x] = BLACK  
2 do if x = left[p[x]]
3 then w ← right[p[x]]
4 if color[w] = RED
5 then color[w] ← BLACK ▹ Case 1
6 color[p[x]] ← RED ▹ Case 1
7 LEFT-ROTATE(T, p[x]) ▹ Case 1
8 w ← right[p[x]] ▹ Case 1
9 if color[left[w]] = BLACK and color[right[w]] = BLACK
10 then color[w] ← RED ▹ Case 2
11 x ← p[x] ▹ Case 2
12 else if color[right[w]] = BLACK
13 then color[left[w]] ← BLACK ▹ Case 3
14 color[w] ← RED ▹ Case 3
15 RIGHT-ROTATE(T, w) ▹ Case 3
16 w ← right[p[x]] ▹ Case 3
17 color[w] ← color[p[x]] ▹ Case 4
18 color[p[x]] ← BLACK ▹ Case 4
19 color[right[w]] ← BLACK ▹ Case 4
20 LEFT-ROTATE(T, p[x]) ▹ Case 4
21 x ← root[T] ▹ Case 4
22 else (same as then clause with "right" and "left" exchanged)
23 color[x] ← BLACK

“上面的修复情况看起来有些复杂,下面我们用一个分析技巧:我们从被删结点后来顶替它的那个结点开始调整,并认为它有额外的一重黑色。这里额外一重黑色是什么意思呢,我们不是把红黑树的结点加上除红与黑的另一种颜色,这里只是一种假设,我们认为我们当前指向它,因此空有额外一种黑色,可以认为它的黑色是从它的父结点被删除后继承给它的,它现在可以容纳两种颜色,如果它原来是红色,那么现在是红+黑,如果原来是黑色,那么它现在的颜色是黑+黑。有了这重额外的黑色,原红黑树性质5就能保持不变。现在只要恢复其它性质就可以了,做法还是尽量向根移动和穷举所有可能性。”–saturnman。

如果是以下情况,恢复比较简单:

  • a)当前结点是红+黑色
    解法,直接把当前结点染成黑色,结束此时红黑树性质全部恢复。
  • b)当前结点是黑+黑且是根结点,
    解法:什么都不做,结束。

但如果是以下情况呢?:

  • 删除修复情况1:当前结点是黑+黑且兄弟结点为红色(此时父结点和兄弟结点的子结点分为黑)
  • 删除修复情况2:当前结点是黑加黑且兄弟是黑色且兄弟结点的两个子结点全为黑色
  • 删除修复情况3:当前结点颜色是黑+黑,兄弟结点是黑色,兄弟的左子是红色,右子是黑色
  • 删除修复情况4:当前结点颜色是黑-黑色,它的兄弟结点是黑色,但是兄弟结点的右子是红色,兄弟结点左子的颜色任意

此时,我们需要调用RB-DELETE-FIXUP(T, x),来恢复与保持红黑性质的工作。

下面,咱们便来分别处理这4种删除修复情况。

删除修复情况1:当前结点是黑+黑且兄弟结点为红色(此时父结点和兄弟结点的子结点分为黑)。

解法:把父结点染成红色,把兄弟结点染成黑色,之后重新进入算法(我们只讨论当前结点是其父结点左孩子时的情况)。此变换后原红黑树性质5不变,而把问题转化为兄弟结点为黑色的情况(注:变化前,原本就未违反性质5,只是为了把问题转化为兄弟结点为黑色的情况)。 即如下代码操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//调用RB-DELETE-FIXUP(T, x) 的1-8行代码
1 while x ≠ root[T] and color[x] = BLACK
2 do if x = left[p[x]]
3 then w ← right[p[x]]
4 if color[w] = RED
5 then color[w] ← BLACK ▹ Case 1
6 color[p[x]] ← RED ▹ Case 1
7 LEFT-ROTATE(T, p[x]) ▹ Case 1
8 w ← right[p[x]] ▹ Case 1
```
变化前:

![upload successful](/uploads/images/pasted-10.png)

变化后:

![upload successful](/uploads/images/pasted-11.png)

**删除修复情况2:当前结点是黑加黑且兄弟是黑色且兄弟结点的两个子结点全为黑色。**

解法:把当前结点和兄弟结点中抽取一重黑色追加到父结点上,把父结点当成新的当前结点,重新进入算法。(此变换后性质5不变),即调用RB-INSERT-FIXUP(T, z) 的第9-10行代码操作,如下:

//调用RB-DELETE-FIXUP(T, x) 的9-11行代码
9 if color[left[w]] = BLACK and color[right[w]] = BLACK
10 then color[w] ← RED ▹ Case 2
11 x p[x] ▹ Case 2

1
2
3
4
5
6
7
8
9
10
11
变化前:  

![upload successful](/uploads/images/pasted-12.png)

变化后:

![upload successful](/uploads/images/pasted-13.png)

**删除修复情况3:当前结点颜色是黑+黑,兄弟结点是黑色,兄弟的左子是红色,右子是黑色。**

解法:把兄弟结点染红,兄弟左子结点染黑,之后再在兄弟结点为支点解右旋,之后重新进入算法。此是把当前的情况转化为情况4,而性质5得以保持,即调用RB-INSERT-FIXUP(T, z) 的第12-16行代码,如下所示:

//调用RB-DELETE-FIXUP(T, x) 的第12-16行代码
12 else if color[right[w]] = BLACK
13 then color[left[w]] ← BLACK ▹ Case 3
14 color[w] ← RED ▹ Case 3
15 RIGHT-ROTATE(T, w) ▹ Case 3
16 w ← right[p[x]] ▹ Case 3

1
2
3
4
5
6
7
8
9
10
11
变化前:  

![upload successful](/uploads/images/pasted-14.png)

变化后:

![upload successful](/uploads/images/pasted-15.png)

**删除修复情况4:当前结点颜色是黑-黑色,它的兄弟结点是黑色,但是兄弟结点的右子是红色,兄弟结点左子的颜色任意。**

解法:把兄弟结点染成当前结点父结点的颜色,把当前结点父结点染成黑色,兄弟结点右子染成黑色,之后以当前结点的父结点为支点进行左旋,此时算法结束,红黑树所有性质调整正确,即调用RB-INSERT-FIXUP(T, z)的第17-21行代码,如下所示:

//调用RB-DELETE-FIXUP(T, x) 的第17-21行代码
17 color[w] ← color[p[x]] ▹ Case 4
18 color[p[x]] ← BLACK ▹ Case 4
19 color[right[w]] ← BLACK ▹ Case 4
20 LEFT-ROTATE(T, p[x]) ▹ Case 4
21 x ← root[T] ▹ Case 4
```
变化前:

upload successful

变化后:

upload successful

本文参考

本文参考了算法导论、STL源码剖析、计算机程序设计艺术等资料,并推荐阅读这个PDF:Left-Leaning Red-Black Trees, Dagstuhl Workshop on Data Structures, Wadern, Germany, February, 2008.
下载地址:http://www.cs.princeton.edu/~rs/talks/LLRB/RedBlack.pdf。

const T、const T*、T *const、const T&、const T*& 的区别

发表于 2019-04-01 更新于 2019-04-02
本文字数: 6.4k 阅读时长 ≈ 6 分钟

这里的T指的是一种数据类型,可以是int、long、doule等基本数据类型,也可以是自己类型的类型class。单独的一个const你肯定知道指的是一个常量,但const与其他类型联合起来的众多变化,你是不是就糊涂了?下面我们一一来解析。

const T

定义一个常量,声明的同时必须进行初始化。一旦声明,这个值将不能被改变。

1
2
3
4
5
int i = 5;
const int constInt = 10; //正确
const int constInt2 = i; //正确
constInt = 20; //错误,常量值不可被改变
const int constInt3; //错误,未被初始化

const T*

指向常量的指针,不能用于改变其所指向的对象的值。

1
2
3
4
5
6
7
8
9
10
11
const int i = 5;
const int i2 = 10;
const int* pInt = &i; //正确,指向一个const int对象,即i的地址
//*pInt = 10; //错误,不能改变其所指缶的对象
pInt = &i2; //正确,可以改变pInt指针本身的值,此时pInt指向的是i2的地址
const int* p2 = new int(8); //正确,指向一个new出来的对象的地址
delete p2; //正确
//int* pInt = &i; //错误,i是const int类型,类型不匹配,不能将const int * 初始化为int *
int nValue = 15;
const int * pConstInt = &nValue; //正确,可以把int *赋给const int *,但是pConstInt不能改变其所指向对象的值,即nValue
*pConstInt = 40; //错误,不能改变其所指向对象的值

const int* 与int* const的区别

指针本身就是一种对象,把指针定义为常量就是常量指针,也就是int* const的类型,也可以写成int *const,声明时必须初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int nValue = 10;
int* const p = &nValue;
int *const p2 = &nValue;
const int* 指针指向的对象不可以改变,但指针本身的值可以改变;int* const 指针本身的值不可改变,但其指向的对象可以改变。
int nValue1 = 10;
int nValue2 = 20;
int* const constPoint = &nValue1;
//constPoint = & nValue2; //错误,不能改变constPoint本身的值
*constPoint = 40; //正确,可以改变constPoint所指向的对象,此时nValue1 = 40


const int nConstValue1 = 5;
const int nConstValue2 = 15;
const int* pPoint = &nConstValue1;
//*pPoint = 55; //错误,不能改变pPoint所指向对象的值
pPoint = &nConstValue2; //正确,可以改变pPoint指针本身的值,此时pPoint邦定的是nConstValue2对象,即pPoint为nConstValue2的地址

const int* const 是一个指向常量对象的常量指针,即不可以改变指针本身的值,也不可以改变指针指向的对象。

1
2
3
4
5
const int nConstValue1 = 5;
const int nConstValue2 = 15;
const int* const pPoint = &nConstValue1;
//*pPoint = 55; //错误,不能改变pPoint所指向对象的值
//pPoint = &nConstValue2; //错误,不能改变pPoint本身的值

const T&

对常量(const)的引用,又称为常量引用,常量引用不能修改其邦定的对象。

1
2
3
4
int i = 5;
const int constInt = 10;
const int& rConstInt = constInt; //正确,引用及邦定的值都是常量
rConstInt = 5; //错误,不能改变引用所指向的对象

允许为一个常量引用邦定一个非常量对象、字面值,甚至是表达式;引用的类型与引用所指向的类型必须一致。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int i = 5;
int& rInt = i; //正确,int的引用
const int constInt = 10;
const int& rConstInt = constInt; //正确,引用及邦定的值都是常量
const int& rConstInt2 = rInt; //正确,用rInt邦定的对象进行赋值
rInt = 30; //这时,rConstInt2、rInt、i的值都为30
//rConstInt2 = 30; //错误,rConstInt2是常量引用,rConstInt2本身不能改变所指向的对象


int i2 = 15;
const int& rConstInt3 = i2; //正确,用非常量的对象为其赋值
const int& rConstInt4 = i + i2; //正确,用表达式为其赋值,值为45
i = 20; //此时i=20, rInt = 20, rConstInt4 = 45,说明rConstInt4邦定的是i + i2的临时变量
const int& rConstInt5 = 50; //正解,用一个常量值为其赋值

const T*&与T *const&

指向常量对象的指针的引用,这可以分两步来理解:1.const T是指向常量的指针;2.const T&指向常量的指针的引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const int nConstValue = 1;                      //常量对象
const int nConstValue2 = 2; //常量对象
const int* pConstValue = &nConstValue; //指向常量对象的指针
const int* pConstValue2 = &nConstValue2; //指向常量对象的指针
const int*& rpConstValue = pConstValue; //指向常量对象的指针的引用
//*rpConstValue = 10; //错误,rpConstValue指向的是常量对象,常量对象的值不可改变
rpConstValue = pConstValue2; //正确,此时pConstValue的值等于pConstValue2
//指向常量对象的指针本身是对象,引用可以改变邦定对象的值

int nValue = 5;
int nValue2 = 10;
int *const constPoint = &nValue; //常量指针
int *const constPoint2 = &nValue2; //常量指针
int *const &rpConstPoint = constPoint; //对常量指针的引用,邦定constPoint
//rpConstPoint = constPoint2; //错误,constPoint是常量指针,指针本身的值不可改变
*rpConstPoint = 20; //正确,指针指向的对象可以改变

在函数中的应用

我们直接从需求出来,假设有这样一个数据结构:

1
2
3
4
5
6
7
typedef struct __Data
{
int value;
public:
__Data()
:value(0){}
}Data;

1.希望传入一个对象,又不想让函数体修改这个对象。

方式<1>

1
2
3
4
5
6
7
void Dealwith(const Data& data)
{
cout << data.value << endl;
//data.value = 5; //错误,data是常量引用,不能改变其邦定的对象
}

这种方式还有一个好处是只有在调用函数的时候会邦定对象,传递的是对象的引用,而不是对象,减少函数调用时对象赋值的花销。

方式<2>

1
2
3
4
void Dealwith(const Data* pData)
{
cout << pData->value << endl;
//pData->value = 5; //错误,pData是指向常量对象的指针,不能改变其指向的对象

这种方式与void Dealwith(const Data& data)的功能相同

方式<3>

1
2
3
4
5
6
Data g_data(20);
void Dealwith(const Data*& pData)
{
cout << pData->value << endl;
//pData->value = 5; //错误,pData邦定的是指向常量对象的指针,常量对象的指针不能改变其指向的对象
pData = &g_data; //正确,pData是[指向常量对象的指针]的引用,引用可改变其邦定的对象

调用如下:

1
2
3
4
Data d(10);
const Data* pData = &d;
Dealwith(pData);
cout << pData->value << endl; //此时pData->value为20,d.value还是10

这种方式函数未改变传入的对象的值,但可以返回另外一个对象的指针。注意返回的指针必须指向全局的对象,如果返回函数内定义的对象,退出函数作用域后,其指针将无效,这是非常危险的;如果Dealwith是成员函数,也可以返回指向成员的指针。

2.在类中的使用,返回一个类的成员,但不希望调用方修改这个成员。

方式<1>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class MyData
{
public :
MyData(std::string name, Data data)
{
m_name = name;
m_data = data;
}

const Data* GetData()
{
return &m_data;
}


private:
std::string m_name;
Data m_data;
};

调用如下:

1
2
3
4
MyData mydata("", Data(100));
const Data* pData = mydata.GetData();
cout << pData->value << endl; //pData->value = 100
//pData->value = 50; //错误,pData是指向常量对象的指向,不能改变其指向对象的值

方式<2>

有人可能会问GetData也可以写成这样:

1
2
3
4
const Data& GetData()
{
return m_data;
}

这样的话,调用方常常容易写成这样:

1
2
3
4
MyData mydata("", Data(100));
Data data = mydata.GetData(); //这会有个赋值的过程,会把mydata.m_data赋给data
cout << data.value << endl; //data.value = 100
data.value = 50; //正确,data.value=50,但mydata.m_data.value还是100

这样调用时会有一个结果赋值的过程,如果Data是一个复杂的类,会有较大的开销,其效果与下面这种方式是一样的:

1
2
3
4
Data GetData()
{
return m_data;
}

当然,如果调用方这样使用是正确的:

1
2
3
4
5
6
7
8
9
const Data& GetData()
{
return m_data;
}

MyData mydata("", Data(100));
const Data& data = mydata.GetData(); //这会有个赋值的过程,会把mydata.m_data赋给data
cout << data.value << endl; //data.value = 100
//data.value = 50; //错误,data是一个对常量的引用,不能改变其邦定的对象

这对调用方的技术能力要求比较高,如果你是设计方,一定要尽量使接口简单易用。

方式<3>

如果你要传入一个Data进行一些处理,处理完后返回类的成员m_data,可如下实现:

1
2
3
4
5
6
7
8
void DoSomething(const Data*& pData)
{
if (pData != NULL)
{
// doto: 这里实现你要处理的功能
}
pData = &m_data;
}

原文

link

C/C++杂记:NULL与0的区别、nullptr的来历

发表于 2019-04-01 更新于 2019-04-13
本文字数: 2.7k 阅读时长 ≈ 2 分钟

某些时候,我们需要将指针赋值为空指针,以防止野指针。

有人喜欢使用NULL作为空指针常量使用,例如:int* p = NULL;。

也有人直接使用0值作为空指针常量,例如:int* p = 0;。

前者可能觉得:NULL作为空指针常量,名字很形象,可读性较强。

后者可能觉得:NULL并不是C/C++语言的关键字,而是一个在标准库头文件<stddef.h>中定义的宏,因此要使用NULL,可能需要直接或简介地包含<stddef.h>头文件,比较麻烦。

问题一:NULL与常数0值有何区别?

要弄清楚这个问题,我们采用问与答的形式来描述。

问:NULL到底是什么?

答:NULL是一个宏。

问:它的值是多少?

答:C/C++标准规定:它的值是一个空指针常量(null pointer constant),由实现定义。#1,#2

问:什么样的值才能称之为空指针常量?

答:C语言中常数0和(void)0都是空指针常量;C++中(暂且忽略C++11)常数0是,而(void)0 不是。#3,#4

问:NULL宏是在哪里定义的?

答:通常是在C标准库的<stddef.h>头文件中,不过别的头文件中可能也有定义。

问:一般编译器的<stddef.h>头文件中NULL宏是如何定义的?

答:以gcc或clang编译器为例,NULL的定义大致如下(稍有简化):

1
2
3
4
5
#if defined(__cplusplus)
# define NULL 0 // C++中使用0作为NULL的值
#else
# define NULL ((void *)0) // C中使用((void *)0)作为NULL的值
#endif

问:为什么C中(void*)0是空指针常量,而C++中不是?

答:因为C语言中任何类型的指针都可以(隐式地)转换为void型,反过来也行,而C++中void型不能隐式地转换为别的类型指针(例如:intp = (void)0;使用C++编译器编译会报错)。#5,#6

问:既然C/C++标准中,常数0都可作为空指针常量,为什么不统一使用0?

答:个人觉得由于(void)0更能体现指针的意义,而常数0更多的时候是用作整数。因此,C语言中NULL定义选择了(void)0。(仅供参考)

问题二:C++11中为什么要引入nullptr?

考虑着这样一个函数重载的情形:

1
2
3
4
5
6
#include <stddef.h>
void foo(int) {} // #1
void foo(char*) {} // #2
int main() {
foo(NULL); // 调用#1还是#2?
}

从字面上来讲,NULL是个空指针常量,我们可能会觉得:既然是个指针,那么应该调用#2。但事实上调用的却是#1,因为C++中NULL扩展为常数0,它是int型。

根本原因就是:常数0既是整数常量,也是空指针常量。

为了解决这种二义性,C++11标准引入了关键字nullptr,它作为一种空指针常量。#7例如:

1
2
3
4
5
void foo(int) {}     // #1
void foo(char*) {} // #2
int main() {
foo(nullptr); // 它会毫无异议地调用#2
}

附注:

[#1] C99: 7.17-p3:
The macros are
NULL
which expands to an implementation-defined null pointer constant; and …

[#2] C++03: 18.1-p4:
The macro NULL is an implementation-defined C + + null pointer constant in this International Standard(4.10).

[#3] C99: 6.3.2.3-p3:
An integer constant expression with the value 0, or such an expression cast to type void *, is called a null pointer constant.

[#4] C++03: 4.10-p1:
A null pointer constant is an integral constant expression (5.19) rvalue of integer type that evaluates to zero.

[#5] C99: 6.3.2.3-p1:
A pointer to void may be converted to or from a pointer to any incomplete or object type. A pointer to any incomplete or object type may be converted to a pointer to void and back again; the result shall compare equal to the original pointer.

[#6] C++03: 4.10-p2:
An rvalue of type “pointer to cv T,” where T is an object type, can be converted to an rvalue of type “pointer to cv void.”

[#7] C++11: 4.10-p1:
A null pointer constant is an integral constant expression (5.19) prvalue of integer type that evaluates to zero or a prvalue of type std::nullptr_t.

参考:

(1) C99/C++03/C++11标准文档

(2) nullptr提案文档:N2431:http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2007/n2431.pdf

link

OSI七层模型详解

发表于 2019-03-30 更新于 2019-04-13 分类于 network
本文字数: 7.3k 阅读时长 ≈ 7 分钟

OSI

OSI 七层模型通过七个层次化的结构模型使不同的系统不同的网络之间实现可靠的通讯,因此其最主要的功能就是帮助不同类型的主机实现数据传输 。

完成中继功能的节点通常称为中继系统。在OSI七层模型中,处于不同层的中继系统具有不同的名称。

一个设备工作在哪一层,关键看它工作时利用哪一层的数据头部信息。网桥工作时,是以MAC头部来决定转发端口的,因此显然它是数据链路层的设备。
具体说:

  • 物理层:网卡,网线,集线器,中继器,调制解调器

  • 数据链路层:网桥,交换机

  • 网络层:路由器

  • 网关工作在第四层传输层及其以上

  • 集线器是物理层设备,采用广播的形式来传输信息。

  • 交换机就是用来进行报文交换的机器。多为链路层设备(二层交换机),能够进行地址学习,采用存储转发的形式来交换报文.。

  • 路由器的一个作用是连通不同的网络,另一个作用是选择信息传送的线路。选择通畅快捷的近路,能大大提高通信速度,减轻网络系统通信负荷,节约网络系统资源,提高网络系统畅通率。

交换机和路由器的区别

交换机拥有一条很高带宽的背部总线和内部交换矩阵。交换机的所有的端口都挂接在这条总线上,控制电路收到数据包以后,处理端口会查找内存中的地址对照表以确定目的MAC(网卡的硬件地址)的NIC(网卡)挂接在哪个端口上,通过内部交换矩阵迅速将数据包传送到目的端口,目的MAC若不存在则广播到所有的端口,接收端口回应后交换机会“学习”新的地址,并把它添加入内部MAC地址表中。
使用交换机也可以把网络“分段”,通过对照MAC地址表,交换机只允许必要的网络流量通过交换机。通过交换机的过滤和转发,可以有效的隔离广播风暴,减少误包和错包的出现,避免共享冲突。
交换机在同一时刻可进行多个端口对之间的数据传输。每一端口都可视为独立的网段,连接在其上的网络设备独自享有全部的带宽,无须同其他设备竞争使用。当节点A向节点D发送数据时,节点B可同时向节点C发送数据,而且这两个传输都享有网络的全部带宽,都有着自己的虚拟连接。假使这里使用的是10Mbps的以太网交换机,那么该交换机这时的总流通量就等于2×10Mbps=20Mbps,而使用10Mbps的共享式HUB时,一个HUB的总流通量也不会超出10Mbps。
总之,交换机是一种基于MAC地址识别,能完成封装转发数据包功能的网络设备。交换机可以“学习”MAC地址,并把其存放在内部地址表中,通过在数据帧的始发者和目标接收者之间建立临时的交换路径,使数据帧直接由源地址到达目的地址。

从过滤网络流量的角度来看,路由器的作用与交换机和网桥非常相似。但是与工作在网络物理层,从物理上划分网段的交换机不同,路由器使用专门的软件协议从逻辑上对整个网络进行划分。例如,一台支持IP协议的路由器可以把网络划分成多个子网段,只有指向特殊IP地址的网络流量才可以通过路由器。对于每一个接收到的数据包,路由器都会重新计算其校验值,并写入新的物理地址。因此,使用路由器转发和过滤数据的速度往往要比只查看数据包物理地址的交换机慢。但是,对于那些结构复杂的网络,使用路由器可以提高网络的整体效率。路由器的另外一个明显优势就是可以自动过滤网络广播。

集线器与路由器在功能上有什么不同?

首先说HUB,也就是集线器。它的作用可以简单的理解为将一些机器连接起来组成一个局域网。而交换机(又名交换式集线器)作用与集线器大体相同。但是两者在性能上有区别:集线器采用的式共享带宽的工作方式,而交换机是独享带宽。这样在机器很多或数据量很大时,两者将会有比较明显的。而路由器与以上两者有明显区别,它的作用在于连接不同的网段并且找到网络中数据传输最合适的路径。路由器是产生于交换机之后,就像交换机产生于集线器之后,所以路由器与交换机也有一定联系,不是完全独立的两种设备。路由器主要克服了交换机不能路由转发数据包的不足。

总的来说,路由器与交换机的主要区别体现在以下几个方面:

  • (1)工作层次不同
    最初的的交换机是工作在数据链路层,而路由器一开始就设计工作在网络层。由于交换机工作在数据链路层,所以它的工作原理比较简单,而路由器工作在网络层,可以得到更多的协议信息,路由器可以做出更加智能的转发决策。

  • (2)数据转发所依据的对象不同
    交换机是利用物理地址或者说MAC地址来确定转发数据的目的地址。而路由器则是利用IP地址来确定数据转发的地址。IP地址是在软件中实现的,描述的是设备所在的网络。MAC地址通常是硬件自带的,由网卡生产商来分配的,而且已经固化到了网卡中去,一般来说是不可更改的。而IP地址则通常由网络管理员或系统自动分配。

  • (3)传统的交换机只能分割冲突域,不能分割广播域;而路由器可以分割广播域
    由交换机连接的网段仍属于同一个广播域,广播数据包会在交换机连接的所有网段上传播,在某些情况下会导致通信拥挤和安全漏洞。连接到路由器上的网段会被分配成不同的广播域,广播数据不会穿过路由器。虽然第三层以上交换机具有VLAN功能,也可以分割广播域,但是各子广播域之间是不能通信交流的,它们之间的交流仍然需要路由器。

  • (4)路由器提供了防火墙的服务
    路由器仅仅转发特定地址的数据包,不传送不支持路由协议的数据包传送和未知目标网络数据包的传送,从而可以防止广播风暴。

物理层

在OSI参考模型中,物理层(Physical Layer)是参考模型的最低层,也是OSI模型的第一层。
物理层的主要功能是:利用传输介质为数据链路层提供物理连接,实现比特流的透明传输。
物理层的作用是实现相邻计算机节点之间比特流的透明传送,尽可能屏蔽掉具体传输介质和物理设备的差异。使其上面的数据链路层不必考虑网络的具体传输介质是什么。“透明传送比特流”表示经实际电路传送后的比特流没有发生变化,对传送的比特流来说,这个电路好像是看不见的。

数据链路层

数据链路层(Data Link Layer)是OSI模型的第二层,负责建立和管理节点间的链路。该层的主要功能是:通过各种控制协议,将有差错的物理信道变为无差错的、能可靠传输数据帧的数据链路。
在计算机网络中由于各种干扰的存在,物理链路是不可靠的。因此,这一层的主要功能是在物理层提供的比特流的基础上,通过差错控制、流量控制方法,使有差错的物理线路变为无差错的数据链路,即提供可靠的通过物理介质传输数据的方法。
该层通常又被分为介质访问控制(MAC)和逻辑链路控制(LLC)两个子层。

MAC子层的主要任务是解决共享型网络中多用户对信道竞争的问题,完成网络介质的访问控制;

LLC子层的主要任务是建立和维护网络连接,执行差错校验、流量控制和链路控制。

数据链路层的具体工作是接收来自物理层的位流形式的数据,并封装成帧,传送到上一层;同样,也将来自上层的数据帧,拆装为位流形式的数据转发到物理层;并且,还负责处理接收端发回的确认帧的信息,以便提供可靠的数据传输。

网络层

网络层(Network Layer)是OSI模型的第三层,它是OSI参考模型中最复杂的一层,也是通信子网的最高一层。它在下两层的基础上向资源子网提供服务。其主要任务是:通过路由选择算法,为报文或分组通过通信子网选择最适当的路径。该层控制数据链路层与传输层之间的信息转发,建立、维持和终止网络的连接。具体地说,数据链路层的数据在这一层被转换为数据包,然后通过路径选择、分段组合、顺序、进/出路由等控制,将信息从一个网络设备传送到另一个网络设备。

一般地,数据链路层是解决同一网络内节点之间的通信,而网络层主要解决不同子网间的通信。例如在广域网之间通信时,必然会遇到路由(即两节点间可能有多条路径)选择问题。

在实现网络层功能时,需要解决的主要问题如下:

  • 寻址:数据链路层中使用的物理地址(如MAC地址)仅解决网络内部的寻址问题。在不同子网之间通信时,为了识别和找到网络中的设备,每一子网中的设备都会被分配一个唯一的地址。由于各子网使用的物理技术可能不同,因此这个地址应当是逻辑地址(如IP地址)。
  • 交换:规定不同的信息交换方式。常见的交换技术有:线路交换技术和存储转发技术,后者又包括报文交换技术和分组交换技术。
  • 路由算法:当源节点和目的节点之间存在多条路径时,本层可以根据路由算法,通过网络为数据分组选择最佳路径,并将信息从最合适的路径由发送端传送到接收端。
  • 连接服务:与数据链路层流量控制不同的是,前者控制的是网络相邻节点间的流量,后者控制的是从源节点到目的节点间的流量。其目的在于防止阻塞,并进行差错检测。

传输层

OSI下3层的主要任务是数据通信,上3层的任务是数据处理。而传输层(Transport Layer)是OSI模型的第4层。因此该层是通信子网和资源子网的接口和桥梁,起到承上启下的作用。
该层的主要任务是:向用户提供可靠的端到端的差错和流量控制,保证报文的正确传输。传输层的作用是向高层屏蔽下层数据通信的细节,即向用户透明地传送报文。该层常见的协议:TCP/IP中的TCP协议、Novell网络中的SPX协议和微软的NetBIOS/NetBEUI协议。
传输层提供会话层和网络层之间的传输服务,这种服务从会话层获得数据,并在必要时,对数据进行分割。然后,传输层将数据传递到网络层,并确保数据能正确无误地传送到网络层。因此,传输层负责提供两节点之间数据的可靠传送,当两节点的联系确定之后,传输层则负责监督工作。综上,传输层的主要功能如下:
传输连接管理:提供建立、维护和拆除传输连接的功能。传输层在网络层的基础上为高层提供“面向连接”和“面向无接连”的两种服务。
处理传输差错:提供可靠的“面向连接”和不太可靠的“面向无连接”的数据传输服务、差错控制和流量控制。在提供“面向连接”服务时,通过这一层传输的数据将由目标设备确认,如果在指定的时间内未收到确认信息,数据将被重发。
监控服务质量。

会话层

会话层(Session Layer)是OSI模型的第5层,是用户应用程序和网络之间的接口,主要任务是:向两个实体的表示层提供建立和使用连接的方法。将不同实体之间的表示层的连接称为会话。因此会话层的任务就是组织和协调两个会话进程之间的通信,并对数据交换进行管理。

用户可以按照半双工、单工和全双工的方式建立会话。当建立会话时,用户必须提供他们想要连接的远程地址。而这些地址与MAC(介质访问控制子层)地址或网络层的逻辑地址不同,它们是为用户专门设计的,更便于用户记忆。域名(DN)就是一种网络上使用的远程地址例如:www.3721.com就是一个域名。会话层的具体功能如下:
会话管理:允许用户在两个实体设备之间建立、维持和终止会话,并支持它们之间的数据交换。例如提供单方向会话或双向同时会话,并管理会话中的发送顺序,以及会话所占用时间的长短。

会话流量控制:提供会话流量控制和交叉会话功能。
寻址:使用远程地址建立会话连接。l

出错控制:从逻辑上讲会话层主要负责数据交换的建立、保持和终止,但实际的工作却是接收来自传输层的数据,并负责纠正错误。会话控制和远程过程调用均属于这一层的功能。但应注意,此层检查的错误不是通信介质的错误,而是磁盘空间、打印机缺纸等类型的高级错误。

表示层

表示层(Presentation Layer)是OSI模型的第六层,它对来自应用层的命令和数据进行解释,对各种语法赋予相应的含义,并按照一定的格式传送给会话层。其主要功能是“处理用户信息的表示问题,如编码、数据格式转换和加密解密”等。表示层的具体功能如下:
数据格式处理:协商和建立数据交换的格式,解决各应用程序之间在数据格式表示上的差异。
数据的编码:处理字符集和数字的转换。例如由于用户程序中的数据类型(整型或实型、有符号或无符号等)、用户标识等都可以有不同的表示方式,因此,在设备之间需要具有在不同字符集或格式之间转换的功能。
压缩和解压缩:为了减少数据的传输量,这一层还负责数据的压缩与恢复。
数据的加密和解密:可以提高网络的安全性。

应用层

应用层(Application Layer)是OSI参考模型的最高层,它是计算机用户,以及各种应用程序和网络之间的接口,其功能是直接向用户提供服务,完成用户希望在网络上完成的各种工作。它在其他6层工作的基础上,负责完成网络中应用程序与网络操作系统之间的联系,建立与结束使用者之间的联系,并完成网络用户提出的各种网络服务及应用所需的监督、管理和服务等各种协议。此外,该层还负责协调各个应用程序间的工作。

应用层为用户提供的服务和协议有:文件服务、目录服务、文件传输服务(FTP)、远程登录服务(Telnet)、电子邮件服务(E-mail)、打印服务、安全服务、网络管理服务、数据库服务等。上述的各种网络服务由该层的不同应用协议和程序完成,不同的网络操作系统之间在功能、界面、实现技术、对硬件的支持、安全可靠性以及具有的各种应用程序接口等各个方面的差异是很大的。应用层的主要功能如下:

用户接口:应用层是用户与网络,以及应用程序与网络间的直接接口,使得用户能够与网络进行交互式联系。
实现各种服务:该层具有的各种应用程序可以完成和实现用户请求的各种服务。

OSI7层模型的小结

由于OSI是一个理想的模型,因此一般网络系统只涉及其中的几层,很少有系统能够具有所有的7层,并完全遵循它的规定。
在7层模型中,每一层都提供一个特殊的网络功能。从网络功能的角度观察:下面4层(物理层、数据链路层、网络层和传输层)主要提供数据传输和交换功能,即以节点到节点之间的通信为主;第4层作为上下两部分的桥梁,是整个网络体系结构中最关键的部分;而上3层(会话层、表示层和应用层)则以提供用户与应用程序之间的信息和数据处理功能为主。简言之,下4层主要完成通信子网的功能,上3层主要完成资源子网的功能。

以下是TCP/IP分层模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  ┌────------────┐┌─┬─┬─-┬─┬─-┬─┬─-┬─┬─-┬─┬─-┐
  │         ││D│F│W│F│H│G│T│I│S│U│ │
  │         ││N│I│H│T│T│O│E│R│M│S│其│
  │第四层,应用层  ││S│N│O│P│T│P│L│C│T│E│ │
  │         ││ │G│I│ │P│H│N│ │P│N│ │
  │         ││ │E│S│ │ │E│E│ │ │E│它│
  │         ││ │R│ │ │ │R│T│ │ │T│ │
  └───────------─┘└─┴─┴─-┴─┴─-┴─┴─-┴─┴─-┴─┴-─┘
  ┌───────-----─┐┌─────────-------┬──--------─────────┐
  │第三层,传输层 ││   TCP   │    UDP    │
  └───────-----─┘└────────-------─┴──────────--------─┘
  ┌───────-----─┐┌───----──┬───---─┬────────-------──┐
  │        ││     │ICMP│          │
  │第二层,网间层 ││     └──---──┘          │
  │        ││       IP            │
  └────────-----┘└────────────────────-------------─-┘
  ┌────────-----┐┌─────────-------┬──────--------─────┐
  │第一层,网络接口││ARP/RARP │    其它     │
  └────────------┘└─────────------┴─────--------──────┘

TCP/IP四层参考模型

  TCP/IP协议被组织成四个概念层,其中有三层对应于ISO参考模型中的相应层。ICP/IP协议族并不包含物理层和数据链路层,因此它不能独立完成整个计算机网络系统的功能,必须与许多其他的协议协同工作。

  TCP/IP分层模型的四个协议层分别完成以下的功能:

  • 第一层:网络接口层

  包括用于协作IP数据在已有网络介质上传输的协议。实际上TCP/IP标准并不定义与ISO数据链路层和物理层相对应的功能。相反,它定义像地址解析协议(Address Resolution Protocol,ARP)这样的协议,提供TCP/IP协议的数据结构和实际物理硬件之间的接口。

  • 第二层:网间层

  对应于OSI七层参考模型的网络层。本层包含IP协议、RIP协议(Routing Information Protocol,路由信息协议),负责数据的包装、寻址和路由。同时还包含网间控制报文协议(Internet Control Message Protocol,ICMP)用来提供网络诊断信息。

  • 第三层:传输层

  对应于OSI七层参考模型的传输层,它提供两种端到端的通信服务。其中TCP协议(Transmission Control Protocol)提供可靠的数据流运输服务,UDP协议(Use Datagram Protocol)提供不可靠的用户数据报服务。

  • 第四层:应用层

  对应于OSI七层参考模型的应用层和表达层。因特网的应用层协议包括Finger、Whois、FTP(文件传输协议)、Gopher、HTTP(超文本传输协议)、Telent(远程终端协议)、SMTP(简单邮件传送协议)、IRC(因特网中继会话)、NNTP(网络新闻传输协议)等,这也是本书将要讨论的重点

原文链接

link

1…171819…26
AIRobot

AIRobot

AIRobot quick note
130 日志
15 分类
23 标签
GitHub E-Mail
Creative Commons
0%
© 2023 AIRobot | 716k | 10:51