类中的静态成员真是个让人爱恨交加的特性。我决定好好总结一下静态类成员的知识点,以便自己在以后面试中,在此类问题上不在被动。
静态类成员包括静态数据成员和静态函数成员两部分。
一 静态数据成员:
类体中的数据成员的声明前加上static关键字,该数据成员就成为了该类的静态数据成员。和其他数据成员一样,静态数据成员也遵守public/protected/private访问规则。同时,静态数据成员还具有以下特点:
1.静态数据成员的定义。
静态数据成员实际上是类域中的全局变量。所以,静态数据成员的定义(初始化)不应该被放在头文件中。
其定义方式与全局变量相同。举例如下:
xxx.h文件
class base{
private:
static const int _i;//声明,标准c++支持有序类型在类体中初始化,但vc6不支持。
};
xxx.cpp文件
const int base::_i=10;//定义(初始化)时不受private和protected访问限制.
注:不要试图在头文件中定义(初始化)静态数据成员。在大多数的情况下,这样做会引起重复定义这样的错误。即使加上#ifndef #define #endif或者#pragma once也不行。
2.静态数据成员被 类 的所有对象所共享,包括该类派生类的对象。即派生类对象与基类对象共享基类的静态数据成员。举例如下:
class base{
public :
static int _num;//声明
};
int base::_num=0;//静态数据成员的真正定义
class derived:public base{
};
main()
{
base a;
derived b;
a._num++;
cout<<"base class static data number _num is"<<a._num<<endl;
b._num++;
cout<<"derived class static data number _num is"<<b._num<<endl;
}
// 结果为1,2;可见派生类与基类共用一个静态数据成员。
3.静态数据成员可以成为成员函数的可选参数,而普通数据成员则不可以。举例如下:
class base{
public :
static int _staticVar;
int _var;
void foo1(int i=_staticVar);//正确,_staticVar为静态数据成员
void foo2(int i=_var);//错误,_var为普通数据成员
};
4.★
静态数据成员的类型可以是所属类的类型,而普通数据成员则不可以。普通数据成员的只能声明为 所属类类型的 指针或引用。举例如下:
class base{
public :
static base _object1;//正确,静态数据成员
base _object2;//错误
base *pObject;//正确,指针
base &mObject;//正确,引用
};
5.★这个特性,我不知道是属于标准c++中的特性,还是vc6自己的特性。
静态数据成员的值在const成员函数中可以被合法的改变。举例如下:
class base{
public:
base(){_i=0;_val=0;}
mutable int _i;
static int _staticVal;
int _val;
void test() const{//const 成员函数
_i++;//正确,mutable数据成员
_staticVal++;//正确,static数据成员
_val++;//错误
}
};
int base::_staticVal=0;
二,静态成员函数
静态成员函数没有什么太多好讲的。
1.静态成员函数的地址可用普通函数指针储存,而普通成员函数地址需要用 类成员函数指针来储存。举例如下:
class base{
static int func1();
int func2();
};
int (*pf1)()=&base::func1;//普通的函数指针
int (base::*pf2)()=&base::func2;//成员函数指针
2.静态成员函数不可以调用类的非静态成员。因为静态成员函数不含this指针。
3.静态成员函数不可以同时声明为 virtual、const、volatile函数。举例如下:
class base{
virtual static void func1();//错误
static void func2() const;//错误
static void func3() volatile;//错误
};
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
在网上看到有关STL中hash_map的文章,以及一些其他关于STL map和hash_map的资料,总结笔记如下:
1、STL的map底层是用红黑树实现的,查找时间复杂度是log(n);
2、STL的hash_map底层是用hash表存储的,查询时间复杂度是O(1);
3、什么时候用map,什么时候用hash_map?
这个药看具体的应用,不一定常数级别的hash_map一定比log(n)级别的map要好,hash_map的hash函数以及解决地址冲突等都要耗时间,而且众所周知hash表是以空间换时间的,因而hash_map的内存消耗肯定要大,一般情况下,如果记录非常大,考虑hash_map,查找效率会高很多,如果要考虑内存消耗,则要谨慎使用hash_map。
以下内容来自:http://blog.163.com/liuruigong_lrg/blog/static/27370306200711334341781/
0. 为什么需要hash_map
用过map吧,map提供一个很常用的功能,那就是提供key-value的存储和查询功能。例如,我要记录一个人名和相应的存储,而且随时增加,要快速查找和修改:
岳不群-华山派掌门人,人称君子剑
张三丰-武当掌门人,太极拳创始人
东方不败-第一高手,葵花宝典
.......
这些信息如果保存下来并不复杂,但是找起来比较麻烦。例如我要找“张三丰”的信息,最笨的方法就是取得所有的记录,然后按照名字一个一个比较。如果要速度快,就需要把这些记录按照字母顺序排列,然后按照二分查找。但是增加记录的时间同时需要保持记录有序,因此需要插入排序。考虑到效率,这就需要用二叉树。如果你使用STL中map容器,你可以非常方便实现这个功能,而不用关心其他细节。看看map的实现。
- map<string, string> namemap;
-
- 2.
-
- 3.
-
- 4. namemap["岳不群"]="华山派掌门人,人称君子剑";
-
- 5. namemap["张三丰"]="武当掌门人,太极拳创始人";
-
- 6. namemap["东方不败"]="第一高手,葵花宝典";
-
- 7. ...
-
- 8.
-
- 9.
-
- 10. if(namemap.find("岳不群") != namemap.end())
-
- 11. {
-
- 12. ...
-
- 13. }
实现起来比较容易,而且效率很高,100万条记录,最多也只要20次的string.compare的比较,就能找到你要找的记录。
速度永远满足不了现实的需求,如果100万条记录,需要频繁的进行搜索时,20次比较也会成为瓶颈,要是能降到一次或者两次比较是否有可能?而且当记录数到200万的时候也是一次或二次比较,是否有可能?而且还需要和map一样的方便使用。
答案是肯定的,这时你需要has_map。虽然hash_map目前没有纳入C++标准模板库中,但几乎每个版本的STL都提供了相应的实现。而且应用十分广泛。在正式使用hash_map之前,先看看hash_map的原理。
1、数据结构:hash_map原理
hash_map基于hash table(哈希表)。哈希表最大的优点是:把数据存储和查询消耗的时间大大降低,几乎可以看成是常数时间;而代价仅仅是消耗比较多的内存。然后在当前可利用内存越来越多的情况下,用空间换时间的做法是值得的。另外,编码比较容易也是它的特点之一。
其基本原理是:使用一个下标范围比较大的数组来存储元素。可以设计一个函数(哈希函数,也叫散列函数),使得每个元素的关键字都与一个函数值(即数组下标,hash值)相对应,于是用这个数组单元来存储这个元素;也可以简单的理解为,按照关键字为每一个元素“分类”,然后将这个元素存储在相应“类”所对应的地方,称为桶。
但是,这不能够保证每个元素的关键字与函数值是一一对应的,因此极有可能出现对不不同的元素,却计算出相同的函数值,这样就产生了“冲突”。换句话说,就是把不同的元素分在了相同的“类”中。总的来说,“直接定址”和“解决冲突”是哈希表的两大特点。
hash_map,首先分配一大片内存,形成许多桶。是利用hash函数,对key进行映射到不同区域进行保存。其插入过程:
1、得到key;
2、通过hash函数得到hash值;
3、得到桶号(一般都为hash值对桶数求模);
4、存放key和value在桶内;
其取值过程是:
1、得到key;
2、通过hash函数得到hash值;
3、得到桶号;
4、比较桶的内部元素是否与key相等,若不相等,则没有找到;
5、取出相等的记录的value;
hash_map中直接地址用hash函数生成,解决冲突,用比较函数解决。这里可以看出,如果每个桶内部只有一个元素,那么查找的时候只有一次比较。当许多桶没有值时,许多查询就会更快了(指查不到的时候)。
由此可见,要实现哈希表,和用户相关的是:hash函数和比较函数。这两个参数刚好是我们在使用hash_map时需要指定的参数。
2、hash_map的使用
2.1 一个简单的例子
不要急着如何把“岳不群”用hash_map表示,先看一个简单的例子:随你给你一个ID号和ID号相应的信息,ID号的范围是1~231。如何快速查找保存。
- #include <iostream>
-
- 2. #include <string>
-
- 3. #include <hash_map>
-
- 4.
-
- 5. using namespace std;
-
- 6.
-
- 7. int main()
-
- 8. {
-
- 9. hash_map<int, string> mp;
-
- 10. mp[9527] = "唐伯虎点秋香";
-
- 11. mp[10000] = "百万富翁的生活";
-
- 12. mp[88888] = "白领的工资底线";
-
- 13.
-
- 14. if(mp.find(10000) != mp.end())
-
- 15. {
-
- 16.
-
- 17. }
-
- 18. }
这也是比较简单的,和map的使用方法一样。这时你或许会问?hash函数和比较函数呢?不是要指定么?你说对了,但是在你没有指定hash函数和比较函数的时候,你会有一个缺省的函数,看看hash_map的声明,你会更加明白。下面是SGI STL的声明。
- template <class _Key, class _Tp, class _HashFcn = hash<_Key>,
-
- 2. class _EqualKey = equal_to<_Key>,
-
- 3. class _Alloc = __STL_DEFAULT_ALLOCATOR(_Tp) >
-
- 4. class hash_map
-
- 5. {
-
- 6. ...
-
- 7. }
也就是说,在上例中,有以下等同关系
- ...
-
- 2. hash_map<int, string> mymap;
-
- 3.
-
- 4. hash_map<int, string, hash<int>, equal_to<int> > mymap;
Alloc这里不需要关注太多的。
2.2 hash_map的hash函数
hash<int>到底是什么样子?看看源代码:
- struct hash<int> {
-
- 2. size_t operator()(int __x) const { return __x; }
-
- 3. };
原来是个函数对象。咋SGI STL中,提供了以下hash函数:
- struct hash<char*>
-
- 2. struct hash<const char*>
-
- 3. struct hash<char>
-
- 4. struct hash<unsigned char>
-
- 5. struct hash<signed char>
-
- 6. struct hash<short>
-
- 7. struct hash<unsigned short>
-
- 8. struct hash<int>
-
- 9. struct hash<unsigned int>
-
- 10. struct hash<long>
-
- 11. struct hash<unsigned long>
也就是说,如果你的可以key是以上类型中的一种,你都可以使用缺省的hash函数。当然你自己也可以定义自己的hash函数。对于自定义变量,例如对于string,就必须自定义hash函数。例如:
- struct str_hash{
-
- 2. size_t operator()(const string& str) const
-
- 3. {
-
- 4. unsigned long __h = 0;
-
- 5. for (size_t i = 0 ; i < str.size() ; i ++)
-
- 6. __h = 5*__h + str[i];
-
- 7. return size_t(__h);
-
- 8. }
-
- 9. };
-
- 10.
-
- 11. struct str_hash{
-
- 12. size_t operator()(const string& str) const
-
- 13. {
-
- 14. return return __stl_hash_string(str.c_str());
-
- 15. }
-
- 16. };
在声明自己的hash函数时要注意以下几点:
1、使用struct,然后重载operator();
2、返回size_t;
3、参数是你要hash的key的类型;
4、函数是const类型的;
如这些比较难记,最简单的方法就是照猫画虎,找一个函数改改就是了。
现在开始对开头的“岳不群”进行哈希化,直接替换成下面的声明即可。
- map<string, string> namemap;
-
- 2.
-
- 3. hash_map<string, string, str_hash> namemap;
其他用法都不用管。当然不要忘记了str_hash的声明以及头文件改为hash_map。
你或许会问:比较函数呢?别急,这里就开始介绍hash_map中的比较函数。
2.3 hash_map的比较函数
在map中的比较函数,需要提供less函数。如果没有提供,缺省的也是less<key>。在hash_map中,要比较桶内的数据和key是否相等,因此需要的是是否等于的函数equal_to<key>。先看看equal_to的源码:
-
-
- 2.
-
- 3. template <class _Arg1, class _Arg2, class _Result>
-
- 4. struct binary_function {
-
- 5. typedef _Arg1 first_argument_type;
-
- 6. typedef _Arg2 second_argument_type;
-
- 7. typedef _Result result_type;
-
- 8. };
-
- 9.
-
- 10. template <class _Tp>
-
- 11. struct equal_to : public binary_function<_Tp,_Tp,bool>
-
- 12. {
-
- 13. bool operator()(const _Tp& __x, const _Tp& __y) const { return __x == __y; }
-
- 14. };
如果你使用一个自定义的数据类型,如struct mystruct或者const char*的字符串,如何使用比较函数?使用比较函数,有两种方法。第一种是:重载==操作符,利用equal_to;看看下面的例子:
- struct mystruct{
-
- 2. int iID;
-
- 3. int len;
-
- 4. bool operator==(const mystruct & my) const{
-
- 5. return (iID==my.iID) && (len==my.len) ;
-
- 6. }
-
- 7. };
这样,就可以使用equal_to<mystruct>作为比较函数了。另一种方法就是使用函数对象。自定义一个比较函数体:
- struct compare_str{
-
- 2. bool operator()(const char* p1, const char*p2) const{
-
- 3. return strcmp(p1,p2)==0;
-
- 4. }
-
- 5. };
有了compare_str,就可以使用hash_map了。
- typedef hash_map<const char*, string, hash<const char*>, compare_str> StrIntMap;
-
- 2. StrIntMap namemap;
-
- 3. namemap["岳不群"]="华山派掌门人,人称君子剑";
-
- 4. namemap["张三丰"]="武当掌门人,太极拳创始人";
-
- 5. namemap["东方不败"]="第一高手,葵花宝典";
2.4 hash_map函数
hash_map的函数和map的函数差不多。这里主要介绍几个常用函数。
1、hash_map(size_type n) 如果讲究效率,这个参数是必须要设置的。n 主要用来设置hash_map 容器中hash桶的个数。桶个数越多,hash函数发生冲突的概率就越小,重新申请内存的概率就越小。n越大,效率越高,但是内存消耗也越大。
2、const_iterator find(const key_type& k) const. 用查找,输入为键值,返回为迭代器。
3、data_type& operator[](const key_type& k) . 这是我最常用的一个函数。因为其特别方便,可像使用数组一样使用。不过需要注意的是,当你使用[key ]操作符时,如果容器中没有key元素,这就相当于自动增加了一个key元素。因此当你只是想知道容器中是否有key元素时,你可以使用find。如果你 希望插入该元素时,你可以直接使用[]操作符。
4、insert 函数。在容器中不包含key值时,insert函数和[]操作符的功能差不多。但是当容器中元素越来越多,每个桶中的元素会增加,为了保证效率, hash_map会自动申请更大的内存,以生成更多的桶。因此在insert以后,以前的iterator有可能是不可用的。
5、erase 函数。在insert的过程中,当每个桶的元素太多时,hash_map可能会自动扩充容器的内存。但在sgi stl中是erase并不自动回收内存。因此你调用erase后,其他元素的iterator还是可用的。
3、相关hash容器
hash容器除了hash_map外,还有hash_set、hash_multimap、hash_multiset,这些容器使用起来和set、multimap、multiset的区别于hash_map和map的区别一样。
4、其他
这里列几个问题,应该对你理解和使用hash_map比较有帮助。
4.1 hash_map和map的区别在哪里?
(1)构造函数 hash_map需要hash函数,等于函数;map只需要比较函数(小于函数)。
(2)存储结构 hash_map采用hash表存储,map一般采用红黑树实现。因此内存数据结构是不一样的。
4.2 什么时候需要使用hash_map,什么时候需要map?
总体来说,hash_map 查找速度会比map快,而且查找速度基本和数据数据量大小,属于常数级别;而map的查找速度是log(n)级别。并不一定常数就比log(n)小, hash还有hash函数的耗时,明白了吧,如果你考虑效率,特别是在元素达到一定数量级时,考虑考虑hash_map。但若你对内存使用特别严格,希望 程序尽可能少消耗内存,那么一定要小心,hash_map可能会让你陷入尴尬,特别是当你的hash_map对象特别多时,你就更无法控制了,而且 hash_map的构造速度较慢。
现在知道如何选择了吗?权衡三个因素: 查找速度, 数据量, 内存使用。
4.3 如何在hash_map中加入自己定义的类型?
你只需要做两件事情:定于hash函数、定义等于比较函数。下面的代码是一个例子:
- #include <hash_map>
-
- 2. #include <string>
-
- 3. #include <iostream>
-
- 4.
-
- 5. using namespace std;
-
- 6.
-
- 7. class ClassA{
-
- 8. public:
-
- 9. ClassA(int a):c_a(a){}
-
- 10. int getvalue()const { return c_a;}
-
- 11. void setvalue(int a){c_a;}
-
- 12. private:
-
- 13. int c_a;
-
- 14. };
-
- 15.
-
- 16.
-
- 17. struct hash_A{
-
- 18. size_t operator()(const class ClassA & A)const{
-
- 19.
-
- 20. return A.getvalue();
-
- 21. }
-
- 22. };
-
- 23.
-
- 24.
-
- 25. struct equal_A{
-
- 26. bool operator()(const class ClassA & a1, const class ClassA & a2)const{
-
- 27. return a1.getvalue() == a2.getvalue();
-
- 28. }
-
- 29. };
-
- 30.
-
- 31. int main()
-
- 32. {
-
- 33. hash_map<ClassA, string, hash_A, equal_A> hmap;
-
- 34. ClassA a1(12);
-
- 35. hmap[a1]="I am 12";
-
- 36. ClassA a2(198877);
-
- 37. hmap[a2]="I am 198877";
-
- 38.
-
- 39. cout<<hmap[a1]<<endl;
-
- 40. cout<<hmap[a2]<<endl;
-
- 41. return 0;
-
- 42. }
4.4 如何用hash_map替换程序中已有的map容器?
这个很容易,但需要你有良好的编程风格。建议你尽量使用typedef来定义你的类型:
typedef map<Key, Value> KeyMap;
当你希望使用hash_map来替换的时候,只需要修改:
typedef hash_map<Key, Value> KeyMap;
其他的基本不变。当然,你需要注意是否有Key类型的hash函数和比较函数。
4.5 为什么hash_map不是标准的?
具体为什么不 是标准的,我也不清楚,有个解释说在STL加入标准C++之时,hash_map系列当时还没有完全实现,以后应该会成为标准。如果谁知道更合理的解释, 也希望告诉我。但我想表达的是,正是因为hash_map不是标准的,所以许多平台上安装了g++编译器,不一定有hash_map的实现。我就遇到了这 样的例子。因此在使用这些非标准库的时候,一定要事先测试。另外,如果考虑到平台移植,还是少用为佳。
BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
STL是标准C++系统的一组模板类,使用STL模板类最大的好处就是在各种C++编译器上都通用。
在STL模板类中,用于线性数据存储管理的类主要有vector, list, map 等等。本文主要针对map对象,结合自己学习该对象的过程,讲解一下具体用法。本人初学,水平有限,讲解差错之处,请大家多多批评指正。
map对象所实现的功能跟MFC得CMap相似,但是根据一些文章的介绍和论述,MFC CMap在个方面都与STL map有一定的差距,例如不是C++标准,不支持赋值构造,对象化概念不清晰等等。
使用map对象首先要包括头文件,包含语句中必须加入如下包含声明
#include <map>
注意,STL头文件没有扩展名.h
包括头文件后就可以定义和使用map对象了,map对象是模板类,需要关键字和存储对象两个模板参数,例如:
std:map<int, CString> enumMap;
这样就定义了一个用int作为关键字检索CString条目的map对象,std表示命名空间,map对象在std名字空间中,为了方便,在这里我仍然使用了CString类,其实应该使用标准C++的std::string类,我们对模板类进行一下类型定义,这样用的方便,当然,不定义也可以,代码如下:
typedef std:map<int, CString> UDT_MAP_INT_CSTRING;
UDT_MAP_INT_CSTRING enumMap;
如此map对象就定义好了,增加,改变map中的条目非常简单,因为map类已经对[]操作符进行了重载,代码如下:
enumMap[1] = "One";
enumMap[2] = "Two";
.....
enumMap[1] = "One Edit";
或者insert方法
enumMap.insert(make_pair(1,"One"));
返回map中目前存储条目的总数用size()方法:
int nSize = enumMap.size();
查找map中是否包含某个关键字条目用find方法,传入的参数是要查找的key,在我们的例子里,是一个int数据,map中的条目数据是顺序存储的,被称作为一个sequence,在这里需要提到的是begin()和end()两个成员,分别代表map对象中第一个条目和最后一个条目,这两个数据的类型是iterator,iterator被定义为map中条目的类型,查找是否包含某个条目的代码如下:
int nFindKey = 2; //要查找的Key
UDT_MAP_INT_CSTRING::iterator it; //定义一个条目变量(实际是指针)
it = enumMap.find(nFindKey);
if(it == enumMap.end()) {
//没找到
}
else {
//找到
}
//find的时候注意key的数据类型,最好用CString之类的能消除数据类型差异的key,否则可能会出现强制转换后仍找不到的情况。
需要说明的是iterator, begin(), end()是STL模板类的一个通用概念,操作方法也大同小异
通过map对象的方法获取的iterator数据类型是一个std::pair对象,包括两个数据 iterator.first 和 iterator.second 分别代表关键字和存储的数据
移除某个条目用erase() 该成员方法的定义如下
iterator erase(iterator it);
iterator erase(iterator first, iterator last);
size_type erase(const Key& key);
分析一下这三个重载方法定义,大家不用说也能看明白一点点了吧,第一个通过一个条目对象删除,这个对象可以从find之类的方法获得,第二个定义删除一个范围,需要一个起始条目和一个终止条目,第三个通过关键字删除,这个与我们的想法和习惯最接近,代码例子如下:
enumMap.erase(1); //删掉关键字“1”对应的条目
enumMap.erase(enumMap.begin()); //删掉第一个条目
enumMap.erase(enumMap.begin(), enumMap.begin() + 1); //删掉起始的两个条目
呵呵,增删改查都说完了,相信读过本文,map对象也应该会使用了,这些是我1个多星期来钻研的结果,拿出来与大家分享。
最后,还有一个clear(),不用问,全删的时候就不要一个一个erase了,clear()就相当于enumMap.erase(enumMap.begin(), enumMap.end());
map的遍历:
#include<map>
#include<string>
#include<iostream>
using namespace std;
int main()
{
map<string,int> m;
m["a"]=1;
m["b"]=2;
m["c"]=3;
map<string,int>::iterator it;
for(it=m.begin();it!=m.end();++it)
cout<<"key: "<<it->first <<" value: "<<it->second<<endl;
return 0;
}
map<string,int>::iterator it; 定义一个迭代指针it。
it->first 为索引键值,it->second 为值。
在对象中应用时,最好在析构函数中要调用它的clear方法,
例如class a{
map<int,int> m;
~a(){
m.clear();
}
}
CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC
1.map定义
map是键-值对的集合。map类型通常可以理解为关联数组:可使用键作为下标来获取一个值,正如内置数组类型一样。而关联的本质在于元素的值与某个特定的键相关联,而并非通过元素在数组中的位置来获取。
<1>map模板原型:
template < class Key, class T, class Compare = less<Key>,
class Allocator = allocator<pair<const Key,T> > > class map;
key:关键值的类型。在map对象中的每个元素是通过该关键值唯一确定元素的。
T:映射值的类型。在map中的每个元素是用来储存一些数据作为其映射值。
compare:Comparison类:A类键的类型,它有两个参数,并返回一个bool。表达comp(A,B),comp是这比较类A和B是关键值的对象,应返回true,如果是在早先的立场比B放置在一个严格弱排序操作。这可以是一个类实现一个函数调用运算符或一个函数的指针(见一个例子构造)。默认的对于<KEY>,返回申请小于操作符相同的默认值(A <B)。 Map对象使用这个表达式来确定在容器中元素的位置。以下这个规则在任何时候都排列在map容器中的所有元素。
Allocator:用于定义存储分配模型分配器对象的类型。默认情况下,分配器类模板,它定义了最简单的内存分配模式,是值独立的
<2>map模板参数
map<Key, Data, Compare, Alloc>
<3>map的详细用法可参考:http://blog.csdn.net/bat603/article/details/1456141
2.map的实现机制
C++ STL 之所以得到广泛的赞誉,也被很多人使用,不只是提供了像vector, string, list等方便的容器,更重要的是STL封装了许多复杂的数据结构算法和大量常用数据结构操作。vector封装数组,list封装了链表,map和 set封装了二叉树等,在封装这些数据结构的时候,STL按照程序员的使用习惯,以成员函数方式提供的常用操作,如:插入、排序、删除、查找等。让用户在 STL使用过程中,并不会感到陌生。
C++ STL中标准关联容器set, multiset, map, multimap内部采用的就是一种非常高效的平衡检索二叉树:红黑树,也成为RB树(Red-Black Tree)。RB树的统计性能要好于一般的平衡二叉树(有些书籍根据作者姓名,Adelson-Velskii和Landis,将其称为AVL-树),所以被STL选择作为了关联容器的内部结构。本文并不会介绍详细AVL树和RB树的实现以及他们的优劣,关于RB树的详细实现参看红黑树: 理论与实现(理论篇)。本文针对开始提出的几个问题的回答,来向大家简单介绍map和set的底层数据结构。
<1>为何map和set的插入删除效率比用其他序列容器高?
之所以效率高,是因为对于关联容器来说,不需要做内存拷贝和内存移动。map和set容器内所有元素都是以节点的方式来存储,其节点结构和链表差不多,指向父节点和子节点。结构图可能如下:
A
/ \
B C
/ \ / \
D E F G
|
因此插入的时候只需要稍做变换,把节点的指针指向新的节点就可以了。删除的时候类似,稍做变换后把指向删除节点的指针指向其他节点就OK了。这里的一切操作就是指针换来换去,和内存移动没有关系。
<2>为何每次insert之后,以前保存的iterator不会失效?
看见了上面答案的解释,你应该已经可以很容易解释这个问题。iterator这里就相当于指向节点的指针,内存没有变,指向内存的指针怎么会失效呢(当然 被删除的那个元素本身已经失效了)。相对于vector来说,每一次删除和插入,指针都有可能失效,调用push_back在尾部插入也是如此。因为为了保证内部数据的连续存放,iterator指向的那块内存在删除和插入过程中可能已经被其他内存覆盖或者内存已经被释放了。即使时push_back的时 候,容器内部空间可能不够,需要一块新的更大的内存,只有把以前的内存释放,申请新的更大的内存,复制已有的数据元素到新的内存,最后把需要插入的元素放 到最后,那么以前的内存指针自然就不可用了。特别时在和find等算法在一起使用的时候,牢记这个原则:不要使用过期的iterator。
<3>为何map和set不能像vector一样有个reserve函数来预分配数据?
究其原理来说时,引起它的原因在于在map和set内部存储的已经不是元素本身了,而是包含元素的节点。也就是说map内部使用的Alloc并不是map<Key, Data, Compare, Alloc>声明的时候从参数中传入的Alloc。例如:
map<int, int, less<int>, Alloc<int> > intmap;
这时候在intmap中使用的allocator并不是Alloc<int>, 而是通过了转换的Alloc,具体转换的方法时在内部通过Alloc<int>::rebind重新定义了新的节点分配器,详细的实现参看彻底学习STL中的Allocator。其实你就记住一点,在map和set内面的分配器已经发生了变化,reserve方法你就不要奢望了。
<4>当数据元素增多时(10000和20000个比较),map和set的插入和搜索速度变化如何?
在map和set中查找是使用二分查找,也就是说,如果有16个元素,最多需要比较4次就能找到结 果,有32个元素,最多比较5次。那么有10000个呢?最多比较的次数为log10000,最多为14次,如果是20000个元素呢?最多不过15次。 看见了吧,当数据量增大一倍的时候,搜索次数只不过多了1次,多了1/14的搜索时间而已。你明白这个道理后,就可以安心往里面放入元素了。
最后,对于map和set Winter还要提的就是它们和一个c语言包装库的效率比较。在许多unix和linux平台下,都有一个库叫isc,里面就提供类似于以下声明的函数:
void
tree_init(
void
**tree);
void
*tree_srch(
void
**tree,
int
(*compare)(),
void
*data);
void
tree_add(
void
**tree,
int
(*compare)(),
void
*data,
void
(*del_uar)());
int
tree_delete(
void
**tree,
int
(*compare)(),
void
*data,
void
(*del_uar)());
int
tree_trav(
void
**tree,
int
(*trav_uar)());
void
tree_mung(
void
**tree,
void
(*del_uar)());
|
许多人认为直接使用这些函数会比STL map速度快,因为STL map中使用了许多模板什么的。其实不然,它们的区别并不在于算法,而在于内存碎片。如果直接使用这些函数,你需要自己去new一些节点,当节点特别多, 而且进行频繁的删除和插入的时候,内存碎片就会存在,而STL采用自己的Allocator分配内存,以内存池的方式来管理这些内存,会大大减少内存碎 片,从而会提升系统的整体性能。本文原作者在自己的系统中做过测试,把以前所有直接用isc函数的代码替换成map,程序速度基本一致。当时间运行很长时间后(例如后台服务程序),map的优势就会体现出来。从另外一个方面讲,使用map会大大降低你的编码难度,同时增加程序的可读性。何乐而不为?
DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD
设要存储对象的个数为num, 那么我们就用len个内存单元来存储它们(len>=num); 以每个对象ki的关键字为自变量,用一个函数h(ki)来映射出ki的内存地址,也就是ki的下标,将ki对象的元素内容全部存入这个地址中就行了。这个就是Hash的基本思路。
Hash为什么这么想呢?换言之,为什么要用一个函数来映射出它们的地址单元呢?
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE
#include<stdio.h>
#include<stdlib.h>
typedef char datatype;
typedef struct node{
datatype data;
struct node *next;
} listnode;
typedef listnode *linklist;
listnode *p;
/* 创建链表,从表头插入新元素 */
linklist createlist(void)
{
char ch;
linklist head;
listnode *p;
head = NULL;/*初始化为空*/
printf("请输入字符序列: \n");
ch = getchar();
while (ch != '\n'){
p = (listnode*)malloc(sizeof(listnode));/*分配空间*/
p->data = ch;/*数据域赋值*/
p->next = head;/*指定后继指针*/
head = p;/*head指针指定到新插入的结点上*/
ch = getchar();
}
return head;
}
/* 删除第i个节点 */
int deletelist(linklist *head, int i)
{
int j = 1;
listnode * p, *r;
p = *head;
if (i == 1) //删除第1个结点
{
*head = p->next;
free(p);
return 1;
}
//删除第i个结点(i>1),寻找第i-1个结点
while (p && j<(i - 1))
{
p = p->next;
++j;
}
if (!p || j>(i - 1))
{
return -1;
}
r = p->next;
p->next = r->next;
free(r);
return 1;
}
int main()
{
linklist list, head;
int i;
/* 创建链表,从表头插入新元素 */
list = createlist();
head = list;
printf("在删除前,输出链表中的元素\n");
do
{
printf("%c", head->data);
head = head->next;
} while (head != NULL);
printf("\n请输入要删除的结点位置(n>=1):");
scanf("%d", &i);
/* 删除第i个节点后的节点 */
deletelist(&list, i);
printf("在删除后,输出链表中的元素\n");
do
{
printf("%c", list->data);
list = list->next;
} while (list != NULL);
printf("\n");
}
=============================》
链表元素的删除
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE
结合http://www.cnblogs.com/feichengwulai/articles/3523905.html这篇文章一起记忆!!!
@哈希表的实际应用
1,Sql中的索引,就是通过哈希表实现的。加大了数据存储空间,但查询速度快了很多!!!
---具体可以查哈希表的应用!!!
@什么是哈希表?
1,google搜索到的头条:
散列表(也叫哈希表),是根据关键码值直接进行访问的数据结构,也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
我觉得这个解释太含糊,想要整明白哈希表,那就得明白哈希表到底有什么样的优势。
数据结构中,有个时间算法复杂度O(n)的概念来衡量某种算法在时间效率上的优劣。哈希表的理想算法复杂度为O(1),也就是说利用哈希表查找某个值,系统所使用的时间在理想情况下为定值,这就是它的优势。那么哈希表是如何做到这一点的呢?
我们定义一个很大的有序数组,想要得到位于该数组第n个位置的值,它的算法复杂度为O(1)。哈希表利用哈希函数将需要存储的内容的关键值转换为这个有序数组中的某个值,在被存储内容和有序数组之间建立了映射关系。这样,下次我们对这个值进行查找时只要使用同一个哈希函数对关键值进行转换,找到这个数组值就可以了。
如果还没有明白是怎么回事的话,那我们来举个例子。假设我们要做个存储结构,需要存储下来三国中的人物,以及他们的详细信息。我们用他们的名字来作为存储 的关键值,例如:刘备,曹操,孙权,关羽,张飞……等等。这个时候我们如果想用一般的方法来查找这些英雄豪杰,需要遍历整个存储空间,如果这些英雄豪杰一 共有n个,那么这时候的时间算法复杂度为O(n)。显然如果n值很大,每次想要找到某个英雄就需要比较长的时间。
此时我们先定义一个大的有序结构数组HashValue[m],用来存放各位英雄豪杰的信息(value值,刘备,曹操...等信息)。然后编写一个哈希函数ChangeToHashValue (name),函数的具体内容就不细说了,反正这个函数会将这些做为关键值的名字转换为HashValue[m]中的某个下标值x。然后可以将英雄的信息放进HashValue[x]中去。这样,可以将所有英雄的信息存储起来。当查询的时候再使用哈希函数ChangeToHashValue(name)得到这个下标值,这样就很容易得到了这个英雄的信息。例如:ChangeToHashValue(刘备)为10,那么就将刘备存储到HashValue [10]里面。当查询的时候再次使用ChangeToHashValue(刘备)得到10,这个时候我们就可以很容易找到刘备的所有信息。在实际应用中如 果我们想把所有的英雄豪杰都存储进系统时,需要定义m>n。就是数组的大小要大于需要存储的信息量,所以说哈希表是一个以空间换取时间的数据结构。
这个时候问题来了,出现了这种情况ChangeToHashValue(关羽)和ChangeToHashValue(张飞)得到的值是一样的,都是250,我们岂不是在存储过程中会遇到麻烦,怎么安排他们二位的地方呢(总不能让二位打一架,谁赢了谁呆在那吧),这就需要一个解决冲突的方法。当遇到这 种情况时我们可以这样处理,先存储好了关羽,当张飞进入系统时会发现关羽已经是250了,那咱就加一位,251得了,这不就解决了。我们查找张飞的时候也 是,一看250不是张飞,那就加个1,就找到了。这时还存在一个问题。直接用ChangeToHashValue(赵云)为251,张飞已经早早占了他的 地方,那就再加1存到252呗。呵呵,这时我们会发现,当哈希函数冲突发生的机率很高时,可能会有一群英雄豪杰在250这个值后面扎堆排队。要命的是查找 的时候,时间算法复杂度早已不是O(1)了(所以我们说理想情况下哈希表的时间算法复杂度为O(1))。
这就是说哈希函数的编写是哈希表的一个关键问题,会涉及到一个存储值在哈希表中的统计分布。如果哈希函数已经定义好了,冲突的解决就成为了改变系统性能的关键因素。其实还有很多种方法来解决冲突情况下的存储和查找问题,不一定非要线性向后排队,如果有好的哈希表冲突的解决方法也能很大程度上提高系统的效 率。
好了,写到这里,哈希表的概念应该搞清楚了吧。今天咱这也是现学现卖,其实我还没有使用过这个数据结构。有不对的地方还请高手指出来,耽误了我自己不怕,免得误导了别人
@总结:
1,哈希表,就是声明一个数组来存放value值(存储的内容),然后声明一个哈希函数,哈希表利用哈希函数将需要存储的内容的关键值转换为这个有序数组中的某个值,在被存储内容和有序数组之间建立了映射关系,这样,下次我们对这个值进行查找时只要使用同一个哈希函数对存储的内容的关键值进行转换,找到这个数组值就可以了。
@值---hash表是一种映射关系,查找值时,先将值经过hash计算,计算后的值在hash表中查找,查找到了,就找到了对应的值。
2,前提是将所有存储的内容计算成唯一的hash值,这样对比才有效。
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
1.插入排序:每次将一个待排的记录插入到前面的已经排好的队列中的适当位置。
①.直接插入排序
直接排序法在最好情况下(待排序列已按关键码有序),每趟排序只需作1次比较而不需要移动元素。所以n个元素比较次数为n-1,移动次数0。
最差的情况下(逆序),其中第i个元素必须和前面的元素进行比较i次,移动个数i+1,所以总共的比较次数 比较多,就不写出来了
总结:是一种稳定的排序方法,时间复杂度O(n^2),排序过程中只要一个辅助空间,所以空间复杂度O(1)
②.希尔排序
缩小增量排序,对直接插入排序的一种改进
分组插入方法。
总结:是一种不稳定的排序方法,时间复杂度O(n^1.25),空间复杂度O(1)
2.交换排序
①.冒泡排序
最好的情况下,就是正序,所以只要比较一次就行了,复杂度O(n)
最坏的情况下,就是逆序,要比较n^2次才行,复杂度O(n^2)
总结:稳定的排序方法,时间复杂度O(n^2),空间复杂度O(1),当待排序列有序时,效果比较好。
②.快速排序
通过一趟排序将待排的记录分割成独立的两部分,其中一部分记录的关键字均比另一个部分的关键字小,然后再分别对这两个部分记录继续进行排序,以达到整个序列有效。
总结:在所有同数量级O(nlogn)的排序方法中,快速排序是性能最好的一种方法,在待排序列无序时最好。算法的时间复杂度是O(nlogn),最坏的时间复杂度O(n^2),空间复杂度O(nlogn)
3.选择排序
①.直接选择排序
和序列的初始状态无关
总结:时间复杂度O(n^2),无论最好还是最坏
②.堆排序
直接选择排序的改进
总结:时间复杂度O(nlogn),无论在最好还是最坏情况下都是O(nlogn)
4.归并排序
总结:时间复杂度O(nlogn),空间复杂度O(n)
5.基数排序
按组成关键字的各个数位的值进行排序,是分配排序的一种。不需要进行排码值间的比较就能够进行排序。
总结:时间复杂度O(d(n+rd))
总总结:
n比较小的时候,适合 插入排序和选择排序
基本有序的时候,适合 直接插入排序和冒泡排序
n很大但是关键字的位数较少时,适合 链式基数排序
n很大的时候,适合 快速排序 堆排序 归并排序
无序的时候,适合 快速排序
稳定的排序:冒泡排序 插入排序 归并排序 基数排序
复杂度是O(nlogn):快速排序 堆排序 归并排序
辅助空间(大 次大):归并排序 快速排序
好坏情况一样:简单选择(n^2),堆排序(nlogn),归并排序(nlogn)
最好是O(n)的:插入排序 冒泡排序
排序法 | 最差时间分析 | 平均时间复杂度 | 稳定度 | 空间复杂度 |
冒泡排序 | O(n2) | O(n2) | 稳定 | O(1) |
快速排序 | O(n2) | O(n*log2n) | 不稳定 | O(log2n)~O(n) |
选择排序 | O(n2) | O(n2) | 稳定 | O(1) |
二叉树排序 | O(n2) | O(n*log2n) | 不一顶 | O(n) |
插入排序 | O(n2) | O(n2) | 稳定 | O(1) |
堆排序 | O(n*log2n) | O(n*log2n) | 不稳定 | O(1) |
希尔排序 | O | O | 不稳定 | O(1) |
TTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTT
访问数组中第 n 个数据的时间花费是 O(1) 但是要在数组中查找一个指定的数据则是 O(N)。当向数组中插入或者删除数据的时候,最好的情况是在数组的末尾进行操作,时间复杂度是O(1) ,但是最坏情况是插入或者删除第一个数据,时间复杂度是 O(N) 。在数组的任意位置插入或者删除数据的时候,后面的数据全部需要移动,移动的数据还是和数据个数有关所以总体的时间复杂度仍然是 O(N) 。
在链表中查找第 n 个数据以及查找指定的数据的时间复杂度是 O(N) ,但是插入和删除数据的时间复杂度是 O(1) ,因为只需要调整指针就可以:
ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ
对关键码序列(66,13,51,76,81,26,57,69,23)进行快速排序。
求第一趟划分后的结果。
关键码序列递增。
以第一个元素为划分基准。
【主要方法步骤】如下:
将两个指针i,j分别指向表的起始和最后的位置。
反复操作以下两步:
(1)j逐渐减小,并逐次比较j指向的元素和目标元素的大小,若p(j)<T则交换位置。
(2)i逐渐增大,并逐次比较i指向的元素和目标元素的大小,若p(i)>T则交换位置。
直到i,j指向同一个值,循环结束。
工具/原料
对关键码序列(66,13,51,76,81,26,57,69,23)进行快速排序。
方法/步骤
-
首先设置两个变量i,j。
分别指向序列的首尾元素。
-
该例子是以第一个元素为基准,从小到大进行排列。
让j从后向前进行查询,直到找到第一个小于66的元素。
则将最后一个j指向的数23,和i指向的66交换位置。
然后将i从前向后查询,直到找到第一个大于66的元素76.
-
将76和66位置互换。
让j从后向前进行查询,直到找到第一个小于66的元素57
-
-
然后将i从前向后查询,直到找到第一个大于66的元素81.
-
将81和66交换位置。
让j从后向前进行查询,直到找到第一个小于66的元素26
-
将26和66交换位置。
此时i,j都同时指向了目标元素66.
查找停止。
所得到的序列就是第一趟排序的序列
HEHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHE八大排序算法http://blog.csdn.net/yxb_yingu/article/list/2
HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH
//冒泡排序
public static void bubbleSort(int[] array){
boolean isExchange = false;
for (int i = 0; i < array.length - 1; i++) {//最大趟数length-1趟
for (int j = 0; j < array.length - 1 - i; j++) {//每次最大数放到后面,则每次减少比较次数
if (array[j]> array[j +1]) {//相邻两个数比较,将较大的放到后面
array[j] ^= array[j + 1];
array[j + 1] ^= array[j];
array[j] ^= array[j + 1];
isExchange = true;
}
}
if (!isExchange) {//如果没有交换,说明中途已经排序完成,可以终止循环,直接退出,优化算法
break;
}
}
}
//选择排序
public static void chooseSort(int[] array){
for (int i = 0; i < array.length - 1; i++) {//循环length - 1趟
int minIndex = i;//将本趟的第一个数暂时定义为最小值,则其下标为最小值下标
for (int j = i; j < array.length; j++) {//每次隔过前面的数,从第i个数开始循环;
if (array[minIndex] > array[j]) {
minIndex = j;//拿最小数跟后面的所有数比较,发现有比它小的,记录下标;
}
}
if (minIndex != i) {//如果最小数不是本身,说明找到了另外的最小数,通过交换将这个最小数放到本趟循环的最前面;
array[i] ^= array[minIndex];//通过异或运算交换两个数的数值;
array[minIndex] ^= array[i];
array[i] ^= array[minIndex];
}
}
}
//直接插入排序
public static void insertSort(int[] array){
for (int i = 1; i < array.length; i++) {
int temp = array[i];//取出本趟第一个数字作为基准
int j = i - 1;
while (j >= 0 && temp < array[j]) {//从现在的位置往前依次寻找
array[j + 1] = array[j];
j --;
}
array[j + 1] = temp;//找到合适的位置插入
}
}
//快速排序主方法
public static void quickSort(int[] array, int low, int high){
if (low < high) {
int mid = findMid(array, low, high);//将第一个数作为基准,将数组分成前后两部分
quickSort(array, low, mid - 1);//递归调用将左半部分再一次往下分,直至左半部分完成排序
quickSort(array, mid + 1, high);//递归调用将右半部分再一次往下分,直至右半部分完成排序
}
}
//将第一个数作为基准,将数组分成前后两部分--填坑法
public static int findMid(int[] array, int low, int high){
int temp = array[low];//将第一个数作为基准拿出,拿出留下一个坑
while (low < high) {
while (low < high && temp <= array[high]) {//从右开始找
high --;
}
if (low < high) {//发现比基准数字小的,就把它填到之前基准数字留下的坑里
array[low ++] = array[high];
}
while (low < high && temp >= array[low]) {//从左开始找
low ++;
}
if (low < high) {//发现比基准数字大的,就把它填到之前被拿出数字留下的坑里
array[high --] = array[low];
}
}
array[low] = temp;//将基准数字放到中间将左右分成两部分
return low;//返回基准数字的下标
}
//希尔排序
public static void shellSort(int[] array){
int d = array.length / 2;
while(d >= 1){
//采用插入排序的思想将增量间隔的数字排序,当增量为1时,即跟直接插入排序相同
for (int i = d; i < array.length; i++) {
int temp = array[i];
int j = i - d;
while(j >= 0 && temp < array[j]){
array[j + d] = array[j];
j -= d;
}
array[j + d] = temp;
}
d /= 2;//增量每次减小
}
}
//归并排序主方法
public static void mergeSort(int[] array){
//拿出三个list作为工具完成归并
ArrayList<Integer> list1 = new ArrayList<Integer>();
ArrayList<Integer> list2 = new ArrayList<Integer>();
ArrayList<Integer> list3 = new ArrayList<Integer>();
for (int gap = 1; gap < array.length ; gap *= 2) {//运行趟数,每次将被归并的数组都会变大,直至完全归并为一个数组
for (int i = 0; i < array.length; i++) {
if (list1.size() < gap) {//往第一个list里加数
list1.add(array[i]);
}else if (list2.size() < gap){//往第二个list里加数
list2.add(array[i]);
}
if ((list1.size() == gap && list2.size() == gap) || (list1.size() == gap && list2.size() < gap && i == array.length - 1)) {
merger(list1, list2, list3);//将前面两个list里的数归并后放入第三个list
list1.clear();//清空第一个list循环使用
list2.clear();//清空第二个list循环使用
}
}
//将已达到目的的第三个list里的数复制到数组里,完成一趟排序;
for (int i = 0; i < array.length; i++) {
array[i] = list3.get(i);
}
list3.clear();
}
}
//将两个有序的数组归并成一个有序的数组的方法:传入list1,list2,最后得到list3
public static void merger(ArrayList<Integer> list1, ArrayList<Integer> list2, ArrayList<Integer> list3){
int m = 0;
int n = 0;
while(m < list1.size() && n < list2.size()){
while (m < list1.size() && n < list2.size() && list1.get(m) < list2.get(n)) {
list3.add(list1.get(m));
m ++;
}
while(m < list1.size() && n < list2.size() && list1.get(m) >= list2.get(n)){
list3.add(list2.get(n));
n ++;
}
}
while (m < list1.size()) {
list3.add(list1.get(m));
m ++;
}
while (n < list2.size()) {
list3.add(list2.get(n));
n ++;
}
}
希尔排序
![](https://img-blog.csdn.net/20180122151207665)
归并排序
![](https://img-blog.csdn.net/20180122151249027)
快速排序
![](https://img-blog.csdn.net/20180122151309299)
ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ
最优情况下时间复杂度
快速排序最优的情况就是每一次取到的元素都刚好平分整个数组(很显然我上面的不是);
此时的时间复杂度公式则为:T[n] = 2T[n/2] + f(n);T[n/2]为平分后的子数组的时间复杂度,f[n] 为平分这个数组时所花的时间;
下面来推算下,在最优的情况下快速排序时间复杂度的计算(用迭代法):
T[n] = 2T[n/2] + n ----------------第一次递归
令:n = n/2 = 2 { 2 T[n/4] + (n/2) } + n ----------------第二次递归
= 2^2 T[ n/ (2^2) ] + 2n
令:n = n/(2^2) = 2^2 { 2 T[n/ (2^3) ] + n/(2^2)} + 2n ----------------第三次递归
= 2^3 T[ n/ (2^3) ] + 3n
......................................................................................
令:n = n/( 2^(m-1) ) = 2^m T[1] + mn ----------------第m次递归(m次后结束)
当最后平分的不能再平分时,也就是说把公式一直往下跌倒,到最后得到T[1]时,说明这个公式已经迭代完了(T[1]是常量了)。
得到:T[n/ (2^m) ] = T[1] ===>> n = 2^m ====>> m = logn;
T[n] = 2^m T[1] + mn ;其中m = logn;
T[n] = 2^(logn) T[1] + nlogn = n T[1] + nlogn = n + nlogn ;其中n为元素个数
又因为当n >= 2时:nlogn >= n (也就是logn > 1),所以取后面的 nlogn;
综上所述:快速排序最优的情况下时间复杂度为:O( nlogn )
最差情况下时间复杂度
最差的情况就是每一次取到的元素就是数组中最小/最大的,这种情况其实就是冒泡排序了(每一次都排好一个元素的顺序)
这种情况时间复杂度就好计算了,就是冒泡排序的时间复杂度:T[n] = n * (n-1) = n^2 + n;
综上所述:快速排序最差的情况下时间复杂度为:O( n^2 )
平均时间复杂度
快速排序的平均时间复杂度也是:O(nlogn)
空间复杂度
其实这个空间复杂度不太好计算,因为有的人使用的是非就地排序,那样就不好计算了(因为有的人用到了辅助数组,所以这就要计算到你的元素个数了);我就分析下就地快速排序的空间复杂度吧;
首先就地快速排序使用的空间是O(1)的,也就是个常数级;而真正消耗空间的就是递归调用了,因为每次递归就要保持一些数据;
最优的情况下空间复杂度为:O(logn) ;每一次都平分数组的情况
最差的情况下空间复杂度为:O( n ) ;退化为冒泡排序的情况
还有个问题就是怎么取哨兵元素才能不会让这个算法退化到冒泡排序,想想了还是算了吧,越深入研究就会有越多感兴趣的问题,而我又不是搞算法分析的,所以就先这样吧。
ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ
在二叉排序树中,每个结点的值均大于其左子树上所有结点的值,小于其右子树上所有结点的值,对二叉排序树进行中序遍历得到一个有序序列。所以,二叉排序树是结点之间满足一定次序关系的二叉树;
堆是一个完全二叉树,并且每个结点的值都大于或等于其左右孩子结点的值(这里的讨论以大根堆为例),所以,堆是结点之间满足一定次序关系的完全二叉树。
具有n个结点的二叉排序树,其深度取决于给定集合的初始排列顺序,最好情况下其深度为log n(表示以2为底的对数),最坏情况下其深度为n;
具有n个结点的堆,其深度即为堆所对应的完全二叉树的深度log n 。
在二叉排序树中,某结点的右孩子结点的值一定大于该结点的左孩子结点的值;在堆中却不一定,堆只是限定了某结点的值大于(或小于)其左右孩子结点的值,但没有限定左右孩子结点之间的大小关系。
在二叉排序树中,最小值结点是最左下结点,其左指针为空;最大值结点是最右下结点,其右指针为空。在大根堆中,最小值结点位于某个叶子结点,而最大值结点是大根堆的堆顶(即根结点)。
二叉排序树是为了实现动态查找而设计的数据结构,它是面向查找操作的,在二叉排序树中查找一个结点的平均时间复杂度是O(log n);
堆是为了实现排序而设计的一种数据结构,它不是面向查找操作的,因而在堆中查找一个结点需要进行遍历,其平均时间复杂度是O(n)。
ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ
1,红黑树与平衡二叉树区别?
2,操作系统中, 信号量与互斥锁使用与区别?
“信号量用在多线程多任务同步的,一个线程完成了某一个动作就通过信号量告诉别的线程,别的线程再进行某些动作(大家都在semtake的时候,就阻塞在 哪里)。而互斥锁是用在多线程多任务互斥的,一个线程占用了某一个资源,那么别的线程就无法访问,直到这个线程unlock,其他的线程才开始可以利用这 个资源。比如对全局变量的访问,有时要加锁,操作完了,在解锁。有的时候锁和信号量会同时使用的”
也就是说,信号量不一定是锁定某一个资源,而是流程上的概念,比如:有A,B两个线程,B线程要等A线程完成某一任务以后再进行自己下面的步骤,这个任务 并不一定是锁定某一资源,还可以是进行一些计算或者数据处理之类。而线程互斥量则是“锁住某一资源”的概念,在锁定期间内,其他线程无法对被保护的数据进 行操作。在有些情况下两者可以互换。
两者之间的区别:
作用域
信号量: 进程间或线程间(linux仅线程间的无名信号量pthread semaphore)
互斥锁: 线程间
上锁时
信号量: 只要信号量的value大于0,其他线程就可以sem_wait成功,成功后信号量的value减一。若value值不大于0,则sem_wait使得线程阻塞,直到sem_post释放后value值加一,但是sem_wait返回之前还是会将此value值减一
互斥锁: 只要被锁住,其他任何线程都不可以访问被保护的资源
以下是信号灯(量)的一些概念:
信号灯与互斥锁和条件变量的主要不同在于”灯”的概念,灯亮则意味着资源可用,灯灭则意味着不可用。如果说后两中同步方式侧重于”等待”操作,即资 源不可用的话,信号灯机制则侧重于点灯,即告知资源可用;
没有等待线程的解锁或激发条件都是没有意义的,而没有等待灯亮的线程的点灯操作则有效,且能保持 灯亮状态。当然,这样的操作原语也意味着更多的开销。
信号灯的应用除了灯亮/灯灭这种二元灯以外,也可以采用大于1的灯数,以表示资源数大于1,这时可以称之为多元灯。
1. 创建和 注销
POSIX信号灯标准定义了有名信号灯和无名信号灯两种,但LinuxThreads的实现仅有无名灯,同时有名灯除了总是可用于多进程之间以外,在使用上与无名灯并没有很大的区别,因此下面仅就无名灯进行讨论。
int sem_init(sem_t *sem, int pshared, unsigned int value)
这是创建信号灯的API,其中value为信号灯的初值,pshared表示是否为多进程共享而不仅仅是用于一个进程。LinuxThreads没有实现 多进程共享信号灯,因此所有非0值的pshared输入都将使sem_init()返回-1,且置errno为ENOSYS。初始化好的信号灯由sem变 量表征,用于以下点灯、灭灯操作。
int sem_destroy(sem_t * sem)
被注销的信号灯sem要求已没有线程在等待该信号灯,否则返回-1,且置errno为EBUSY。除此之外,LinuxThreads的信号灯 注销函数不做其他动作。
sem_destroy destroys a semaphore object, freeing the resources it might hold. No threads should be waiting on the
semaphore at the time sem_destroy is called. In the LinuxThreads implementation, no resources are associated with
semaphore objects, thus sem_destroy actually does nothing except checking that no thread is waiting on the semaphore.
2. 点灯和灭灯
int sem_post(sem_t * sem)
点灯操作将信号灯值原子地加1,表示增加一个可访问的资源。
int sem_wait(sem_t * sem)
int sem_trywait(sem_t * sem)
sem_wait()为等待灯亮操作,等待灯亮(信号灯值大于0),然后将信号灯原子地减1,并返回。sem_trywait()为sem_wait()的非阻塞版,如果信号灯计数大于0,则原子地减1并返回0,否则立即返回-1,errno置为EAGAIN。
3. 获取灯值
int sem_getvalue(sem_t * sem, int * sval)
读取sem中的灯计数,存于*sval中,并返回0。
4. 其他
sem_wait()被实现为取消点。(取消点事什么意思???)
sem_wait is a cancellation point.
取消点的含义:
当用pthread_cancel()一个线程时,这个要求会被pending起来,当被cancel的线程走到下一个cancellation point时,线程才会被真正cancel掉。
而且在支持原子”比较且交换CAS”指令的体系结构上,sem_post()是唯一能用于异步信号处理函数的POSIX异步信号 安全的API。
On processors supporting atomic compare-and-swap (Intel 486, Pentium and later, Alpha, PowerPC, MIPS II, Motorola 68k),
the sem_post function is async-signal safe and can therefore be called from signal handlers. This is the only thread syn-
chronization function provided by POSIX threads that is async-signal safe.
On the Intel 386 and the Sparc, the current LinuxThreads implementation of sem_post is not async-signal safe by lack of
the required atomic operations.
互斥量(Mutex)
互斥量表现互斥现象的数据结构,也被当作二元信号灯。一个互斥基本上是一个多任务敏感的二元信号,它能用作同步多任务的行为,它常用作保护从中断来的临界段代码并且在共享同步使用的资源。
![clip_image001 clip_image001](https://i-blog.csdnimg.cn/blog_migrate/85d17528911abfa9cebfe82aaa9d49fb.gif)
Mutex本质上说就是一把锁,提供对资源的独占访问,所以Mutex主要的作用是用于互斥。Mutex对象的值,只有0和1两个值。这两个值也分别代表了Mutex的两种状态。值为0, 表示锁定状态,当前对象被锁定,用户进程/线程如果试图Lock临界资源,则进入排队等待;值为1,表示空闲状态,当前对象为空闲,用户进程/线程可以Lock临界资源,之后Mutex值减1变为0。
Mutex可以被抽象为四个操作:
- 创建 Create
- 加锁 Lock
- 解锁 Unlock
- 销毁 Destroy
Mutex被创建时可以有初始值,表示Mutex被创建后,是锁定状态还是空闲状态。在同一个线程中,为了防止死锁,系统不允许连续两次对Mutex加锁(系统一般会在第二次调用立刻返回)。也就是说,加锁和解锁这两个对应的操作,需要在同一个线程中完成。
不同操作系统中提供的Mutex函数:
动作\系统 | Win32 | Linyx | Solaris |
创建 | CreateMutex | pthread_mutex_init | mutex_init |
加锁 | WaitForSingleObject | pthread_mutex_lock | mutex_lock |
解锁 | ReleaseMutex | pthread_mutex_unlock | mutex_unlock |
销毁 | CloseHandle | pthread_mutex_destroy | mutex_destroy |
信号量
信号量(Semaphore),有时被称为信号灯,是在多线程环境下使用的一种设施, 它负责协调各个线程, 以保证它们能够正确、合理的使用公共资源。
信号量可以分为几类:
² 二进制信号量(binary semaphore):只允许信号量取0或1值,其同时只能被一个线程获取。
² 整型信号量(integer semaphore):信号量取值是整数,它可以被多个线程同时获得,直到信号量的值变为0。
² 记录型信号量(record semaphore):每个信号量s除一个整数值value(计数)外,还有一个等待队列List,其中是阻塞在该信号量的各个线程的标识。当信号量被释放一个,值被加一后,系统自动从等待队列中唤醒一个等待中的线程,让其获得信号量,同时信号量再减一。
信号量通过一个计数器控制对共享资源的访问,信号量的值是一个非负整数,所有通过它的线程都会将该整数减一。如果计数器大于0,则访问被允许,计数器减1;如果为0,则访问被禁止,所有试图通过它的线程都将处于等待状态。
计数器计算的结果是允许访问共享资源的通行证。因此,为了访问共享资源,线程必须从信号量得到通行证, 如果该信号量的计数大于0,则此线程获得一个通行证,这将导致信号量的计数递减,否则,此线程将阻塞直到获得一个通行证为止。当此线程不再需要访问共享资源时,它释放该通行证,这导致信号量的计数递增,如果另一个线程等待通行证,则那个线程将在那时获得通行证。
Semaphore可以被抽象为五个操作:
- 创建 Create
- 等待 Wait:
线程等待信号量,如果值大于0,则获得,值减一;如果只等于0,则一直线程进入睡眠状态,知道信号量值大于0或者超时。
-释放 Post
执行释放信号量,则值加一;如果此时有正在等待的线程,则唤醒该线程。
-试图等待 TryWait
如果调用TryWait,线程并不真正的去获得信号量,还是检查信号量是否能够被获得,如果信号量值大于0,则TryWait返回成功;否则返回失败。
-销毁 Destroy
信号量,是可以用来保护两个或多个关键代码段,这些关键代码段不能并发调用。在进入一个关键代码段之前,线程必须获取一个信号量。如果关键代码段中没有任何线程,那么线程会立即进入该框图中的那个部分。一旦该关键代码段完成了,那么该线程必须释放信号量。其它想进入该关键代码段的线程必须等待直到第一个线程释放信号量。为了完成这个过程,需要创建一个信号量,然后将Acquire Semaphore VI以及Release Semaphore VI分别放置在每个关键代码段的首末端。确认这些信号量VI引用的是初始创建的信号量。
动作\系统 | Win32 | POSIX |
创建 | CreateSemaphore | sem_init |
等待 | WaitForSingleObject | sem _wait |
释放 | ReleaseMutex | sem _post |
试图等待 | WaitForSingleObject | sem _trywait |
销毁 | CloseHandle | sem_destroy |
互斥量和信号量的区别
1. 互斥量用于线程的互斥,信号量用于线程的同步。
这是互斥量和信号量的根本区别,也就是互斥和同步之间的区别。
互斥:是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。
同步:是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源
2. 互斥量值只能为0/1,信号量值可以为非负整数。
也就是说,一个互斥量只能用于一个资源的互斥访问,它不能实现多个资源的多线程互斥问题。信号量可以实现多个同类资源的多线程互斥和同步。当信号量为单值信号量是,也可以完成一个资源的互斥访问。
3. 互斥量的加锁和解锁必须由同一线程分别
对应使用,信号量可以由一个线程释放,
另一个线程得到。
zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz
由此我们可以得出结论:对于给定的黑色高度为 N 的红黑树,从根到叶子节点的最短路径长度为 N-1,最长路径长度为 2 * (N-1)。
提示:排序二叉树的深度直接影响了检索的性能,正如前面指出,当插入节点本身就是由小到大排列时,排序二叉树将变成一个链表,这种排序二叉树的检索性能最低:N 个节点的二叉树深度就是 N-1。
红黑树通过上面这种限制来保证它大致是平衡的——因为红黑树的高度不会无限增高,这样保证红黑树在最坏情况下都是高效的,不会出现普通排序二叉树的情况。
由于红黑树只是一个特殊的排序二叉树,因此对红黑树上的只读操作与普通排序二叉树上的只读操作完全相同,只是红黑树保持了大致平衡,因此检索性能比排序二叉树要好很多。
但在红黑树上进行插入操作和删除操作会导致树不再符合红黑树的特征,因此插入操作和删除操作都需要进行一定的维护,以保证插入节点、删除节点后的树依然是红黑树
ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ
史上最通俗易懂的关于二叉查找树、平衡二叉树、红黑树的关系讲解
这些关系是为了后面集合中TreeMap的学习打下基础,讨论TressMap删除、插入的效率就是以红黑树为基础。而红黑树又是优化了的二叉查找树。
二叉树是每个节点最多有两个子树的树结构。通常子树被称作“左子树”(left subtree)和“右子树”(right subtree)。
先来看一下基本的概念:
第一、二叉查找树(Binary Search Tree)和二叉排序树(Binary Sort Tree)都是一样的。
第二、二叉排序树或者是一棵空树,或者是具有下列性质的二叉树:
若左子树不空,则左子树上所有结点的值均小于或等于它的根结点的值;若右子树不空,则右子树上所有结点的值均大于或等于它的根结点的值;左、右子树也分别为二叉排序树;没有键值相等的节点(no duplicate nodes)。
因为一棵由n个结点随机构造的二叉查找树的高度为lgn,所以顺理成章,二叉查找树的一般操作的执行时间为O(lgn)。但二叉查找树若退化成了一棵具有n个结点的线性链后,则这些操作最坏情况运行时间为O(n)。
第三、平衡二叉树:AVL树是根据它的发明者G. M. Adelson-Velskii和E. M. Landis命名的。AVL树本质上还是一棵二叉搜索树,它的特点是:
本身首先是一棵二叉查找树。带有平衡条
件:每个结点的左右子树的高度之差的绝
对值(平衡因子)最多为1。
第四、红黑树:虽然本质上是一棵二叉查
找树,但它在二叉查找树的基础上增加了
着色和相关的性质使得红黑树相对平衡,
从而保证了红黑树的查找、插入、删除的
时间复杂度最坏为O(log n)。
算法导论里是这样定义一棵红黑树的,
每个结点或是红色的,或是黑色的
根节点是黑色的每个叶结点(叶结点即指树尾端NIL指针或NULL结点)都是黑的。如果一个结点是红的,那么它的两个儿子都是黑的。对于任意结点而言,其到叶结点树尾端NIL指针的每条路径都包含相同数目的黑结点。