数据结构及算法学习总结(一)

1.vector 封装数组、list封装链表、map和set封装二叉树

C++ STL中标准关联容器set, multiset, map, multimap内部采用的就是一种非常高效的平衡检索二叉树:红黑树,也成为RB树(Red-Black Tree)。RB树的统计性能要好于一般的平衡二叉树(有些书籍根据作者姓名,Adelson-Velskii和Landis,将其称为AVL-树),所以被STL选择作为了关联容器的内部结构。iterator这里就相当于指向节点的指针。

2.STL map常用操作简介

  • 使用map得包含map类所在的头文件
  • #include <map> //注意,STL头文件没有扩展名.h
  • map对象是模板类,需要关键字和存储对象两个模板参数:
  • std:map<int, string> personnel;这样就定义了一个用int作为索引,并拥有相关联的指向string的指针.
  • 为了使用方便,可以对模板类进行一下类型定义,typedef map<int, CString> UDT_MAP_INT_CSTRING;UDT_MAP_INT_CSTRING enumMap;

(1)在map中插入元素

改变map中的条目非常简单,因为map类已经对[]操作符进行了重载

enumMap[1] = "One";
enumMap[2] = "Two";
.....

这样非常直观,但存在一个性能的问题。入2时,先在enumMap中查找主键为2的项,没发现,然后将一个新的对象插入enumMap,键是2,值是一个空字符串,插入完成后,将字符串赋为"Two"; 该方法会将每个值都赋为缺省值,然后再赋为显示的值,如果元素是类对象,则开销比较大。我们可以用以下方法来避免开销:

enumMap.insert(map<int, CString> :: value_type(2, "Two"))

(2)查找并获取map中的元素

下标操作符给出了获得一个值的最简单方法:

CString tmp = enumMap[2];

但是,只有当map中有这个键的实例时才对,否则会自动插入一个实例,值为初始化值

我们可以使用Find()和Count()方法来发现一个键是否存在。

查找map中是否包含某个关键字条目用find()方法,传入的参数是要查找的key,在这里需要提到的是begin()和end()两个成员,分别代表map对象中第一个条目和最后一个条目,这两个数据的类型是iterator.

int nFindKey = 2;            //要查找的Key
//定义一个条目变量(实际是指针)
UDT_MAP_INT_CSTRING::iterator it= enumMap.find(nFindKey); 
if(it == enumMap.end()) {
    //没找到
}
else {
    //找到
}

通过map对象的方法获取的iterator数据类型是一个std::pair对象,包括两个数据 iterator->first 和 iterator->second 分别代表关键字和存储的数据

(3)从map中删除元素

移除某个map中某个条目用erase()

该成员方法的定义如下

  • iterator erase(iterator it); //通过一个条目对象删除
  • iterator erase(iterator first, iterator last);        //删除一个范围
  • size_type erase(const Key& key); //通过关键字删除

clear()就相当于 enumMap.erase(enumMap.begin(), enumMap.end());

3.STL hash_map简介

hash_map的用法和map是一样的,提供了 insert,size,count等操作,并且里面的元素也是以pair类型来存贮的。虽然对外部提供的函数和数据类型是一致的,但是其底层实现是完全不同的,map底层的数据结构是rb_tree而,hansh_map却是哈希表来实现的。

4.hashtable的构造方法

哈希表的几个概念:

映像:由哈希函数得到的哈希表是一个映像。

冲突:如果两个关键字的哈希函数值相等,这种现象称为冲突。

处理冲突的几个方法:

(1)开放地址法:用开放地址处理冲突就是当冲突发生时,形成一个地址序列,沿着这个序列逐个深测,直到找到一个“空”的开放地址,将发生冲突的关键字值存放到该地址中去。

例如:hash(i)=(hash(key)+d(i)) MOD m (i=1,2,3,......,k(k<m-1)) d为增量函数,d(i)=d1,d2,d3,...,dn-1

根据增量序列的取法不同,可以得到不同的开放地址处理冲突探测方法。

有线性探测法、二次方探测法、伪随机探测法。

(2)链地址法:把所有关键字为同义词的记录存储在一个线性链表中,这个链表成为同义词链表,即把具有相同哈希地址的关键字值存放在同义链表中。

(3)再哈希表:费时间的一种方法

     开放地址法  线性探测动态演示:

http://student.zjzk.cn/course_ware/data_structure/web/flashhtml/kaifang.htm

   

(1)哈希表的构造程序

#include<iostream>
using namespace std;


typedef int KeyType; //设关键字域为整形,需要修改类型时,只需修改这里就可以
const int NULLKEY=0; //NULLKEY表示该位置无值
int c=0; //用来统计冲突次数


struct Elemtype //数据元素类型
{
KeyType key;
int ord; 
};


int hashsize[]={11,19,29,37,47}; //hash表容量递增表
int Hash_length=0;//hash表表长


class HashTable
{
private:
Elemtype *elem; //数据元素数组,动态申请
int count;// 当前数据元素个数
int size; //决定hash表的容量为第几个,hashsize[size]为当前hash容量
public:


int Init_HashTable() //构造一个空hash表
{
int i;
count=0;
size=0; //初始化容量为hashsize[0]=11
Hash_length=hashsize[0];
elem=new Elemtype[Hash_length];
if(!elem)
{
cout<<"内存申请失败"<<endl;
exit(0);
}
for(i=0;i<Hash_length;i++)
elem[i].key=NULLKEY;
return 1;
}


void Destroy_HashTable()
{
delete[]elem;
elem=NULL;
count=0;
size=0;
}


unsigned Hash(KeyType k) //hash函数的一种(取模法)
{
return k%Hash_length;
}


void Collision(int &p,int d) //解决冲突
{
p=(p+d)%Hash_length; //采用开放地址法里的线性探测
}


bool Search_Hash(KeyType k,int &p) //查找
{
//在开放地址hash表中查找关键字等于k的元素
//若找到用p表示待查数据,查找不成功时,p指向的是可插入地址
c=0;
p=Hash(k); //求hash地址
while(elem[p].key!=NULLKEY && elem[p].key!=k)
{
c++;
if(c<Hash_length)
Collision(p,c);
else
return 0; //表示查找不成功
}
if(elem[p].key==k)
return 1;
else
return 0;
}


int Insert_Hash(Elemtype e) //插入
{
//在查找不成功的情况下将k插入到hash表中
int p;
if(Search_Hash(e.key,p))
return -1; //表示该元素已在hash表中
else if(c<hashsize[size]/2) //冲突次数未达到上限
{
//插入e
elem[p]=e;
count++;
return 1;
}
else
ReCreate_HashTable(); // 重建hash表
return 0; //插入失败
}


void ReCreate_HashTable() //重建hash表
{
int i,count2=count;
Elemtype *p,*elem2=new Elemtype[count];
p=elem2;
cout<<"____重建hash表_____"<<endl;
for(i=0;i<Hash_length;i++) //将原有元素暂存到elem2中
if(elem[i].key!=NULLKEY)
*p++=*(elem+i);
count=0;
size++; //hash容量增大
Hash_length=hashsize[size];
p=new Elemtype[Hash_length];
if(!p)
{
cout<<"空间申请失败"<<endl;
exit(0);
}
elem=p;
for(i=0;i<Hash_length;i++)
elem[i].key=NULLKEY;
for(p=elem2;p<elem2+count2;p++) //将原有元素放回新表
Insert_Hash(*p);
}


void Traverse_HashTable()
{
cout<<"哈希地址0->"<<Hash_length-1<<endl;
for(int i=0;i<Hash_length;i++)
if(elem[i].key!=NULLKEY)
cout<<"元素的关键字值和它的标志分别是:"<<elem[i].key<<"  "<<elem[i].ord<<endl;


}


void Get_Data(int p)
{
cout<<"元素的关键字值和它的标志分别是:"<<elem[p].key<<"  "<<elem[p].ord<<endl;
}

};

(2)主函数

#include"myhash.h"
int main()
{
Elemtype r[12]={{17,1},{60,2},{29,3},{38,4},{1,5},{2,6},{3,7},{4,8},{5,9},{6,10},{7,11},{8,12}};
HashTable H;
int i,p,j;
KeyType k;
H.Init_HashTable();
for(i=0;i<11;i++) //插入前11个记录
{
j=H.Insert_Hash(r[i]);
if(j==-1)
cout<<"表中已有关键字为"<<r[i].key<<"  "<<r[i].ord<<"的记录"<<endl;
}


cout<<"按哈希地址顺序遍历哈希表"<<endl;
H.Traverse_HashTable();
cout<<endl;


cout<<"输入要查找的记录的关键字:";
cin>>k;
j=H.Search_Hash(k,p);
if(j==1)
H.Get_Data(p);
else
cout<<"无此记录"<<endl;


j=H.Insert_Hash(r[11]); //插入最后一个元素
if(j==0)
{
cout<<"插入失败"<<endl;
cout<<"需要重建哈希表才可以插入"<<endl;
cout<<"____重建哈希表____"<<endl;
H.Insert_Hash(r[i]); //重建后重新插入
}


cout<<"遍历重建后的哈希表"<<endl;
H.Traverse_HashTable();
cout<<endl;


cout<<"输入要查找的记录的关键字:";
cin>>k;
j=H.Search_Hash(k,p);
if(j==1)
H.Get_Data(p);
else
cout<<"该记录不存在"<<endl;


cout<<"____销毁哈希表____"<<endl;
H.Destroy_HashTable();


return 0;
}

(3)测试结果

按哈希地址顺序遍历哈希表
哈希地址0->10
元素的关键字值和它的标志分别是:5  9
元素的关键字值和它的标志分别是:1  5
元素的关键字值和它的标志分别是:2  6
元素的关键字值和它的标志分别是:3  7
元素的关键字值和它的标志分别是:4  8
元素的关键字值和它的标志分别是:60  2
元素的关键字值和它的标志分别是:17  1
元素的关键字值和它的标志分别是:29  3
元素的关键字值和它的标志分别是:38  4
元素的关键字值和它的标志分别是:6  10
元素的关键字值和它的标志分别是:7  11


输入要查找的记录的关键字:5
元素的关键字值和它的标志分别是:5  9
____重建hash表_____
插入失败
需要重建哈希表才可以插入
____重建哈希表____
遍历重建后的哈希表
哈希地址0->18
元素的关键字值和它的标志分别是:38  4
元素的关键字值和它的标志分别是:1  5
元素的关键字值和它的标志分别是:2  6
元素的关键字值和它的标志分别是:3  7
元素的关键字值和它的标志分别是:4  8
元素的关键字值和它的标志分别是:5  9
元素的关键字值和它的标志分别是:60  2
元素的关键字值和它的标志分别是:6  10
元素的关键字值和它的标志分别是:7  11
元素的关键字值和它的标志分别是:8  12
元素的关键字值和它的标志分别是:29  3
元素的关键字值和它的标志分别是:17  1


输入要查找的记录的关键字:7
元素的关键字值和它的标志分别是:7  11
____销毁哈希表____
Press any key to continue

5.BitMap操作

什么是Bit-map

    所谓的Bit-map就是用一个bit位来标记某个元素对应的Value, 而Key即是该元素。由于采用了Bit为单位来存储数据,因此在存储空间方面,可以大大节省。

    如果说了这么多还没明白什么是Bit-map,那么我们来看一个具体的例子,假设我们要对0-7内的5个元素(4,7,2,5,3)排序(这里假设这些元素没有重复)。那么我们就可以采用Bit-map的方法来达到排序的目的。要表示8个数,我们就只需要8个Bit(1Bytes),首先我们开辟1Byte的空间,将这些空间的所有Bit位都置为0(如下图:)

    然后遍历这5个元素,首先第一个元素是4,那么就把4对应的位置为1(可以这样操作 p+(i/8)|(0×01<<(i%8)) 当然了这里的操作涉及到Big-ending和Little-ending的情况,这里默认为Big-ending),因为是从零开始的,所以要把第五位置为一(如下图):

      

然后再处理第二个元素7,将第八位置为1,,接着再处理第三个元素,一直到最后处理完所有的元素,将相应的位置为1,这时候的内存的Bit位的状态如下:

然后我们现在遍历一遍Bit区域,将该位是一的位的编号输出(2,3,4,5,7),这样就达到了排序的目的。下面的代码给出了一个BitMap的用法:排序。

//定义每个Byte中有8个Bit位
#include <memory.h>
#define BYTESIZE 8
void SetBit(char *p, int posi)
{
	for(int i=0; i < (posi/BYTESIZE); i++)
	{
		p++;
	}

	*p = *p|(0x01<<(posi%BYTESIZE));//将该Bit位赋值1
	return;
}

void BitMapSortDemo()
{
	//为了简单起见,我们不考虑负数
	int num[] = {3,5,2,10,6,12,8,14,9};

	//BufferLen这个值是根据待排序的数据中最大值确定的
	//待排序中的最大值是14,因此只需要2个Bytes(16个Bit)
	//就可以了。
	const int BufferLen = 2;
	char *pBuffer = new char[BufferLen];

	//要将所有的Bit位置为0,否则结果不可预知。
	memset(pBuffer,0,BufferLen);
	for(int i=0;i<9;i++)
	{
		//首先将相应Bit位上置为1
		SetBit(pBuffer,num[i]);
	}

	//输出排序结果
	for(int i=0;i<BufferLen;i++)//每次处理一个字节(Byte)
	{
		for(int j=0;j<BYTESIZE;j++)//处理该字节中的每个Bit位
		{
			//判断该位上是否是1,进行输出,这里的判断比较笨。
			//首先得到该第j位的掩码(0x01<<j),将内存区中的
			//位和此掩码作与操作。最后判断掩码是否和处理后的
			//结果相同
			if((*pBuffer&(0x01<<j)) == (0x01<<j))
			{
				printf("%d ",i*BYTESIZE + j);
			}
		}
		pBuffer++;
	}
}

int _tmain(int argc, _TCHAR* argv[])
{
	BitMapSortDemo();
	return 0;
}


6.几种排序算法的稳定性分析

首先,排序算法的稳定性大家应该都知道,通俗地讲就是能保证排序前2个相等的数其在序列的前后位置顺序和排序后它们两个的前后位置顺序相同。在简单形式化一下,如果Ai =Aj, Ai原来在位置前,排序后Ai还是要在Aj位置前。

     其次,说一下稳定性的好处。排序算法如果是稳定的,那么从一个键上排序,然后再从另一个键上排序,第一个键排序的结果可以为第二个键排序所用。基数排序就 是这样,先按低位排序,逐次按高位排序,低位相同的元素其顺序再高位也相同时是不会改变的。另外,如果排序算法稳定,对基于比较的排序算法而言,元素交换 的次数可能会少一些(个人感觉,没有证实)。

回到主题,现在分析一下常见的排序算法的稳定性,每个都给出简单的理由。

   (1)冒泡排序

       冒泡排序就是把小的元素往前调或者把大的元素往后调。比较是相邻的两个元素比较,交换也发生在这两个元素之间。所以,如果两个元素相等,我想你是不会再无 聊地把他们俩交换一下的;如果两个相等的元素没有相邻,那么即使通过前面的两两交换把两个相邻起来,这时候也不会交换,所以相同元素的前后顺序并没有改 变,所以冒泡排序是一种稳定排序算法。

(2)选择排序

      选择排序是给每个位置选择当前元素最小的,比如给第一个位置选择最小的,在剩余元素里面给第二个元素选择第二小的,依次类推,直到第n-1个元素,第n个 元素不用选择了,因为只剩下它一个最大的元素了。那么,在一趟选择,如果当前元素比一个元素小,而该小的元素又出现在一个和当前元素相等的元素后面,那么 交换后稳定性就被破坏了。比较拗口,举个例子,序列5 8 5 2 9, 我们知道第一遍选择第1个元素5会和2交换,那么原序列中2个5的相对前后顺序就被破坏了,所以选择排序不是一个稳定的排序算法。

(3)插入排序
     插入排序是在一个已经有序的小序列的基础上,一次插入一个元素。当然,刚开始这个有序的小序列只有1个元素,就是第一个元素。比较是从有序序列的末尾开 始,也就是想要插入的元素和已经有序的最大者开始比起,如果比它大则直接插入在其后面,否则一直往前找直到找到它该插入的位置。如果碰见一个和插入元素相 等的,那么插入元素把想插入的元素放在相等元素的后面。所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,所以插入排序是稳 定的。

(4)快速排序
    快速排序有两个方向,左边的i下标一直往右走,当a[i] <=a[center_index],其中center_index是中枢元素的数组下标,一般取为数组第0个元素。而右边的j下标一直往左走,当a[j] >a[center_index]。如果i和j都走不动了,i<= j, 交换a[i]和a[j],重复上面的过程,直到i>j。 交换a[j]和a[center_index],完成一趟快速排序。在中枢元素和a[j]交换的时候,很有可能把前面的元素的稳定性打乱,比如序列为 5 3 3 4 3 8 9 10 11, 现在中枢元素5和3(第5个元素,下标从1开始计)交换就会把元素3的稳定性打乱,所以快速排序是一个不稳定的排序算法,不稳定发生在中枢元素和a[j] 交换的时刻。

(5)归并排序
    归并排序是把序列递归地分成短序列,递归出口是短序列只有1个元素(认为直接有序)或者2个序列(1次比较和交换),然后把各个有序的段序列合并成一个有 序的长序列,不断合并直到原序列全部排好序。可以发现,在1个或2个元素时,1个元素不会交换,2个元素如果大小相等也没有人故意交换,这不会破坏稳定 性。那么,在短的有序序列合并的过程中,稳定是是否受到破坏?没有,合并过程中我们可以保证如果两个当前元素相等时,我们把处在前面的序列的元素保存在结 果序列的前面,这样就保证了稳定性。所以,归并排序也是稳定的排序算法。

(6)基数排序
   基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优 先级排序,最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以其是稳定的排序算法。

(7)希尔排序(shell)
    希尔排序是按照不同步长对元素进行插入排序,当刚开始元素很无序的时候,步长最大,所以插入排序的元素个数很少,速度很快;当元素基本有序了,步长很小, 插入排序对于有序的序列效率很高。所以,希尔排序的时间复杂度会比o(n^2)好一些。由于多次插入排序,我们知道一次插入排序是稳定的,不会改变相同元 素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以shell排序是不稳定的。

(8)堆排序
   我们知道堆的结构是节点i的孩子为2*i和2*i+1节点,大顶堆要求父节点大于等于其2个子节点,小顶堆要求父节点小于等于其2个子节点。在一个长为n 的序列,堆排序的过程是从第n/2开始和其子节点共3个值选择最大(大顶堆)或者最小(小顶堆),这3个元素之间的选择当然不会破坏稳定性。但当为n/2-1, n/2-2, ...1这些个父节点选择元素时,就会破坏稳定性。有可能第n/2个父节点交换把后面一个元素交换过去了,而第n/2-1个父节点把后面一个相同的元素没 有交换,那么这2个相同的元素之间的稳定性就被破坏了。所以,堆排序不是稳定的排序算法。

综上,得出结论: 选择排序、快速排序、希尔排序、堆排序不是稳定的排序算法,而冒泡排序、插入排序、归并排序和基数排序是稳定的排序算法。


总结:

n比较小的时候,适合 插入排序和选择排序

基本有序的时候,适合 直接插入排序和冒泡排序

n很大但是关键字的位数较少时,适合 链式基数排序

n很大的时候,适合 快速排序 堆排序 归并排序

无序的时候,适合 快速排序

稳定的排序:冒泡排序 插入排序 归并排序 基数排序

复杂度是O(nlogn):快速排序 堆排序 归并排序

辅助空间(大 次大):归并排序 快速排序

好坏情况一样:简单选择(n^2),堆排序(nlogn),归并排序(nlogn)

最好是O(n)的:插入排序 冒泡排序

堆排序

算法思想简单描述:

堆排序是一种树形选择排序,是对直接选择排序的有效改进。

堆的定义如下:具有n个元素的序列(h1,h2,...,hn),当且仅当

满足(hi>=h2i,hi>=2i+1)或(hi<=h2i,hi<=2i+1)(i=1,2,...,n/2)

时称之为堆。在这里只讨论满足前者条件的堆。

由堆的定义可以看出,堆顶元素(即第一个元素)必为最大项。完全二叉树可以

很直观地表示堆的结构。堆顶为根,其它为左子树、右子树。

初始时把要排序的数的序列看作是一棵顺序存储的二叉树,调整它们的存储顺序,

使之成为一个堆,这时堆的根节点的数最大。然后将根节点与堆的最后一个节点

交换。然后对前面(n-1)个数重新调整使之成为堆。依此类推,直到只有两个节点

的堆,并对它们作交换,最后得到有n个节点的有序序列。

从算法描述来看,堆排序需要两个过程,一是建立堆,二是堆顶与堆的最后一个元素

交换位置。所以堆排序有两个函数组成。一是建堆的渗透函数,二是反复调用渗透函数

实现排序的函数。

堆排序是不稳定的。算法时间复杂度O(nlog2n)。

算法实现:

/*

功能:渗透建堆

输入:数组名称(也就是数组首地址)、参与建堆元素的个数、从第几个元素开始

*/

下标

0

1

2

3

4

5

6

7

8

9

元素

16

4

10

5

7

8

57

2

3

9

# include <stdio.h>
void sift(int *x, int n, int s)
{
int t, k, j;
t = *(x+s); /*暂存开始元素*/
k = s;   /*开始元素下标*/
j = 2*k + 1; /*左子树元素下标*/
while (j<n)
{
   if (j<n-1 && *(x+j) < *(x+j+1))/*用 j 标定两个儿子节点中的最大的那一个,然后将该节点与根

节点比较*/
   {
      j++;
   }
   if (t<*(x+j)) /*如果上述判断得到的最大的儿子节点大于根节点则调整*/
   {
      *(x+k) = *(x+j);
      k = j; /*调整后,开始元素也随之调整*/
      j = 2*k + 1;
   }
   else /*没有需要调整了,已经是个堆了,退出循环。*/
   {
      break;
   }
}
*(x+k) = t; /*开始元素放到它正确位置*/
}
/*
功能:堆排序
输入:数组名称(也就是数组首地址)、数组中元素个数
*/
void heap_sort(int *x, int n)
{
int i, k, t;
int *p;
for (i=n/2-1; i>=0; i--)//这个循环得到的只是大顶堆,符合堆的定义,父亲大于儿子节点。
{
   sift(x,n,i); /*初始建堆*/
}
for (k=n-1; k>=1; k--)//这一步根据上述的堆进行排序,使得输出序列是排序好的。
{
   t = *(x+0); /*堆顶放到最后*/
   *(x+0) = *(x+k);//将根节点与最后的元素交换,因为要得到从小到大的输出,所以这时候交换以后

最后一元素就是所有元素最大的,下步对前n-1个重新建堆
   *(x+k) = t;//将根节点与最后的元素交换
   sift(x,k,0); /*剩下的数再建堆*/ 
}
}
int main()
{
	int a[10]={6,1,3,5,8,7,9,2,4,0};
	int leng=10;
	heap_sort(a,leng);
	for(int i=0;i<10;i++)
	{
		printf("%d",a[i]);
	}
        return 0;
}


堆排序的动态演示:http://www.tyut.edu.cn/kecheng1/site01/suanfayanshi/heap_sort.asp

快速排序

  递归实现

//快速排序的整数实现 QUICKSORT(int []array)

#include "stdafx.h"
#include <stdio.h> 

void Swap(int &a,int &b) 
{ 
	int c= 0; 
	c = a; 
	a = b; 
	b = c; 
} 

void disp(int a[],int n) 
{ 
	for(int i=0;i <n;i++) 
	{
		printf( "%d",a[i]); 
		printf(" "); 
	}
} 
int partion(int a[],int L,int R)//下面两个过程 A 和 B 的顺序不能颠倒,否则会覆盖数据。
{
	int i=L;
	int j=R;
	int privot=a[L];
	while(i<j)
	{
		while(a[j]>=privot&&i<j)//过程A
		{
			j--;
		}
		a[i]=a[j];
		while(a[i]<=privot&&i<j)//过程B
		{
			i++;
		}
		a[j]=a[i];
	}
	a[i]=privot;
	return i;
}
void quicksort(int a[],int L,int r) 
{ 
	if(L>=r)   
		return; 
	int i; 
	i=partion(a,L,r); 
	quicksort(a,L,i-1); 
	quicksort(a,i+1,r); 
} 
int main() 
{ 
   int a[]={1,12,3,-4,8,6,10}; 
   quicksort(a,0,6); 
   disp(a,7); 
   getchar(); 
}

希尔排序

动画演示:http://student.zjzk.cn/course_ware/data_structure/web/flashhtml/shell.htm

#include <stdio.h> 
void ShellPass(int *R,int d,int n)
{                     
	for(int k=d;k<n;k++)
	{
		int i=k;
		while(i<n)
		{
			if(R[i]<R[i-d])//将第二个元素插入到正确的位置
			{
				int temp=R[i];
				R[i]=R[i-d];
				for(int j=i-d;temp<R[j];j=j-d)
					R[j+d]=R[j];
				R[j+d]=temp; 
			}
			i=i+d;
		}
	}
}
void ShellSort(int *R,int n)
{
    int increment=n;//增量初值,不妨设n>0
    do{
          increment=increment/3+1; //求下一增量
          ShellPass(R,increment,n); //一趟增量为increment的Shell插入排序
      }while(increment>1);
}//ShellSort
int main()
{
  int a[15]={49,38,102,97,76,13,180,49,55,4,34,58,44,65,990};
  ShellSort(a,sizeof(a)/sizeof(a[0]));
  for(int i=0;i<sizeof(a)/sizeof(a[0]);i++)
  {
	 printf("%d  ",a[i]);
  }
  return 0;
 }

基数排序:

1、单关键字和多关键字
     文件中任一记录R[i]的关键字均由d个分量
                      
构成。
若这d个分量中每个分量都是一个独立的关键字,则文件是多关键字的(如扑克牌有两个关键字:点数和花色);否则文件是单关键字的,
                
(0≤j<d)只不过是关键字中其中的一位(如字符串、十进制整数等)。
    多关键字中的每个关键字的取值范围一般不同。如扑克牌的花色取值只有4种,而点数则有13种。单关键字中的每位一般取值范围相同。

2、基数
      设单关键字的每个分量的取值范围均是:
      C0≤kj≤Crd-1(0≤j<d)
可能的取值个数rd称为基数。
     基数的选择和关键字的分解因关键宇的类型而异:
(1) 若关键字是十进制整数,则按个、十等位进行分解,基数rd=10,C0=0,C9=9,d为最长整数的位数;
(2) 若关键字是小写的英文字符串,则rd=26,Co='a',C25='z',d为字符串的最大长度。

3、基数排序的基本思想
     基数排序的基本思想是:从低位到高位依次对Kj(j=d-1,d-2,…,0)进行箱排序。在d趟箱排序中,所需的箱子数就是基数rd,这就是"基数排序"名称的由来。

4、基数排序的排序过程
     要排序的记录关键字取值范围是0到99之间的整数(36,5,16,98,95,47, 32,36,48)。对这些关键字进行基数排序的过程参见动画演示。

动画演示: http://student.zjzk.cn/course_ware/data_structure/web/flashhtml/jishupaixu.htm

 

#include<stdio.h>
#include<stdlib.h>
int main()
{
	int data[10]={73,22,93,43,55,14,28,65,39,81};
	int temp[10][10]={0};
	int order[10]={0};
	int i,j,k,n,lsd;
	k=0;n=1;
	printf("\n排序前: ");
	for (i=0;i<10;i++) printf("%d ",data[i]);
		putchar('\n');
	while (n<=10)
	{
		for (i=0;i<10;i++)
		{
			lsd=((data[i]/n)%10);
			temp[lsd][order[lsd]]=data[i];
			order[lsd]++;
		}
		printf("\n重新排列: ");
		for (i=0;i<10;i++)
		{
		if(order[i]!=0)
		for (j=0;j<order[i];j++)
		{
		data[k]=temp[i][j];
		printf("%d ",data[k]);
		k++;
		}
		order[i]=0;
		}
		n*=10;
		k=0;
	}
	putchar('\n');
	printf("\n排序后: ");
	for(i=0;i<10;i++)
		printf("%d ",data[i]);
	return 0;

7.搜索引擎架构

1.搜索器

一个搜索引擎由搜索器、索引器、检索器和用户接口等四个部分组成。   

    1.搜索器   搜索器的功能是在互联网中漫游,发现和搜集信息。它常常是一个计算机程序,日夜不停地运行。它要尽可能多、尽可能快地搜集各种类型的新信息,同时因为互联网上的信息更新很快,所以还要定期更新已经搜集过的旧信息,以避免死连雍臀扌Я印D壳坝辛街炙鸭畔⒌牟呗裕?从一个起始URL集合开始,顺着这些URL中的超链(Hyperlink),以宽度优先、深度优先或启发式方式循环地在互联网中发现信息。这些起始URL可以是任意的URL,但常常是一些非常流行、包含很多链接的站点(如Yahoo!)。将Web空间按照域名、IP地址或国家域名划分,每个搜索器负责一个子空间的穷尽搜索。 搜索器搜集的信息类型多种多样,包括HTML、XML、Newsgroup文章、FTP文件、字处理文档、多媒体信息。 搜索器的实现常常用分布式、并行计算技术,以提高信息发现和更新的速度。商业搜索引擎的信息发现可以达到每天几百万网页。

    2.索引器   索引器的功能是理解搜索器所搜索的信息,从中抽取出索引项,用于表示文档以及生成文档库的索引表。 索引项有客观索引项和内容索引项两种:客观项与文档的语意内容无关,如作者名、URL、更新时间、编码、长度、链接流行度(Link Popularity)等等;内容索引项是用来反映文档内容的,如关键词及其权重、短语、单字等等。内容索引项可以分为单索引项和多索引项(或称短语索引项)两种。单索引项对于英文来讲是英语单词,比较容易提取,因为单词之间有天然的分隔符(空格);对于中文等连续书写的语言,必须进行词语的切分。 在搜索引擎中,一般要给单索引项赋与一个权值,以表示该索引项对文档的区分度,同时用来计算查询结果的相关度。使用的方法一般有统计法、信息论法和概率法。短语索引项的提取方法有统计法、概率法和语言学法。 索引表一般使用某种形式的倒排表(Inversion List),即由索引项查找相应的文档。索引表也可能要记录索引项在文档中出现的位置,以便检索器计算索引项之间的相邻或接近关系(proximity)。   索引器可以使用集中式索引算法或分布式索引算法。当数据量很大时,必须实现即时索引(Instant Indexing),否则不能够跟上信息量急剧增加的速度。索引算法对索引器的性能(如大规模峰值查询时的响应速度)有很大的影响。一个搜索引擎的有效性在很大程度上取决于索引的质量。 

为什么我们要说倒排索引呢? 
    因为倒排索引是目前 搜索引擎公司最对搜索引擎最常用的存储方式.也是搜索引擎的核心内容!
    在搜索引擎实际的引用之中,有时需要按照关键字的某些值查找记录,所以我们是按照关键字建立索引,这个索引我们就称之为:倒排索引, 而带有倒排索引的文件我们又称作: 倒排索引文件 也可以叫它为: 倒排文件 来实现快速的检索与高速的效率!

那想问下 什么是倒排表呢?
     倒排文件中的 次关键字索引 我们称做: 倒排表
     其主要优点是: 在处理复杂的多关键字查询时,可在倒排表中先完成查询的交、并等逻辑运算,得到结果后再对记录进行存取。这样不必对每个记录随机存取,把对记录的查询转换为地址集合的运算,从而提高查找速度!



下面就是整个倒排表的建立过程(组图):

  数据表

 
索引表
 
右项归并后的索引表
 

那我最后问下 我们因该怎样建立倒排索引呢?

 

    3.检索器   检索器的功能是根据用户的查询在索引库中快速检出文档,进行文档与查询的相关度评价,对将要输出的结果进行排序,并实现某种用户相关性反馈机制.检索器常用的信息检索模型有集合理论模型、代数模型、概率模型和混合模型四种。   

    4.用户接口   用户接口的作用是输入用户查询、显示查询结果、提供用户相关性反馈机制。主要的目的是方便用户使用搜索引擎,高效率、多方式地从搜索引擎中得到有效、及时的信息。用户接口的设计和实现使用人机交互的理论和方法,以充分适应人类的思维习惯。 用户输入接口可以分为简单接口和复杂接口两种。 简单接口只提供用户输入查询串的文本框;复杂接口可以让用户对查询进行限制,如逻辑运算(与、或、非;+、-)、相近关系(相邻、NEAR)、域名范围(如.edu、.com)、出现位置(如标题、内容)、信息时间、长度等等。目前一些公司和机构正在考虑制定查询选项的标准。

二、搜索引擎的实现原理,可以看作四步:从互联网上抓取网页→建立索引数据库→在索引数据库中搜索→对搜索结果进行处理和排序。而搜索引擎的策略都是采用服务器群集和分布式计算技术,其是面向互联网访问者的。

三、实例——  对新闻搜索

“用户”通过提交查询请求给“查询服务器”,服务器在“索引数据库”中进行相关网页的查找,同时“网页评级”把查询请求和链接信息结合起来对搜索结果进行相关度的评价,通过“查询服务器”按照相关度进行排序,并提取关键词的内容摘要,组织最后的页面返回给“用户

   首先,我们提交要搜索的关键字,其搜索引擎就会经过查询处理与分词(我觉得这里的关键问题就是词法和语义分析),然后由搜索系统程序从网页索引数据库中找到符合该关键词的所有相关网页。最后综合相关信息和网页级别形成相关度数值,然后进行排序,相关度越高,排名越靠前。最后由页面生成系统将搜索结果的链接地址和页面内容摘要等内容组织起来返回给用户。


8.附十道海量数据面试题(转)

第一部分、十道海量数据处理面试题

   1、海量日志数据,提取出某日访问百度次数最多的那个IP。

  此题,在我之前的一篇文章 算法里头有所提到,当时给出的方案是:IP的数目还是有限的,最多2^32个,所以可以考虑使用hash将ip直接存入内存,然后进行统计。

  再详细介绍下此方案:首先是这一天,并且是访问百度的日志中的IP取出来,逐个写入到一个大文件中。注意到IP是32位的,最多有个2^32个IP。 同样可以采用映射的方法,比如模1000,把整个大文件映射为1000个小文件,再找出每个小文中出现频率最大的IP(可以采用hash_map进行频率 统计,然后再找出频率最大的几个)及相应的频率。然后再在这1000个最大的IP中,找出那个频率最大的IP,即为所求。

  2、搜索引擎会通过日志文件把用户每次检索使用的所有检索串都记录下来,每个查询串的长度为1-255字节。

  假设目前有一千万个记录(这些查询串的重复度比较高,虽然总数是1千万,但如果除去重复后,不超过3百万个。一个查询串的重复度越高,说明查询它的用户越多,也就是越热门。),请你统计最热门的10个查询串,要求使用的内存不能超过1G。

  典型的Top K算法,还是在这篇文章里头有所阐述。 文中,给出的最终算法是:第一步、先对这批海量数据预处理,在O(N)的时间内用Hash表完成排序;然后,第二步、借助堆这个数据结构,找出Top K,时间复杂度为N‘logK。 即,借助堆结构,我们可以在log量级的时间内查找和调整/移动。因此,维护一个K(该题目中是10)大小的小根堆,然后遍历300万的Query,分别 和根元素进行对比所以,我们最终的时间复杂度是:O(N) + N'*O(logK),(N为1000万,N’为300万)。ok,更多,详情,请参考原文。

  或者:采用trie树,关键字域存该查询串出现的次数,没有出现为0。最后用10个元素的最小推来对出现频率进行排序。

  3、有一个1G大小的一个文件,里面每一行是一个词,词的大小不超过16字节,内存限制大小是1M。返回频数最高的100个词。

  方案:顺序读文件中,对于每个词x,取hash(x)%5000,然后按照该值存到5000个小文件(记为x0,x1,...x4999)中。这样每个文件大概是200k左右。

  如果其中的有的文件超过了1M大小,还可以按照类似的方法继续往下分,直到分解得到的小文件的大小都不超过1M。 对每个小文件,统计每个文件中出现的词以及相应的频率(可以采用trie树/hash_map等),并取出出现频率最大的100个词(可以用含100个结 点的最小堆),并把100个词及相应的频率存入文件,这样又得到了5000个文件。下一步就是把这5000个文件进行归并(类似与归并排序)的过程了。

  4、有10个文件,每个文件1G,每个文件的每一行存放的都是用户的query,每个文件的query都可能重复。要求你按照query的频度排序。

  还是典型的TOP K算法,解决方案如下: 方案1: 顺序读取10个文件,按照hash(query)%10的结果将query写入到另外10个文件(记为)中。这样新生成的文件每个的大小大约也1G(假设 hash函数是随机的)。 找一台内存在2G左右的机器,依次对用hash_map(query, query_count)来统计每个query出现的次数。利用快速/堆/归并排序按照出现次数进行排序。将排序好的query和对应的 query_cout输出到文件中。这样得到了10个排好序的文件(记为)。

  对这10个文件进行归并排序(内排序与外排序相结合)。

  方案2: 一般query的总量是有限的,只是重复的次数比较多而已,可能对于所有的query,一次性就可以加入到内存了。这样,我们就可以采用trie树/hash_map等直接来统计每个query出现的次数,然后按出现次数做快速/堆/归并排序就可以了。

  方案3: 与方案1类似,但在做完hash,分成多个文件后,可以交给多个文件来处理,采用分布式的架构来处理(比如MapReduce),最后再进行合并。

  5、 给定a、b两个文件,各存放50亿个url,每个url各占64字节,内存限制是4G,让你找出a、b文件共同的url?

  方案1:可以估计每个文件安的大小为5G×64=320G,远远大于内存限制的4G。所以不可能将其完全加载到内存中处理。考虑采取分而治之的方法。

  遍历文件a,对每个url求取hash(url)%1000,然后根据所取得的值将url分别存储到1000个小文件(记为a0,a1,...,a999)中。这样每个小文件的大约为300M。

  遍历文件b,采取和a相同的方式将url分别存储到1000小文件(记为b0,b1,...,b999)。这样处理后,所有可能相同的url都在对应 的小文件(a0vsb0,a1vsb1,...,a999vsb999)中,不对应的小文件不可能有相同的url。然后我们只要求出1000对小文件中相 同的url即可。

  求每对小文件中相同的url时,可以把其中一个小文件的url存储到hash_set中。然后遍历另一个小文件的每个url,看其是否在刚才构建的hash_set中,如果是,那么就是共同的url,存到文件里面就可以了。

  方案2:如果允许有一定的错误率,可以使用Bloom filter,4G内存大概可以表示340亿bit。将其中一个文件中的url使用Bloom filter映射为这340亿bit,然后挨个读取另外一个文件的url,检查是否与Bloom filter,如果是,那么该url应该是共同的url(注意会有一定的错误率)。

  Bloom filter日后会在本BLOG内详细阐述。

  6、在2.5亿个整数中找出不重复的整数,注,内存不足以容纳这2.5亿个整数。

  方案1:采用2-Bitmap(每个数分配2bit,00表示不存在,01表示出现一次,10表示多次,11无意义)进行,共需内存内存,还可以接 受。然后扫描这2.5亿个整数,查看Bitmap中相对应位,如果是00变01,01变10,10保持不变。所描完事后,查看bitmap,把对应位是 01的整数输出即可。

  方案2:也可采用与第1题类似的方法,进行划分小文件的方法。然后在小文件中找出不重复的整数,并排序。然后再进行归并,注意去除重复的元素。

  7、腾讯面试题:给40亿个不重复的unsigned int的整数,没排过序的,然后再给一个数,如何快速判断这个数是否在那40亿个数当中?

  与上第6题类似,我的第一反应时快速排序+二分查找。以下是其它更好的方法: 方案1: oo,申请512M的内存,一个bit位代表一个unsigned int值。读入40亿个数,设置相应的bit位,读入要查询的数,查看相应bit位是否为1,为1表示存在,为0表示不存在。

  dizengrong: 方案2: 这个问题在《编程珠玑》里有很好的描述,大家可以参考下面的思路,探讨一下:又因为2^32为40亿多,所以给定一个数可能在,也可能不在其中;这里我们把40亿个数中的每一个用32位的二进制来表示假设这40亿个数开始放在一个文件中。

  然后将这40亿个数分成两类: 1.最高位为0 2.最高位为1 并将这两类分别写入到两个文件中,其中一个文件中数的个数<=20亿,而另一个>=20亿(这相当于折半了);与要查找的数的最高位比较并接着进入相应的文件再查找

再然后把这个文件为又分成两类: 1.次最高位为0 2.次最高位为1

  并将这两类分别写入到两个文件中,其中一个文件中数的个数<=10亿,而另一个>=10亿(这相当于折半了); 与要查找的数的次最高位比较并接着进入相应的文件再查找。 ....... 以此类推,就可以找到了,而且时间复杂度为O(logn),方案2完。

  附: 这里,再简单介绍下,位图方法: 使用位图法判断整形数组是否存在重复 判断集合中存在重复是常见编程任务之一,当集合中数据量比较大时我们通常希望少进行几次扫描,这时双重循环法就不可取了。

  位图法比较适合于这种情况,它的做法是按照集合中最大元素max创建一个长度为max+1的新数组,然后再次扫描原数组,遇到几就给新数组的第几位置 上1,如遇到5就给新数组的第六个元素置1,这样下次再遇到5想置位时发现新数组的第六个元素已经是1了,这说明这次的数据肯定和以前的数据存在着重复。 这种给新数组初始化时置零其后置一的做法类似于位图的处理方法故称位图法。它的运算次数最坏的情况为2N。如果已知数组的最大值即能事先给新数组定长的话 效率还能提高一倍。

  8、怎么在海量数据中找出重复次数最多的一个?

   方案1:先做hash,然后求模映射为小文件,求出每个小文件中重复次数最多的一个,并记录重复次数。然后找出上一步求出的数据中重复次数最多的一个就是所求(具体参考前面的题)。

  9、上千万或上亿数据(有重复),统计其中出现次数最多的钱N个数据。

  方案1:上千万或上亿的数据,现在的机器的内存应该能存下。所以考虑采用hash_map/搜索二叉树/红黑树等来进行统计次数。然后就是取出前N个出现次数最多的数据了,可以用第2题提到的堆机制完成。

  10、一个文本文件,大约有一万行,每行一个词,要求统计出其中最频繁出现的前10个词,请给出思想,给出时间复杂度分析。

  方案1:这题是考虑时间效率。用trie树统计每个词出现的次数,时间复杂度是O(n*le)(le表示单词的平准长度)。然后是找出出现最频繁的前 10个词,可以用堆来实现,前面的题中已经讲到了,时间复杂度是O(n*lg10)。所以总的时间复杂度,是O(n*le)与O(n*lg10)中较大的 哪一个。

  附、100w个数中找出最大的100个数。

  方案1:在前面的题中,我们已经提到了,用一个含100个元素的最小堆完成。复杂度为O(100w*lg100)。

  方案2:采用快速排序的思想,每次分割之后只考虑比轴大的一部分,知道比轴大的一部分在比100多的时候,采用传统排序算法排序,取前100个。复杂度为O(100w*100)。

  方案3:采用局部淘汰法。选取前100个元素,并排序,记为序列L。然后一次扫描剩余的元素x,与排好序的100个元素中最小的元素比,如果比这个最 小的要大,那么把这个最小的元素删除,并把x利用插入排序的思想,插入到序列L中。依次循环,知道扫描了所有的元素。复杂度为O(100w*100)。



©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页