这个笔记是用于以后自己看的,有些地方写的可能有点难理解.
API(Application Programming Interface,应用程序编程接口)
数据结构看做是一个ADT(抽象数据类型abstract data type),描述容器存储数据元素的方式
Minicontainer类使用STL接口直接实现操作
第一章
数据结构是组织和访问数据的系统方法(定义)
数据结构将数据与处理数据的操作捆绑在一起
v.resize(num)可以重新定义向量的容器大小,如果是num大于原本就是多余的全部都是0,如果小于就删尾,存在第二个参数可以在多出的情况用来填补
在软件工程中,限制私有数据访问是重要的原则,这便是信息隐藏的概念
可以先写好一个函数部分的功能,在之后发现个事不会的时候可以再写一个规格化函数包含这个函数就可以了
字符串:
字符串赋值给c-串用c_str()但是这能赋给const *char, 然后再通过它中转给char*
文件流的open()要用c-串形式
Getline(cin,str)返回的是流状态,并且是用string来进行输入
Str匹配find_first(last)_of(char c,int start=0)返回第一个(最后一个)跟字符c匹配的位置 0 开始,失败返回-1,start处开始
String成员substr(int start=0,int count=-1)count为获取多少个字符-1为字符串尾部
Int find(const string& s,int start=0)字符串s的匹配
Void inser(int start,const string& s)字符串的插入操作
Void erase(int start=0,int count=-1)其中-1为删除到字符串尾部
C++用’\\’代表\由于转码的原因
大多数的容器都是按照存储元素的位置来查找元素,但是图map是按照值来存储和查找元素.
Vs2010貌似对构造函数存在一个bug就是如果用初始化表在外部实现的话可能会出现识别不了,编译器把该构造函数当做了赋值构造函数了,如果把外部实现变成内嵌或者到放在同一文档就成功…
第2章:
当函数检测到错误时回抛出异常,这种异常通常是负责处理异常的某个特定类的对象.它绕过了通常的函数返回机制,沿异常发生之间的函数调用链逆向查找,直到确定了异常处理程序的位置为止.处理程序通常输出错误消息,然后采取正确的操作或终止程序.
开发模型,即软件开发生命周期.尽管采用的技术不同,但模型都包括下面几个阶段:
需求:分析:设计:实现:设计:
从长远的角度来看,测试时开发周期中最合算的步骤之一
Exit()在库<cstdlib>中
抛出异常的使用:
例如知道如果在setYear或者setMonth可能引起错误,
则在setMonth中有
{
..
..
If(mm<1||mm>12)
Throw dataError(“calendar setMonth():”,mm,”invalidmonth”);
}
在调用函数中,并且是存在dataError这个类
Int main()
{
…
…
//吧容易错的函数用try
Try
{
Cal.setYear(year);
Cal.setMonth(month);
}
//并有相应的处理模块catch
Catch(const dateError&de)//注意这里的类型
{
Cerr<<de.what()<<endl;
Exit(1);
}
}
Try程序块后必须紧跟一个或多个异常处理程序,捕获try程序块执行时产生的异常,使用下列语法可以再catch块中实现异常处理程序
如果发生异常,检测系统不能再执行序列中找到对应的catch块,异常将标记为未捕获,同时系统执行一个函数来终止程序
复合包含的类称为供应类,包含复合对象的类称为客户类
Class appointment 客户类
{
Private:
Sting motoristName;
Data apptDate;
Time24 apptTime;
//上面3个都是供应类,供应给客户类
}
运算符重载:
友元函数可以访问私有变量或者函数,但是不是类的成员,所以如果是模板则需要对他用template<class T>并且在友元函数里面的变量需要加上 . or ->指明是哪个类的,可以吧友元函数当做是外部的,但是却可以访问类的私有就可以;所以在外部实现友元函数是不需要加作用域运算符的
运算符>>将数据从数据流取出,这个流cin指从输入操作流,fstream指从文件操作流取出
赋值运算符=重载为成员函数,原则:使用成员函数重载任何其操作改变对象本身的二元函数
如=,+=,etc…所以返回的是引用,引用这个对象
处理错误最好就是把错误处理的决定权交给调用代码块,所以可以通过一个布尔型或者用异常处理机制来处理
友元函数在类外实现不需要加friend
第3章
选择排序:
选择排序从表头开始,遍历表的每个位置,在每个位置,算法都要从表中选择恰当的元素,并把其值复制到这个位置.
void xuanzesort(int arr[],int n)
{
intsmallIndex;
intpass,j;
inttemp;
for(pass=0;pass<n-1;pass++)
{
smallIndex=pass;
for(j=pass+1;j<n;j++)
if(arr[j]<arr[smallIndex])
smallIndex=j;
if(smallIndex!=pass)
{
temp=arr[pass];
arr[pass]=arr[smallIndex];
arr[smallIndex]=temp;
}
}
}
冒泡排序:
void bubbleSort(T arr[],int n)
{
inttemp,i,j;
boolflag=false;
for(i=0;i<n;i++)//控制趟数
{
flag=false;
for(j=1;j<n;j++)
if(arr[j-1]>arr[j])
{
temp=arr[j];
arr[j]=arr[j-1];
arr[j-1]=temp;
flag=true;
}
if(!flag)
break;
}
}
一般情况下,设计者倾向于一种具有较好的最坏情形性能的算法,即使平均性能不好
线性算法:当算法的时间复杂度与表的大小成正比时,算法的效率为O(n),这样的算法为线性算法
模板有两个重要的限制:
1. 函数参数表中至少一个参数为模板类型T
2. 函数体中的C++语句可以使用类型T对象,但只能使用对应于类型T的实际类型有效的操作.例如,某个程序可能不能对矩形使用包含<运算符的模板函数,因为矩形类没有定义这个操作.当然也可以用操作符重载来重载他就可以用了
当算法通过将一个问题分割成几个子问题,且子问题是同同样的算法解决时,便用递归的方法.
递归函数的设计包括以下几个因素:
1. 对于某些参数可以直接计算法一个或多个停止条件
2. 一个或多个递归的步骤,在这些递归步骤中,函数的当前值可通过重复调用此函数来计算出,而在调用的过程中,所用的参数最终将到达停止条件
十进制转换成各种进制:
void displayInBase(int n,int base)
{
if(n>0)
{
displayInBase(n/base,base);
cout<<n%base;
}
}
汉诺塔:
void hanoi(int n,const string&init,const string &end,const string &temp)//从这里看的参数表中一开始递归是初始,目标,临时
{
if(n==1)
cout<<"move"<<init<<"to"<<end<<endl;
else
{
//从init到temp移动n-1个盘子,使用end进行临时存储盘子
//因为是最终要吧最底下最大的放在end,所以要把n-1放在temp
hanoi(n-1,init,temp,end);//在参数表中充分说明了这个过程.也从这里看出递归其实可以用直接一个表示.理解递归
cout<<"move"<<init<<"to"<<end<<endl;//移动最大的盘子到end
//从temp到end移动n-1个盘子,使用init进行临时存储
hanoi(n-1,temp,end,init);
}
}
求最大公约数:
int gcd(int a,int b)
{
if(b==0)
returna;
else
returngcd(b,a%b);
}
当使用递归方法能够增强算法设计并提供具有合理空间和时间效率的函数实现时,我们就采用递归
//精确到毫秒 =====时间测量函数,要包含头文件<time.h>
clock_t c1,c2;
c1=clock();
//此处放置要测试的代码
Sleep(100);//延时
c2=clock();
printf("%d %d %d毫秒\n",c1,c2,c2-c1);
从对比冒泡和选择排序可以看的出都是按照最坏情况的时间来运行的.正序和随机的序列差别不大
第4章:
STL容器类的3个种类为:顺序容器,适配器容器,关联容器
适配器包含另一个容器作为其基本的存储结构,然而,适配器向程序员提供的的接口仅仅是底层存储结构操作的一个受限子集
表容器(list)是按位置存储元素的数据结构,访问只能从表头开始,直到找到数据值
映射(map)不按位置存储元素
通常,模板类中的函数的参数通过常量引用传递,对于程序员自定义的类型,这就避免了按值调用时参数所需要的复制,而且这种做法也适用于C++的基本类型.
用模板的构造函数的参数表可以用store(const T& item=T())//使用item或类型T的默认对象进行初始化
实现模板类成员函数的外部函数本身就是模板函数.其声明必须包含一个模板参数表
如果是友元函数,因为友元函数不是成员函数,所以在声明的时候要另外加模板参数表
一个类是模板只需要在声明类的上面加上模板参数表就可以了,但是对友元需要另外再加模板参数表
Int arrsize=sizeof(intarray)/sizeof(int);//这样就可以不改变向量的声明下改变数组大小.
Vector<int> intVectoe(intarray, intarray+arrsize);
向量容器可以用push_back()和pop_back()可以更改序列尾部,也可以用back()来访问或者更新尾部元素,就是可以用在=任何一边
Resize(int num,const &value=T())第二个参数是默认值,可以在resize后如果大约初始值就以这个参数来填充
当对象为常量时,程序员只能调用类中用const修饰符声明的成员函数.成员函数back()和下标运算符[]各有一个独立的实现,声明为const.因此,这些函数可以被常量对象调用.
Template<class T>
template<class T>
void writeVectorEnds(const vector<T>& v)//常量对象,所以在向量的back和下标运算符中有另外const版本的back和下标运算符的实现
{
if(v.size()>0)
cout<<v[0]<<''<<v.back()<<endl;
}
插入排序:
void insertionSort(T arr[],int n)
{
inti,j;
Ttarget;
for(i=1;i<n;i++)
{
j=i;
target=arr[i];
while(j>0&&target<arr[j-1])
{
arr[j]=arr[j-1];
j--;
}
arr[j]=target;
}
}
从测试数据看的出如果插入排序在运行时间复杂度为平方级的排序算法中的表现最好,也别是表已经有序或者基本上有序时间复杂度都是O(n)如果是无序则为O(n^2),但是高级算法中的希尔排序比这个好
交换排序:
template<class T>
void swapSort(T arr[],int n)
{
inti,j,temp;
for(inti=0;i<n;i++)
{
for(j=i+1;j<n;j++)
{
if(arr[j]<arr[i])
{
temp=arr[i];
arr[i]=arr[j];
arr[j]=temp;
}
}
}
}
可以把交换排序当做的选择排序的退化版
希尔排序:
template<class T>
void shellSort(T arr[],int n)
{
inti,j,gap=n;
Ttemp;
do
{
gap=gap/3+1;//间隔
for(i=gap;i<n;i++)
if(arr[i]<arr[i-gap])
{
temp=arr[i];
j=i-gap;
do
{
arr[j+gap]=arr[j];
j=j-gap;
}
while(j>=0&&temp<arr[j]);
arr[j+gap]=temp;//插入
}
}
while(gap>1);//间隔
}
从数量不大的情况下可以看出希尔排序比直接插入排序还好,因为希尔里面用到了插入排序
对10000个数据排序的时间效率结果,最好是希尔排序,最差是交换排序,最好最坏情况的时间效率相差不大,从这里可以看出为什么算法宁愿找个最差情况比较好的算法也不要平均情况比较好的算法.
快速排序:
void qsort(int arr[],int first,int last)//快速
{
intf=first,l=last;
intk=arr[f];
if(f>=l)//如果是左边的数大于右边的数
return;
while(l!=f)
{
while(f<l&&arr[l]>=k)//如果是正序就序号左移动
l--;
arr[f]=arr[l];//找到序号不同的就把那个放回第一个
while(f<l&&arr[f]<=k)//如果是正序就序号右移动
f++;
arr[l]=arr[f];;//找到序号不同的就把那个放回最后一个
}
arr[f]=k;//换回去,k当做temp
qsort(arr,first,f-1);//找到前半段递归
qsort(arr,f+1,last);//找到后半段递归
}
或者是另外一种表达
template<class T>
void qsort(T arr[],const int left,const intright)
{
if(left<right)
{
intpivotpos=partition(arr,left,right);
qsort(arr,left,pivotpos-1);
qsort(arr,pivotpos+1,right);
}
}
template<class T>
int partition(T arr[],const int low,constint hight)
{
intpivotpos=low;
Tpivot=arr[low];
for(inti=low+1;i<=hight;i++)
if(arr[i]<pivot)//比基准元素小的全部都放在左边
{
pivotpos++;
if(pivotpos!=i)
{
Ttemp=arr[pivotpos];
arr[pivotpos]=arr[i];
arr[i]=temp;
}
}
arr[low]=arr[pivotpos];//最后在把基准元素和基准位置的值互换
arr[pivotpos]=pivot;
returnpivotpos;//返回基准位置
}
这两种实现会让每一趟的结果不同,但是最终答案一样
第5章:
一但程序开始执行,他就能够从系统提供的资源(堆)中请求附加内存.C++运用指针和操作new,delete使程序能够从堆中分配和释放内存
指针变量(简称指针)拥有指定类型对象的地址
C++语法允许指针变量使用指针算数运算和下边运算符[].例如
Int *p=arr;
p[1]==*(p+1)==arr[1]//就是指针可以用下标运算符,也可以用++运算符p++,指向下一个元素
->运算符叫做反引用和选择运算符,它仅与指向对象的指针一起使用.
我们把来自堆中内存称为动态内存,因为程序使用运行时指令分配和释放资源.
如果堆没有足够的内存分配给请求的变量或对象,则new操作返回值NULL
内存泄露指的是没有释放掉分配的内存(已经没有用的).
类经常使用动态内存存储数据,我们把这些类称为动态类.一般使用动态分配的东西都是动态的.
当某个类有动态分配内存对象时,运行系统知识销毁对象和所有的声明内存,并不销毁任何相关的动态内存.
当程序终止时,运行时系统为主程序中声明的所有对象调用析构函数.对于程序块中的局部对象,当程序退出程序块时调用析构函数
对于动态分配内存的类,初始化和赋值的默认版本不能正确地管理内存,这将潜在产生一些运行时错误.
如果有OBJB=OBJA,则原本OBJB指向的内存将存在泄漏
或者是OBJA或者OBJB其中一个被销毁,那么另外一个将可能导致致命的应用程序错误
程序员只能在类成员函数内部使用标识符this
初始化是创建新对象,这个新对象是现有对象的副本.而对于赋值,由于先前有声明,所以两个对象都已经存在,其焦点在复制上.运行时系统用通过调用一个称作复制构造函数的成员函数,来执行初始化.此成员函数名具有描述析构,该函数是一个构造函数,它通过从现存对象复制值来初始化当前对象
复制构造函数的参数必须是引用参数,不然将会出现无限调用复制构造函数.还有必须是常引用
程序员只能把二元运算符[]作为一个成员函数重载
STL向量类不具备下标范围检测
异常处理程序并不是仅能现实错误信息,还能处理或者修复信息
对于二维数组参数传递
Int matsum(int mat[][],int numrow,intnumcols)//这样是错误的,因为不知道数组每行有多少个元素
Int matsum(int mat[][4],int numrow,intnumcols)//必须这样,这样的话numcols就不需要传递了
Vector<vector<T> > mat外部向量对应矩阵的行,内部类型T的向量中的元素对应每一向量行的列项,这样mat[0]对应行0的列项的向量,主义在内部的vector和外部vector的> >这2个符号之间要用空格隔开,不然会被认为是>>
Matrix<T>::matrix(int numrows,intnumcols,const T&initval):nrows(numrows),ncols(numcols),mat(numrows,vector<T>(numcols,initval)){}
Mat为这个类的内部向量成员.这里编译器通过调用mat的向量构造函数来解释表达式,调用时numrows作为大小, vector<T>(numcols,initval)作为mat中每个元素的初始值.
第6章:
List是用双链表实现
在表中可以用front和back一样在左右两边都可以访问和更新数据
Isalpha(ch)可以判断一个字符是不是字母,tolower(ch)可以吧大写转换字母转换成小写
迭代器对象的声明必须吧类名”iterator”和作用域运算符表达式”list<T>::”放在一起.
所有的STL容器类都有一个嵌套的iterator类
Liset<T>::iteratorFind_last_of(list<T> &alist,const T& value)在表中寻找最后一个跟值相匹配的迭代器位置
常量迭代器只用于访问数据,不可以更改数据,例如insert和erase函数都不可以用
List<T>::const_iteratoriter;
用不用常量迭代器的规则是:用常量迭代器访问和扫描常量表,用非常量迭代器访问和扫描非常量表
Iter=seqSearch<int>(intlist.begin(),intlist.end(),5);
通常,不论何时编译器不能为函数调用确定模板类型,都可以使用这种结构,指示编译器使用指定的类型.
STL的插入操作都是插入迭代器之前的位置,并且返回新插入位置的迭代器
一般的习惯是erase(iter++)这样就不会在删除了元素后iter指向非法区域.而是指向下一个元素
如果存在多种重复的,并且占用的内存比较大的,可以考虑用占用内存比较小的数据来代替,到时候在映射回去
如果一个迭代器指向end然后再用++则迭代器指向第一个元素
第7章:
栈 top()可以放在=号前后,修改或更新
进制转换,将非负整数N转换到以B基数的算法:用B重复去除N.在每一步中,余数N%B为所求数组,商N/B为下一步要除的数值,商为零时结束
关于进制转换如果是十六进制或者更高可以用一个字符串做映射,例如digitchar=”0123456789ABCDE”然后用digitchar[i]其中i为余数
函数是响应函数调用而执行的一序列指令.执行过程首先由调用语句建立一个活动记录,其中包括运行时参数表,函数中局部变量和对象的空间,一个返回地址,这个地址是函数返回到调用语句后,下一条要执行指令的位置.
在函数调用时,运行时系统将活动记录推入到系统提供的栈中,称为运行栈.然后将控制权传递给函数中的语句,在这里,记录中的数据可以被函数体使用,在退出函数时,运行时系统从活动记录中提取返回地址,然后把记录出栈
双端队列允许用下标访问元素外,双端队列的元素序列能够在头部和尾部有效的动态增长
栈是使用双端队列实现的(在默认情况下)
适配器类是容器,它使用某类提供的接口来实现另一个类所需的接口.
可以选择用什么容器来实现栈或者队列例如可以这样声明栈,选择不同的底层实现将影响效率,一般都是用双端队列.
Stack<int,list<int>> sta;
Isdigit()可以分辨一个字符是不是数字
用等级表达式可以确认是不是有效的输入,例如运算符为-1,操作数为1,括号为0,则最终的级别肯定为0或1,如果值为2说明表达式中有太多的运算数,为负就是太多的运算符
第8章:
基数排序,有多少位就要排序多少次,都要用到10(根据单个都有多少个元素定扑克是13个)个队列来先从个位然后十位…最后排完后的序列就是有序的,所谓的多少位就是排序码数,称为多排序码,个位和十位…在这里就是个位优先,个位称为最高位排序码,最高位优先法MSD(一般都是递归过程),如果个位是最高位,那么得到的是从小到大,如果是个位不是最高位(或者用最低位优先LSD)那么得到的是从大到小.
内囿算法是指不需要额外的存储空间.
双端队列在加入和删除数据时的操作和向量类似,队列默认是用双端队列来实现,但是如果考虑到内存开销的话可以改选用list来实现,因为list是动态的分配和删除内存,但是运行速度相对于双端队列慢一点
只要实现了关系运算符<的都可以使用优先队列或者是STL有关大小的操作
在优先级队列中有相同优先级的按照先进先出
可以通过枚举来罗列映射优先级,这样做会更加直观
返回staff这个枚举类型的时候要这样返回,因为编译器首先需要处理了返回类型信息,然后才能通过域运算符发现getStatus()是jobRequest类的成员函数
jobRequest::staffjobRequest::getStatus()const
{
Return staff;
}
第9章
重载前缀运算符时,重载函数没有任何参数.
后缀运算符要加一个参数int,当编译器发现了一条使用后缀加1运算符时,它会提供一个值为0的运行参数.可以不需要给出这个参数的名字
只有在预先知道元素个数的情况下,才使用C++数组
如果应用程序需要根据下标访问数据元素,并且所有的添加和删除元素的工作都在序列末端进行,那么使用向量.
如果程序需要频繁的在中间位置上进行添加和删除操作,就应该选用表
如果程序需要根据下标访问元素,并且所有添加和删除元素的工作都在序列两端进行,那么久使用双端队列,双端队列吧数据分块存储,并通过一个块映射吧各个块连接起来.
内嵌类的声明
Template<classT>
Class minilist
{
Public:
#Include”neiqianlei.h”//这个内嵌类的头文件
}
内嵌类不能访问外类的成员,外类也不能访问内嵌类的成员,所有成员
STL的list库里面有sort和merge(按照序列排)
第10章:
一棵n个结点的完全二叉树的高度是log2 n
没有结点的树也是二叉树
这里跟其他书上写的有出入,比如说根节点的层次是处于第一层,并且只有根结点的数的高度为1
打印树可以用影子树来建立(算法采用中序遍历来建立影子树,并采用层次遍历来现实结点.)
每个树结点包含值和指定结点位置的信息
可以通过返回一个pair来返回删除或者添加的迭代器和成功过与否标志
二插搜索树的删除算法本质上是找到替代的结点,并且使其脱离,然后链接到被删除结点的位置
有以下4种情况:
1. 结点D是叶节点,在这种情况下,没有必要寻找代替的结点.知识让父节点的子树为空就可以了
2. 结点D有一个左子结点,但是没有右子节点,在这种情况下,左子节点就是代替的结点R.将左子节点连接到父节点上
3. 结点D有一个右子节点,但没有左子节点.在这种情况下,右子节点就是代替的结点R.将右子节点连接到父节点上
4. 结点D有两个子节点
(a) 在右子节点上寻找最小的结点来替代
如果层次遍历不用队列用栈来实现,那么得到的序列将是从右上边一列一列开始向左扫的序列
第11章:
Set和map的内部实现存储结构是二叉搜索树(红黑树)
Equalrange()和count()是多重子集才有的
关联容器没有push_back和pop_front函数,这些只是用于顺序容器
Insert()返回的是一个pair<iterator,bool>第二个参数指明是否插入成功,如果已经存在在容器里面就插入失败,可以定义一个pair<set<T>::iterator,bool>来获得这个pair
Erase(first,last)可以用这个来删除这个范围内的所有元素,2个参数都是iterator
Erase(键或者迭代器)
埃拉托斯特尼筛法是在一个1到n的集合里面从2开始每次都删除这个数的倍数,最后判断所有的合数都被删除为止.可以找到1到n的素数,只考虑2->根号n之间的序列
Set集合可以用集合运算符+(并)*(交)-(差)
可以通过这样来声明包含pair<string,int>对象的数组.
typedefmap<string,int>::value_type entry;//其中的value_type指的就是键值对类型pair
entry arr[]={entry("asd",105),entry("aasd",125),entry("adwsd",135)};
map<string,int>obj(arr,arr+3);
然后通过用obj来访问
Map的insert跟set的insert一样返回一个pair
Multiset和multimap中的erase(键)是删除全部具有这个键的元素,find将返回指向第一个目标元素的迭代器,可以用equal_range(键)返回pair<first,last>来得到地址,inset是返回指向新元素的地址
使用简化符号来定义
TypedefminiPair<const Key,T> value_type;
那么就可以直接这样写语句
Returnt.find(value_type(key,T()));
使用关联容器必须具备<和==运算
可以通过map<string,list<string> >来模仿multimap
第12章:
哈希是根据直接访问的模式构建的,能替换二插搜索树
哈希表避免冲突的两种方法:线性探测开放寻址法(线性探查法,二次探查法,双散列法)和独立表链地址法
2-3-4树是完全平衡树的模型,红黑树就是由这个概念派生出来
哈希函数提供的访问方式是根据键求出一个索引,并用改索引来定位表中的某项数据
函数对象:
函数对象也是类的对象,其行为类似于函数.但与普通函数不同的是,像其他类型的对象一样,可以创建.存储和销毁函数对象他们也可以拥有相关的数据成员和操作.定义函数对象的第一步是声明一个类模板,其中包含了重载函数调用运算符()的成员函数,运算符()的参数表就是函数对象的参数表
函数对象可以这样用
template<classT>
class greaterthan
{
public:
bool operator()(const T& x,const T& y) const//参数表就是函数的参数表
{
return x>y;
}
};
int main()
{
greaterthan<int> f;//函数对象的声明
int a=3,b=4;
if(f(a,b))
cout<<a<<'>'<<b<<endl;
else
cout<<a<<"<="<<b<<endl;
return 0;
}
也可以是用匿名对象来调用函数
String stra=”walk”,strb=”crawl”;
If(greatthan<string>()(stra,strb))//其中()是匿名对象
Cout<<stra<<’>’<<strb<<endl;
哈希函数有,恒等函数,平方中值函数
平方中值函数:吧一个无符号整数平方,然后截取中间某几个连续位,作为哈希返回值
探测开放寻址法模式的哈希表不是一种能够自动增长来适应新数据的动态结构
class employee
{
publc:
friend classhfemp;
};
class hfemp
{
public:
unsigned intoperator()(const employee &item) const
{
returnhfstring()(item.ssn);
}
};
这样就可以用自己定义的哈希类来当存储结构了
其中hfstring是一个函数类,里面是一个对字符串的一个哈希函数
哈希表越短就越容易产生冲突
哈希表和二叉树不同的是迭代器不能按元素的大小顺序访问他们
哈希函数是一个定位函数,它用键作参数,返回表中的索引值
STL在头文件<functional>中实现了函数对象类型greater和less这两个类型
独立表链地址法:
是一个可以动态增长的结构,主要缺点是要管理表容器,需要额外的运行时间和内存消耗
当表容量大于数据的一倍时用线性探测开放寻址的方法效率比较高,如果哈希函数做到的平均都是等价的情况下
如果数据元素的个数没有上界的话,程序员是不会选择哈希表的,因为这时候的复杂度是O(n)
四种查找方法的使用:
当元素个数,并且数据元素不必排序时,顺序查找
二分查找要求数组或者向量中的数据元素必须有序排列,不适合需要频繁添加或者删除数据的应用程序,维护顺序开销大.
对于动态改变的数据,二叉搜索树和哈希表适合.
2-3-4树种不允许重复数值存在
2-3-4树用自顶向下式的4-结点拆分方法
性质1 红黑树的根结点是黑色的
性质2 永远不会有两个红色的结点直接连接
性质3 从根结点到空子树(NULL指针)的任意一条路径上,都有相同数量的的黑结点.我们把这个数量叫做该树的黑色深度
红黑树的插入规则:
维护根结点,一定为黑色结点
新元素总是作为红结点插入到树中
无论什么时候,插入操作造成了两个连续的红结点,都要在平衡树结构的时候旋转结点,建立黑色的父结点
党想下遍历一条路径,寻找插入位置时,拆分任何4-结点
*迭代器的设计和实现,好好理解…
第13章:
多态是指在不同的类中分别实现有相同名字的成员函数.当应用程序需要调用这个函数时,在运行期间,将动态决定使用该函数的哪个版本.C++通过使用虚成员函数来实现多态
如果基类有一个默认的构造函数,同时有默认值,则派生类无需显式执行默认的构造函数.
对于继承链中的对象而言,运行系统以调用构造函数的相反顺序调用析构函数.
不能用基类对象初始化派生类对象,这是因为基类构造函数的声明中并不包括用于派生类对象中的派生部分的那些参数,但是用指针就可以这样,因为这正是动态绑定的机制
在基类中动态绑定的函数可以这样定义
Voiddisplay(employee *empPtr)//传递是什么基类的对象就等于什么
{
empPtr->displayEmployeeInfo();//这里可以通过动态决定用那个版本的函数
}//通过传递派生类对象的地址
在Windows环境中,句柄是用来标识项目的。
在程序设计中,句柄是一种特殊的智能指针。当一个应用程序要引用其他系统(如数据库、操作系统)所管理的内存块或对象时,就要使用句柄。
句柄与普通指针的区别在于,指针包含的是引用对象的内存地址,而句柄则是由系统所管理的引用标识,该标识可以被系统重新定位到一个内存地址上。这种间接访问对象的模式增强了系统对引用对象的控制。
通过在派生类的私有部分声明其函数原型,就可以将基类有的共有函数隐藏起来
基类的析构函数最好为虚函数,不然在动态绑定的时候可能会出现内存泄露.delete只吊销了基类,没有吊销派生类
C++允许程用户在函数原型后添加”=0”来使用纯虚函数.除了允许该函数使用多态性以外,在基类中,该函数的出现在还保证了每个派生类都必须覆盖该函数.有纯虚函数的类不允许声明对象
第14章:
基于数组实现的堆,对于i的父节点是(i-1)/2,左子结点2*i+1,右子结点2*i+2
插入新节点:
先把结点用push_back放在最后2
堆排序被称为原地排序(不需要额外的存储)
I/O操作在处理非字符数据时,需要先使用(char *)将其转换为字符型数据.如果忽略这个操作,将导致编译错误.
使用read和write的流操作的时候要转换成为(char *)
F.read((char*)&intdata,sizeof(int))
F.write((char*)arr,sizeof(arr)
可以用以下的方法来知道文件有多少条记录
先用seekg()指向文件尾,然后用tellg()获取文件总的字节数,然后再除以sizeof(account),就可以得到总的记录数
当计算中涉及到对整数乘以或除以2的整数倍时,可以用移位运算符提高计算速度
术语”无符号”是指地府的数字值是0到255之间的无符号的整数
哈夫曼算法属于贪婪算法
第15章:
所谓回溯算法,就是每部计算都产生一个看起来与最后结果一致的部分解答.如果某部产生了与最后结果不一致的部分解答,算法就会倒退一步或几步,直到退到最近与最后结果一致的部分解答的那步为止,有时,回溯发使得每前进一步,就要后退n步.这个技术经常出现用在操作模型,博弈论,和图的研究
当这些调用函将数据分隔成一些互相独立的集合从而处理这些较小的数据集合时,分而治之算法将会有最高的效率
归并排序:
template<classT>
void merge(Tarr[],int first,int mid,int last)//合并
{
vector<T>tarr;
intindexa,indexb,indexv;
indexa=first;
indexb=mid;
while(indexa<mid&&indexb<last)//把小的压入数组
if(arr[indexa]<arr[indexb])
tarr.push_back(arr[indexa++]);
else
tarr.push_back(arr[indexb++]);
while(indexa<mid)//如果数组a还有剩余
tarr.push_back(arr[indexa++]);
while(indexb<last)//如果数组b还有剩余
tarr.push_back(arr[indexb++]);
indexa=first;//复制回原来的数组
for(indexv=0;indexv< tarr.size();indexv++)
arr[indexa++]=tarr[indexv];
}
template<classT>
void mergeSort(Tarr[],int first,int last)
{
if(first+1<last)//分割到单个
{
intmidpt=(last+first)/2;//中间划分
mergeSort(arr,first,midpt);//左排序
mergeSort(arr,midpt,last); //右排序
merge(arr,first,midpt,last);//排序好后的合并
}
}
可以看出这个算法构造跟快速排序的一样.也是基于划分左右的思想
不过第一个开始排序的时候是从单个数据开始,而快速排序是先整个表开始
任何使用比较来达到排序目的的算法的最坏性能不可能好于O(nlog2n),平均性能不可能为O(n)
大数据的排序性能快速排序>堆排序>归并排序>插入排序
找第几大或小的思路可以用快速排序的思想,就不必排序后再计算
一般而言,当分而治之法划分的子问题不是互相独立的话,即使直接的递归实现也会有很差的时间复杂度,因为子问题互相重叠会导致数目惊人的多余计算和函数的调用
运用DP(动态规划)可以使指数型增长的算法变成O(n),重点是如何记住以前计算过的数据并想办法如何使用这个数据
背包问题就是不仅能放进背包里面,而且这些物品加起来得到的值是最大的
最优原理规定无论第一个决策是否是最优的,而其余的决策都必须是最优的,以确保第一个决策之后算法中的所有状态都是最优的
是否出现在同一个对角线上可以用行-列是否等于另外一个的行-列||行+列是否等于另外一个行+列
一般递归的参数是保存会改变的量和这个步骤的初始值
递归让算法很美~~~
第16章:
拓扑排序使用与按优先次序计划活动的情况(依次找入度为0的点)
DFS,BFS,AOV,强连通分量算法的时间复杂度都是O(V+E)
强连通图就是任何两点都是可以互相到达
可以用(v,w)和(w,v)多次调用来确定是否是连通的就知道是不是强连通图
Dijkstra算法使用”贪婪”(试图最大化其短期收益)策略分阶段解决问题
Dijkstra算法时间复杂度是O(V+Elog2E)
*Dijkstr算法是通过为每个顶点 v 保留目前为止所找到的从s到v的最短路径来工作的。