数据结构与算法

来源:互联网 发布:arm单片机介绍 编辑:程序博客网 时间:2024/06/05 06:22

整理和自己总结的部分数据结构和算法。

数据结构

队列

特点是FIFO。是一种常见的数据结构。可用链表和数组实现。
出队时,链表只需要给出链头并将链头重新指向即可,而数组则需要进行一次全数组移动的操作。
入队时,链表需要遍历一遍链表,数组则不需要。
扩容时,链表是需要遍历一次,而数组需要进行一次拷贝。

操作 链表实现复杂度 数组实现复杂度 出队 1 n 入队 n 1 扩容 n n

树的子树还是树;
度:节点的子树个数;
树的度:树中任意节点的度的最大值;
兄弟:两节点的parent相同;
层:根在第一层,以此类推;
高度:叶子节点的高度为1,根节点高度最高;
有序树:树中各个节点是有次序的;
森林:多个树组成;

二叉树

二叉树的数组存储下标公式:第i个节点的左右子节点是2i+1, 2i+2
满二叉树的深度为k,节点数是2的k次方-1
先序遍历:先访问根节点,再访问左节点,再访问右节点
中序遍历:左->根->右
后序遍历:左->右->根
中序遍历+另外一种遍历可以还原一棵二叉树。

二叉查找树:
1. 左子树的所有节点值都小于根节点值,右子树所有节点值都大于根节点值。
2. 左右子树也为二叉查找树。
3. 所有节点值都不重复。
4. 中序遍历是递增序。
5. 插入和删除操作。

红黑树 (有点看不懂,先放着)

某blog
一种平衡二叉查找树。C++ stl 中set, multiset, map , multimap(muti允许内部有重复元素)用了红黑树。
红黑树是每个节点都带有颜色属性的二叉查找树,颜色或红色或黑色。在二叉查找树强制一般要求以外,对于任何有效的红黑树我们增加了如下的额外要求:
1. 节点是红色或黑色。
2. 根节点是黑色。
3 每个叶节点(NIL节点,空节点)是黑色的。
4 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
5. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。

关键性质: 从根到叶子的最长的可能路径不多于最短的可能路径的两倍长。结果是这个树大致上是平衡的。因为操作比如插入、删除和查找某个值的最坏情况时间都要求与树的高度成比例,这个在高度上的理论上限允许红黑树在最坏情况下都是高效的,而不同于普通的二叉查找树。

旋转

旋转是为了让失去平衡的红黑树(比如插删操作后,左右子树的高度不一致了。)重新恢复平衡。
如图:
i am pic

左旋,如图所示(左->右),以x->y之间的链为“支轴”进行,
使y成为该新子树的根,x成为y的左孩子,而y的左孩子则成为x的右孩子。

重新着色

旋转后红黑色颜色特性被破坏了。需要重新着色。

某blog
也叫优先队列,是完全二叉树。大顶堆(小顶堆)是父节点大于(小于)两个子节点的值,且两个子节点也都是一个大顶堆(小顶堆)。
堆的数组表示:下标从1开始。对一个节点t,他的父节点为t/2,左孩子节点是2t,右孩子节点2t+1.
保持堆的性质(heapfy):左右子树都必须已经是堆(这里,只有一个节点的树显然是堆)。将root 与left, right比较,最大的值与root交换,比如是right,然后递归调整右子树。
数组转堆:数组长度为n,从堆的最后一个非叶子节点n/2的地方开始,往前进行heapfy操作。

void BuildHeap(int A[],)   {                int i;            for(i = HEAP_SIZE(A)/2; i>=1; i--)                   Heapify(A, i);   } 

优先队列(priority queue)

优先队列是一种基于堆实现的数据结构。其特性是队列中的元素是按照从大到小排列的。

pop操作:获取队列中的最大值

步骤有三步,时间复杂度为o(lg n):
1. 从堆中取堆顶元素,对应的是堆底层数据存储结构数组A[1](注意,A[0]是不用留空的。)
2. 将数组A最后一个元素A[n]放在A[1]的位置,同时将数组长度置为n-1.
3. 对数组从上往下往下进行heapfy操作。

push 操作:往队列中加入一个数。
步骤有两步,时间复杂度为o(lg n):
1. 在数组A尾部放入这个树。
2. 对该数组至下往上进行heapfy操作。

void Insert(int A[], int i)   {  //i为插入的值    int n=++HEAP_SIZE(A);         A[n] = -99999;//小无穷     int p = n;     while(p >1 && A[PARENT(p)] < i)  {       A[p] = A[PARENT(p)];       p = PARENT(p);      }      A[p]=i;  }   ```### 栈  [某文库](http://wenku.baidu.com/link?url=2EZUKGYzfpAZoYgx7h6SCHNqmBD8DOqHluQbtiVATV4ONrdvBKgQhLBenGjvrMhpSTDbcU-HubODxTKbTwdwC04kTekr0LIKdVRGcKeOq_e)FILO。懒得打字了。线性表的储存罢了。对栈的操作,感觉更像是一种特殊的约定。他们的操作更像是在线性的数组上认为的添加限制。即加入时只能加到数组头,移出时只能移除数组尾部。### 另,计算机的堆区和栈区  **堆区和栈区实际上不属于数据结构与算法的范畴了,应该是操作系统的部分。但是名字一样在这也补一个。懒得提炼直接复制了。**  [某blog](http://blog.csdn.net/slj_win/article/details/8608436)  一、预备知识—程序的内存分配    一个由c/C++编译的程序占用的内存分为以下几个部分   1、栈区(stack)— 由编译器自动分配释放   ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。    2、堆区(heap) — 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收     。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表,呵呵。      3、全局区(静态区)(static)—,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,      未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 - 程序结束后有系统释放       4、文字常量区—常量字符串就是放在这里的。 程序结束后由系统释放     5、程序代码区—存放函数体的二进制代码。   二、堆和栈的理论知识   2.1申请方式  stack:    由系统自动分配。 例如,声明在函数中一个局部变量 int b; 系统自动在栈中为b开辟空间   heap:    需要程序员自己申请,并指明大小,在c中malloc函数    如p1 = (char *)malloc(10);   在C++中用new运算符   如p2 = (char *)malloc(10);   但是注意p1、p2本身是在栈中的。  2.2申请后系统的响应    栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。     堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,   会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。   2.3申请大小的限制    栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因 此,能从栈获得的空间较小。   堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。   2.4申请效率的比较:   栈由系统自动分配,速度较快。但程序员是无法控制的。    堆是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便.   另外,在WINDOWS下,最好的方式是用VirtualAlloc分配内存,他不是在堆,也不是在栈是直接在进程的地址空间中保留一快内存,虽然用起来最不方便。但是速度快,也最灵活。  2.5堆和栈中的存储内容    栈: 在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。   当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。   堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排。  ## 排序算法### 插入思路:在一个有序的序列中,插入一个新的数,让这个序列依然有序。  插入排序是稳定的排序,即在一个序列中,如果有两个数相等,那么他们之间的前后顺序在排序后不变。  复杂度: 一般情况o(n2),最优情况排好序的情况o(n),最差情况o(n2),数列为逆序。   算法:  ![pic](http://img.my.csdn.net/uploads/201301/03/1357220939_7742.png)  ![递归版](http://img.my.csdn.net/uploads/201301/10/1357799464_6962.GIF)  ### 冒泡思想:通过两两交换,像水中的泡泡一样,小的先冒出来,大的后冒出来。是稳定排序。  复杂度:o(n2),没有最优或最差情况。  算法:  ![pic](http://img.my.csdn.net/uploads/201301/03/1357220942_6357.png)  ### 选择思路:在数列中,每次选择一个最小的值,放到新排列数列的最后面。     不稳定排序。    复杂度:o(n2)  算法:  ![pic](http://img.my.csdn.net/uploads/201301/10/1357793648_7622.GIF)### 归并思路:分治思想解决排序。  是稳定排序。  复杂度:o(n lgn)。  伪代码:  

mergeSort(A,i,j){
if i < j
mid = (i+j)/2
mergeSort(A,i,mid)
mergeSort(A, mid+1,j)
merge(A,i,m,g)
}

其中,merge的操作就是将两个基本有序的数列合并到一起。这个过程需要额外的一个数组来做临时变量。  算法:  ![pic](http://img.my.csdn.net/uploads/201301/03/1357220944_1498.png)### 快速**基本是必考的排序。**思路:利用分治的思想,将大问题化为两个小问题,然后再合并。首先在一个已排序的序列中,随便选择一个数。然后将比这个数大的放在这个数右边,小的放在左边。然后对这两个子区间再排序。  是不稳定的排序。  复杂度:o(nlgn) 没有最优情况,最差情况是每次选择都选择到了数组的最值,这个时候分区最不平衡。一般的实现上来讲,会发生在排序的数列是逆序或者所有数列都是相等的时候。    解决方法是三值取中法。算法:  ![pic](http://img.my.csdn.net/uploads/201301/03/1357220950_4864.png)### 堆思路:利用最大堆和最小堆进行排序。将数组整理为堆,然后每次弹出堆顶即可。  复杂度:o(nlg n)  ![pic](http://img.my.csdn.net/uploads/201301/03/1357220958_2074.png)### 基底思路:从个位到高位,对于每一位进行排序,而每一位的排序,都视为计数排序。  是稳定的排序,需要额外的空间。  例图:   ![pic](http://img.my.csdn.net/uploads/201301/03/1357220964_6225.png)复杂度:o((n+k)d)  算法:  ![pic](http://img.my.csdn.net/uploads/201301/03/1357220967_7660.png)### 桶思路:先预筛选再进行排序。把一个大的复杂的数列先按大小范围把不同元素放到不同的桶里面。然后对不同桶里面的数进行排序。  稳定排序,需要额外的空间。  复杂度:最优情况是o(n),这个时候所有的元素均匀分布到各个桶里面,而且桶内元素正好有序。最坏情况是全部元素都分到一个桶里面,为o(n2)或者o(nlg n)如果桶内排序用的是快排的话。  算法:    ![pic](http://img.my.csdn.net/uploads/201301/03/1357220972_2232.png)### 计数思路:利用数组的下标进行统计的排序。这个算法速度快(o(n),基本上这种有一定使用条件的排序算法速度上都会优于通用的排序方法。),需要额外的空间,不适用于有负数的排序和数组中含有特别大的数的数列。  复杂度:o(n+k),k为数列中最大的数。需要额外的空间复杂度。o(k)  是不稳定的排序。本质上是先统计在根据统计还原。和原来的数组已经没有关系了。   算法:    ![pic](http://img.my.csdn.net/uploads/201301/03/1357220962_8346.png)### ~~睡眠~~~~这是来搞笑的。= =~~~~构造n个线程,它们和这n个数一一对应。初始化后,线程们开始睡眠,等到对应的数那么多个时间单位后各自醒来,然后输出它对应的数。这样最小的数对应的线程最早醒来,这个数最早被输出。~~## 算法补遗[查找算法](http://blog.csdn.net/chinabhlt/article/details/47420391)### 二分查找思路:折办查找,元素必须是有序的才可以。用给定值k先与中间结点的关键字比较,中间结点把线形表分成两个子表,若相等则查找成功;若不相等,再根据k与该中间结点关键字的比较结果确定下一步查找哪个子表,这样递归进行,直到查找到或查找结束发现表中没有这样的结点。复杂度: o(lg n)  算法:```c++//二分查找(折半查找),版本1int BinarySearch1(int a[], int value, int n){  int low, high, mid;  low = 0;  high = n-1;  while(low<=high)  {    mid = (low+high)/2;    if(a[mid]==value)      return mid;    if(a[mid]>value)      high = mid-1;    if(a[mid]<value)      low = mid+1;  }  return -1;}//二分查找,递归版本int BinarySearch2(int a[], int value, int low, int high){  int mid = low+(high-low)/2;//这种求mid的写法是为了防止溢出,因为high+low是有可能溢出的。  if(a[mid]==value)    return mid;  if(a[mid]>value)    return BinarySearch2(a, value, low, mid-1);  if(a[mid]<value)    return BinarySearch2(a, value, mid+1, high);}<div class="se-preview-section-delimiter"></div>

partition中位数

这个算法是在乱序的数组中查找中位数的算法。
利用快排关键字的查找方法
a.随机选取一个关键字key,将序列二分;
b.若关键字的下标大于N/2,则继续对序列的左半部分执行partition;
c.若关键字的下标小于N/2,则继续对序列的右半部分执行partition;
d.若关键字的下标等于N/2,则返回key。
算法复杂度是o(nlg n)?.

基于流的中位数

提炼一下就是利用大小堆来存储这个流的数。在存储的过程中,大堆保存中位数左边的数,小堆保存中位数右边的数。在拿到流中的数时,动态调整(在新到达的数,大堆顶,小堆顶三个数中比较,然后选则将新的数送到哪个堆中)。需要取中位数数时只要取两个堆顶即可。
给一个数据流,找出中位数,由于数据流中的数据并不是有序的,所以我们首先应该想个方法让其有序。如果我们用vector来保存数据流的话,每进来一个新数据都要给数组排序,很不高效。所以之后想到用multiset这个数据结构,是有序保存数据的,但是它不能用下标直接访问元素,找中位数也不高效。这里用到的解法十分巧妙,我们使用大小堆来解决问题,其中大堆保存右半段较大的数字,小堆保存左半段较小的数组。这样整个数组就被中间分为两段了,由于堆的保存方式是由大到小,我们希望大堆里面的数据是从小到大,这样取第一个来计算中位数方便。我们用到一个小技巧,就是存到大堆里的数先取反再存,这样由大到小存下来的顺序就是实际上我们想要的从小到大的顺序。当大堆和小堆中的数字一样多时,我们取出大堆小堆的首元素求平均值,当小堆元素多时,取小堆首元素为中位数

最短路径

迪杰斯特拉
pic
pic

动态规划

不知道该怎么讲,丢个连接dp

蓄水池抽样

介绍

从一个长度未知或者很长的序列中随机抽取出k个元素,保证k个元素的输出是完全随机的。
构造一个可以放置k个元素的蓄水池,将序列的前k个元素放入蓄水池类,然后从第k+1个元素开始,以k/n的概率来决定钙元素是否需要被替换到水池中。

实现

伪代码如下:

Init : a reservoir with the size: k        for    i= k+1 to N            M=random(1, i);            if( M < k)                 SWAP the Mth value and ith value       end for<div class="se-preview-section-delimiter"></div>
证明
  1. 对于第i个数(i