如何使用 c++ stl 中的 map 以及红黑树 (一)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u012062760/article/details/43237013


Two Sum

Given an array of integers, find two numbers such thatthey add up to a specific target number.

The function twoSum should return indices of the twonumbers such that they add up to the target, where index1 must be less thanindex2. Please note that your returned answers (both index1 and index2) are notzero-based.

You may assume that each input would have exactly one solution.

Input: numbers={2, 7, 11, 15}, target=9
Output: index1=1, index2=2

 

 

       乍看到这题,想必学过编程的小白都会想到用暴力破解,但试过之后发现数据量大之后会爆炸,超时。

       但是整体思路还是不变的。

      暴力破解的思路是: 有了A之后,就寻找一个B,使得满足A+B=C,且AC是已知的,那么就是寻找B=C-A

 

既然用循环寻找B会超时,那么使用一个寻找起来很快的数据结构是不是会好点呢?

这里我就想到了c++里的map(当然,并不是说只能用map)了

 

 



第一部分

Map的特点是:

              1、支持快速查找,查找的复杂度基本是Log(N)
              2
、快速插入,快速删除,快速修改

       因此这里复习一下如何使用map

 

 

第二部分

Map采用的是红黑树实现的

       因此,我也会趁此机会复习一下简单的红黑树实现。

 

 

第三部分

       虽然知道了重点在于查找的速度,但是普通的map可是不支持重复的key的啊?我们如何利用map来解决这个问题呢?

                                   第一部分、如何使用stl中的map

 







-----------------------------------------------------------------------------------------------------------------------------------------------------------------------

                                                                                             第一部分


1.1特点:

  1. map中的数据类型是 key-value 结构,且keyvalue的类型可以是任意类型。

  2. map增加或删除元素十分容易

  3. 可以根据key的值修改value,但不能修改key的值

  4. map的查询时间复杂度为LogN,也就是如果包含1000个记录,查询次数最多为  10次,1,000,000个记录,最多查找20次。

 

 

1.2使用:

 

       1.2.1加载头文件

              与使用stl 中其他数据结构类似,都是加上相应名称的头文件

                     #include<map>

      

       1.2.2map对象的声明(构造函数)

              方法一、

                     比如  map<int, int> Map 即声明了一个keyint类型, valueint类型的对象Map

                     比如  map<string, int> Map 即声明了一个keystring类型, valueint类型的对象Map

              方法二、

                     也可以采取更方便的一种方式:

                                   typedefmap<int, string> My_type;

                                   My_typeMap;

                     即声明了一个keyint类型, valuestring类型的对象Map

      

map中也有几种可用的数据类型:

                     map<K,V>::key_type : 表示map容器中,索引的类型;

map<K, V>::mapped_type : 表示map容器中,键所关联的值的类型;

map<K, V>::value_type : 表示一个pair类型:

它的first元素具有constmap<K,V>::key_type类型,而second元素则有map<K, V>::mapped_type类型

 

1.2.3 map中插入元素

       map<string, int> Map;

       一、Map[“Ben”] = 5;                  //key”Ben”value 5

       二、Map.insert(map<string,int>::value_type(“Ben”,5));     

       三、Map.insert(pair<string,int>(“Ben”,5))

      

       经实验后发现,若插入的一对 key-value keymap中已经存在的话,该key对应的原来的value会被新的value覆盖。

 

1.2.4 map中查找元素

       即是用map中的find函数。

              返回值为 iterato 类型, 比如:

                     map<int,string> Map;

                     map<int,string>::iterator it;

                     it= Map.find(5);                        //寻找key5map中的一项

                     if(it == Map.end() )

                            cout<< “No such data” << endl;

                     else

                            cout<< “The value is ” << it->first << “ and its value is ”<< it->second << endl;

 

              因此这里可以发现,itfirst的值即为map项中的key值,second即为value

              因此,我们可以通过这个来修改某个value

                     It->second= “str”;

 

       find 是正统的查找函数,还有另一个函数 count 其返回值为01,因为map中的key是不会重复的,所以只可能为01,为0即表示该项不存在。

 

 

1.2.5 map中删除某项

       (1)   my_Map.erase(my_Itr);                                 //删除 iterator 类型的数据
              (2)   my_Map.erase("c");                                       //
根据key值删除

       (3)  my.Map.erase(iteratorfirst, iterator last);      //删除一个范围

 

 

1.2.6 遍历整个map

       使用 iterator

       比如:

              map<int,string> Map;

              map<int,string>::iterator it;

              for(it = Map.begin(); it != Map.end(); it++)

                     cout<< it->first << it –>second << endl;

 

 

1.2.7   其它方法

  Map.size()               返回元素数目 
  Map.empty()       
判断是否为空
 
  Map.clear()           
清空所有元素 

      

 

       1.2.8  下面附上一个综合的程序

#include <map>

#include<iostream>

#include<string>

 

using namespace std;

 

int main()

{

    //一个key值只能出现一次

    map <string, string> Map;

 

    //How to insert ?

    Map.insert(pair<string,string>("Y", "yuanhaitao"));                    //Way 1

    Map.insert(map<string,string>::value_type("X","xiaopenegyou"));         //Way 2

    Map.insert(pair<string,string>("A", "abigmouse"));

    Map.insert(pair<string,string>("B", "butwhat"));

    Map.insert(pair<string,string>("C", "candidate"));

    Map["D"] ="dogcanyoufly";                                              //Way3

    Map["D"] = "fuck";                                              //重复的就会覆盖之前的数据

 

    //How to print all ?

    map<string, string>::iterator it;

    for (it = Map.begin(); it != Map.end();it++)

        //first represents key and secondrepresents value.

        cout << "Key is "<< it->first << " and its value is " <<it->second << endl;

 

    cout << endl;

 

    //How to find ?

    string valueb = Map["B"];

    cout << "The value of the key 'B'is " << valueb << endl;

 

    string key = "Y";

   

    it = Map.find(key);

    cout << endl << "find :" << endl;

    cout << "Key is " <<it->first << " and its value is " << it->second<< endl;

 

    int count = Map.count(key);

    cout << endl << "count :"<< endl;

    cout << "The key 'Y' appears" << count << " times , so it exists !" <<endl;

 

    key = "Z";

    count = Map.count(key);

    cout << endl << "count:" << endl;

    cout << "The key 'Z' appears" << count << " times , so it doesn't exist !"<< endl << endl;

 

    //How to erase ?

    Map.erase("D");

    cout << "After erase :"<< endl;

    for (it = Map.begin(); it != Map.end();it++)

        cout << "Key is "<< it->first << " and its value is " <<it->second << endl;

 

    system("pause");

    return 0;

}


 








-----------------------------------------------------------------------------------------------------------------------------------------------------------------------



                                     第二部分、如何实现简单的红黑树


1.1  参考:  1、算法导论

                     2http://blog.csdn.net/v_JULY_v/article/category/774945


本篇文章是留给自己复习用的,也只是照葫芦画瓢,无创新无亮点,c语言实现。


1.2  红黑书满足的几个基本性质:


        1 每个节点要么是红的,要么是黑的

        2 根节点是黑的

        3 每个叶节点,即度为0的节点是黑色的

        4 如果一个节点是红色的,那么他的两个儿子就都是黑的

        5 对每个节点,从该节点到其所有叶子节点的所有路径包含相同数目的黑节点

        6)如下为棵红黑树:(黑色节点用黑色表示,红色则以白色表示,叶子节点都会忽略不显示)


             



    7 图中可以看出,红黑树的高度与节点数【大致】是成log(n) 关系的,所以一般认为其操作执行的时间复杂度就是 O(logn)(非常不严谨 --. (想想之前学过的平衡二叉树,其实这两种树有类似,都通过旋转实现平衡)





1.3  既然是一个简单的红黑树,侧重点就在增加、删除、查找这3个实现上。


       1.3.1  红黑树添加节点,我们考虑

                     既然要添加新的节点,那么问题来了,平时二叉树我们都是怎么添加的?

                     红黑树的性质:

                            “4 如果一个节点是红色的,那么他的两个儿子就都是黑的

                     性质里好像对黑节点下面的节点没有限制

                     我们插红节点还是黑节点?


                先解答第4个问题:

                            如果我们一直选择的是插黑色节点,那么根据性质,我们插入以后就跟普通的二叉树一样了,好像并没有需要调整的地方,那我们怎么保证这棵树的平衡性呢? 所以我们选择插红色节点,这样我们才能保证第5条性质 “对每个节点,从该节点到其所有子孙节点的所有路径包含相同数目的黑节点”


 

                     这样,选择插红色节点就面临很多种情况了:

                            Case1:若这个节点为树的根节点

                            Case2:若这个节点的父节点为黑色节点

                            Case3:若这个节点的父节点为红色节点(第4个性质)


1.3.2  问题出现了,我们就有着手点了:


                     1、若为情况1、要插入的节点为树的根节点

                            那简单,直接给一个左右子为空的节点就成了。不过,应注意第一点性质,根节点可是黑色的啊!这里就插了个值为12的根节点。


                                                                

                     2、若为情况2,这个节点的父节点为黑色节点

                            这也简单,性质中对黑色节点的子节点没有限制,我插一个值为1的红色节点(除了根节点,以后都是用红色节点插了),但插入时候也要根据二叉树插入的顺序来,即左子小于父,右子大于父的原则。


                             



                      3、若情况为3,这个节点的父节点为红色节点

                            这就复杂了呀,需要考虑两个方面:

                                   4 如果一个节点是红色的,那么他的两个儿子就都是黑的

                                   5 对每个节点,从该节点到其所有叶子节点的所有路径包含相同数目的黑节点

                            比如我这时候就要插入一个9,红的哟。

                     第一步我们想到的就是:


 

                               


           但是这样就违背了第四点了啊。那如何才能既不违背第4点,又能满足二叉树的基本顺序还要符合第五点呢?

                            改颜色?

                                   19涂黑了?       --- 这样根节点就不符合第5点性质了(首要)

                            换结构?

                                   9为根?                  ---不是说好的根节点为黑吗?


                     这里就要引入一种旋转机制了。以后但凡遇到这种情况,都会采取这种旋转机制以摆脱困境 - -

                     这里的旋转是指两个节点之间的旋转。


                     A 左旋转

                            拿我们刚才的例子来讲,首先我要以9为根,颜色以后变。注意,旋转是两个节点的事,明显看出来9是不可能直接转成根节点的,只能跟1在那里转。


                            那我们先把9转过来

                            



            看,很顺滑的向左转过来了! 这过程~~~特么的太清晰了!

                          下面给出代码:

看,很顺滑的向左转过来了! 这过程~~~特么的太清晰了!
		下面给出代码:
			void LEFT_ROTATE(RBTree &T,RBTree x)
			//T为要改动的树的名字 ; 没有另一个节点是因为左转必然是右孩子和父节点进行的,所以只需要给出父节点。
{
				RBTree y = x->Rchild;
				x->Rchild = y->Lchild;
				//当前的例子简单,所以右孩子y没有子节点;当右孩子y也有子节点时候 :如果右孩子y拥有右子,则不变;有左子,则寄托在原来父节点x的右边,因为x原来右子y已经变成x老子了
				if(y->Lchild)
					y->Lchild->Parent = x;
					//老子的儿子的身份变了,那儿子的老子的身份也要变一下
				if(!x->Parent)
					//如果x恰好为根,根就换人了
					T = y;
				else if(x==x->Parent->Lchild)
					//如果x恰好在他的父节点左边
					x->Parent->Lchild = y;
				else
					//否则,在右边
					x->Parent->Rchild = y;
				y->Parent = x->Parent;
				y->Lchild = x;
				x->Parent = y;
}



                     B 右旋转

                            现在已经转成这样了,那我们的目的还没达到,要让9成为根节点啊,我们可以想象是有一个指针指向这棵树的根节点的,就好像有根线吊着这个值为12的节点。


                               



               现在这根线吊着9

                           具体代码为:(与左转类似的)

void RIGHT_ROTATE(RBTree &T,RBTree y)								//右旋
{
	RBTree x = y->Lchild;
	y->Lchild = x->Rchild;
	if(x->Rchild)
		x->Rchild->Parent = y;
	if(!y->Parent)
		T = x;
	else if(y==y->Parent->Lchild)
		y->Parent->Lchild = x;
	else
		y->Parent->Rchild = x;
	x->Rchild = y;	
	RBTree temp = y->Parent;
	//printf("\n%p\n%p\n%p\n",&x->Parent,&y,temp);
	x->Parent = temp;
	y->Parent = x;
}

 


        好了,这里介绍完了如何进行旋转,当然,也仅仅是旋转。

            我们回顾一下,当左旋转结束之后,如果我们直接继续右旋转,最终情况并没有如我们想象中解决问题,因为颜色不符合要求。


    前面也探讨过:

           若是单纯变换颜色,爷爷节点不符合第5点性质。

    现在我们继续讨论:

           若是变换颜色后,进行左旋转,还是与之前情况一样,或者说没什么影响

    这时候:

           若是在左旋转之后,我们改变颜色,再进行右旋转,是不是情况就不一样了呢?

                      


                 



    这里还需要一些运算来排除一些情况,这里就暂不讨论了


    通过这个例子,我们发现,如果我们当初插入的不是9,而是0的话,不需要经过左旋转,而是直接改变颜色后进行右旋转,因此,我们这里总结一下一种情况:


        当我们要插入一个节点,且其父节点时红色的:

                若我是父节点的右子节点,

                     1、我的父节点做为新的当前节点,以新当前节点为支点左转

                     2、当前节点父节点变为黑色,祖父节点变为红色,以祖父节点为支点右旋


                 若我是父节点的左子节点,则

                          父节点变为黑色,祖父节点变为红色,在祖父节点为支点右旋转


        等一下,这样就够了吗? 我们是不是忽略了一个很重要的节点?


                就是与父节点有相同地位的节点(爷爷节点的另一个孩子,以后称为叔叔节点,什么?没有叔叔节点?叶子就是丫叔叔),如果当初插入9的时候,叔叔节点是个存在的红色的节点,我们是不是还需要这样旋转啊?

                                  


               之前不是说因为爷爷节点不符合第5点性质所以颜色变换不符合吗,现在我们决定:   把父节点和叔叔节点都涂黑了,我们新插入的节点不就没差了吗?不,差的!多了一层黑色节点,为了不影响【某节点到所有叶子节点的所有路径上黑色节点出现的次数】那只能把爷爷节点涂红了(当然,若爷爷是根节点,那自然不用动了),这时候就要看爷爷节点的改动是不是对其他性质产生了影响了


                                 


(这里因为爷爷节点是根节点,所以不变,其他情况下爷爷节点会被涂红)


      因此,让我们再次总结:

     当我们要插入一个节点,且其父节点时红色的:

            若叔叔节点是黑色的,那么,

                若我是父节点的右子节点,则

                        1、我的父节点做为新的当前节点,以新当前节点为支点左旋
                        2、当前节点父节点变为黑色,祖父节点变为红色,在祖父节点为支点右旋
                   若我是父节点的左子节点,则
                           父节点变为黑色,祖父节点变为红色,在祖父节点为支点右旋转
               若叔叔节点时红色的,那么
                           调整父亲和叔叔颜色都变成黑色 祖父节点变成红色 然后当前要处理的节点指针指向祖父节点



       这之后,在实际操作过程中,我们发现,父亲节点在爷爷节点的左右也影响着旋转的方向



最后,我们做一个插入的最终总结:

插入:

       (在插入新节点之前 初始化一个新节点的时候总是将其赋予红色 因为这样尽量不引起红黑树的黑高度的变化) 且 (插入调整主要看叔叔节点 及 当前节点在当前节点的父亲节点的左边还是右边)

      【首先这里讨论的是父亲是祖父的左子节点】+【这里的处理对象是新加入的节点】

       

    case ①: 为根

    solution: 将节点涂黑


    case ②: 插在黑节点的下面

    solution: 没有限制,无需调整


    case ③: 插在红节点下面

                  

                case ⑴: 叔叔节点为红色

                solution:调整父亲和叔叔颜色都变成黑色 祖父节点变成红色 然后当前要处理的节点指针指向祖父节点


             case ⑵: 叔叔为黑

                    {

                  case ㈠: 当前节点为父亲节点的右子节点

                  solution: 当前节点的父节点做为新的当前节点,以新当前节点为支点左旋


                  case㈡:  当前节点为父亲节点的左子节点

                  solution:父节点变为黑色,祖父节点变为红色,在祖父节点为支点右旋

                    

      

            



可能我们会问:我们这样处理某几个节点,难道不会对以前处理过的节点产生影响吗?
        
当然会啊,只是因为我们插入节点都是插在最底层的,处理某几个节点之后就会把当前节点转移到上层,然后像是插新的节点一样对他们进行判断。

        所以,这里会采用递归处理。



那么,接下来,我们就把一棵完整的红黑树插完吧

    这里借鉴的是

                http://blog.csdn.net/v_JULY_v/article/details/6284050

    我们按照以下顺序进行插入

        12 1 9 2 0 11 7 19 4 15 18 514 13 10 16


    首先插入 1219

            之前讲过,所以前3个节点顺序直接给出

                     
                                  

     插入2

                 

     插入0

                               


     插入11


                                

     插入7

               

     插入19

                                 

     插入4

                 


     插入15

                         


     插入18:

                     


     插入5:

                        


                         


     插入14


                       

     插入13、10

                        

     插入16:


 



真是麻烦啊 ,还有删除和查找;删除部分另开一篇吧。


展开阅读全文

没有更多推荐了,返回首页