数据结构提纲(1)

1 引言

1.1 数据结构

集合结构->线性结构->树形结构->图型结构
关系的存储

  • 顺序存储
  • 链接存储
  • 哈希存储方式
  • 索引存储方式

1.2 算法分析

时间复杂度:渐进分析法,大O表示法

1.3 最大连续子序列问题

找到和最大的连续序列

  • 穷举法:O(n3)
  • 优化掉最里层循环O(n2)
    在这里插入图片描述
  • O(n)开始子序列和不可能是负的,遍历一遍即可过滤掉不可能的情况。

3 栈

后进先出的线性表

3.1 栈的顺序实现

利用动态数组,栈底下标为0,用一个整型数储存栈顶的位置

3.2 栈的链接实现

将单链表的头指针指向栈顶

3.3 栈的应用

递归消除
括号配对
计算器

4 队列

先进先出的线性表

4.1 队列的顺序实现

队头位置固定:队头下标为0,出队操作后面的元素都要向前移动
队头位置不固定:入队出队操作是O(1),但是浪费空间
循环队列:规定front指向的单元不存储队列

4.1 队列的链接实现

单链表的表头是队头,单链表的表尾是队尾,保存一个链接队列只需要两个指向单链表结点指针front和rear
当队列为空时,front和rear均为NULL

5 树

5.1 树的概念

树的定义:
有一个根节点,其余节点可分成若干个互不相交的树
不含任何节点的是空树,由若干互不相交的树的集合是森林
一个结点的直接后继的数目称为结点的度

5.2 二叉树

5.2.1 基本概念

二叉树:二叉树或者为空,或者由根节点和互不相交的左右子树构成,且左右子树都是二叉树
满二叉树:一个高度为k并具有2k-1个结点的二叉树称为满二叉树。
完全二叉树:满二叉树最底层从右到左去掉若干个结点。
注意只有根节点的二叉树不是空二叉树,而且二叉树需严格区分左右子树。

5.2.2 二叉树的遍历

前序遍历:根节点->左子树->右子树
中序遍历:左子树->根节点->右子树
后序遍历:左子树->右子树->根节点
顺序遍历:一层一层的来
在这里插入图片描述
仅有前序遍历和后序遍历的结果无法确定唯一的二叉树。

5.2.3 二叉树的顺序实现

利用完全二叉树按层编号的性质,编号可表现父子关系
缺点:对于非完全二叉树,比较费空间

5.2.4 二叉树的链接实现

二叉链表
三叉链表:存放父节点、左右儿子结点

5.3 树的应用

5.3.1 表达式树

5.3.2 哈夫曼树和哈夫曼编码

由于在哈夫曼树中只有叶子结点和度为2的结点,对于n个待编码的元素,哈夫曼树中共有2n-1个结点,可以通过静态数组储存;struct结点储存待编码元素、结点权重、父节点儿子节点的下标位置。
申请一个长度为2n的数组,0表示树根,暂时不用;n个待编码的元素放大后n个位置

5.4 树和森林

5.4.1 树的存储实现

  • 树的标准储存:二叉树的扩展,一个儿子用一个指针储存
  • 儿子链表表示法:表头数组+链表表示
  • 儿子兄弟链表表示法:将任意一棵树表示成二叉树的形态
  • 双亲表示法:通过指向父节点的指针将树中的所有结点组织在一起

5.4.2 树的遍历

  • 前序遍历
  • 后序遍历
  • 层次遍历

5.4.3 树、森林和二叉树

和儿子兄弟链表表示法相似,将任意一个森林或者任意一棵树都能表示成二叉树

6 优先级队列

6.1 二叉堆

  • 最小化堆:根节点最小
  • 二叉堆的存储:顺序存储,利用数组下标反应父子关系与大小关系

6.1.1 优先级队列需要的操作

create():建堆,如果看成n次插入需要O(nlogn)的时间复杂度#
enquene():新元素入队
dequene():优先级最高的元素出队
getHead():返回头结点
isEmpty()
dequene过程中,定义向下过滤的函数percolateDown(int n),将最后一个结点换到根节点,将根节点和两个儿子中最小的一个比较,交换父子,一直到子节点为止。

6.2 D堆

为什么一般用二叉堆而不是D堆?
乘2除2用左移右移就好了,其他乘除会慢
为什么还会有人用D堆?
数据存在外存中,D堆可以减少读取外存次数

6.3 二项堆(贝努里树)

  • 归并:类似二进制加法,O(logn)
  • 入队:入队是归并的特例,相当于二进制加法中加一,最坏的时间复杂度是O(logn)
  • 出队:找到根最小的树O(logn),拆散的树再归并就好了

6.4 多服务台的排队系统

发生时间早的事件先处理,发生时间晚的后处理,用优先级队列存放事件,方式时间早的优先级高
事件驱动的仿真:每次优先级队列吐出一个事件:
到达事件:直接服务(生成离开时间)或者排队
离开事件:排队队列少一个元素或者空闲柜台+14

7 集合和静态查找表

7.1 集合的定义

集合中的元素没有任何逻辑关系。
键值/关键字值:集合元素的唯一标识

7.2 查找表

  • 查找表:用于查找的集合
  • 静态查找表:数据元素个数和每个数据元素的值不变,用数组即可
  • 动态查找表:会动态变化,要进行插入删除操作
  • 内部查找:数据在内存
  • 外部查找:数据在外存

7.3 无序表的查找

无序表:数组中的元素是无序的。
无序表的查找:顺序查找O(n)的复杂度
改进的顺序查找只需要n+1次查找,而不是2n。

7.4 有序表的查找

  • 顺序查找 O(n)
  • 二分查找
  • 插值查找
    为什么不用插值查找?
    计算一次运算量太大,不如二分查找右移
    为什么还会有人用插值查找?
    当数据在外存时,可以减少读取数据的次数
  • 分块查找(索引查找)
    类似于字典,利用索引表,先确定大致位置

8 动态查找表

需要支持查找、增、删操作,线性表不适合动态查找表。
查找树:用于处理动态查找表的树

8.1 二叉查找树(二叉排序树)

左子树的所有元素<根节点<右子树所有元素
中序遍历二叉查找树即可得到递增次序

  • find:查找,O(logn)
  • insert:插入,先执行查找算法,找出被插结点的父亲结点,再插入为左右结点O(logn)
  • remove:删除,也是O(logn)
    如何删除?
    叶子结点和只有一个子节点的很容易删除
    对于有两个子节点的结点,用左子树的最大值作为替身,因为左子树的最大值一定没有右儿子。
  • 当二叉查找树极不平衡时,退化成单列表,为线性复杂度。

8.2 AVL树(平衡二叉查找树)

  • 平衡因子(平衡度):结点的平衡度是结点左子树高度-右子树高度
  • AVL树:左右子树高度最多差一层
    每个结点需要保存平衡信息
  • find
  • insert:有可能会影响平衡性,旋转一下就好了,最多只用调整一个结点
    LL/RR:
    在这里插入图片描述
    LR/RL:
    在这里插入图片描述

删除操作:删除后不平衡,需要调整,之后高度变矮,可能父节点也要调整,最后一直可能都要调整。
如果要删除结点x,有可能x是叶节点,有可能x只有一个儿子,有可能x有两个儿子。前两种情况x的父节点的相应子树的高度减一。
当x有两个儿子时,用右子树的最小结点或者左子树的最大结点代替x即可。

8.3 伸展树

90-10规则:90%的访问都是针对10%的数据
向根旋转策略:被访问的数据元素向父节点旋转,朝根节点移动。
希望自底向上的伸展使树更加平衡,针对不同的情况有三种不同的旋转:zig,zig-zig和zig-zag

8.3.1 zig

在这里插入图片描述

8.3.2 zig-zag

在这里插入图片描述

8.3.2 zig-zig

在这里插入图片描述

9 散列表

哈希法:也称散列法,根据所求结点的关键字值key直接直接找到这个结点,时间复杂度为O(1)
散列函数(hash function):将一个项映射成一个较小的下标的函数,即定义域大,值域小
哈希函数的要求:计算速度快,散列地址尽可能均匀,使冲突的概率降低

9.1 哈希函数:

直接地址法
取模运算(用得最多):p最好是质数,这样更均匀一些
数字分析法
平方取中法:4731*4731=22,382,361,所以取中间两位82
折叠法:对于542,242,241,542+242+241=1025,所以取025

9.2 冲突解决

  • 线性探测法:当散列发生冲突时,探测下一个单元,直到发现一个空单元
  • 二次探测法
  • 再次散列法
  • 开散列表:链地址法:

9.3 闭散列表类

支持三个操作:插入、查找和删除
必须提供一个指向函数的指针,从数据元素中提取关键词字段并转换成整型数
删除:迟删除,只是做一个标记

9.4 二次探测法

地址序列为k+12,, k+22,
如果采用二次探测法,而且表的大小是一个素数,那么如果表至少有一半是空的,新元素总能被插入,而且在插入过程中没有一个单元被探测两次。
这个表还要存储负载因子这个参数

9.5 开散列表

指针数组+不带头节点的单链表实现

10 排序

  • 内排序:数据元素全部存放在计算机的内存之中
  • 外排序:数据元素主要存放在外存储器
    排序的大致分类:
  • 插入排序
  • 选择排序:把最小元素选择出来,一个一个拿出来
  • 交换排序:不另外申请空间,光交换
  • 归并排序:利用分治法
  • 基数排序

10.1 插入排序

  • 直接插入排序
  • 折半插入排序:查找采用二分查找,但是搬家还是一个一个搬,所以时间复杂度还是O(n2)
  • 希尔排序:对于相隔距离为gap的元素进行排序,直到gap=1,
    为什么希尔排序交换效率比直接插入排序高?
    因为插入排序最好的情况就是不操作,到最后序列越来越有序,应用希尔增量,最坏的时间复杂度是O(n2),平均时间复杂度是O(n3/2)

10.2 选择排序

依次选出最小的元素

  • 直接选择排序
    时间复杂度O(n2)
  • 堆排序
    建堆用O(n)的时间,基于堆的优先级队列选出最小元素只需要O(logn),所以总的时间是O(nlogn),没有最好最坏情况
    空间问题:需要另申请n个空间吗?
    不需要,每次dequeue之后堆缩小1,可以在堆的最后一个位置存储刚被删去的元素。
template <class KEY, class OTHER>
void heapSort(SET<KEY, OTHER> a[], int size){
    int  i; 
    SET<KEY, OTHER> tmp;
    // 创建初始的堆
    for(  i = size / 2 - 1; i >= 0; i-- )
	    percolateDown( a, i, size );
   //执行n-1次deQueue
    for ( i = size - 1; i > 0; --i) {
	    tmp = a[0]; a[0] = a[i]; a[i] = tmp;       //delete  a[0]
	    percolateDown( a, 0, i );
   }	
}
template <class KEY, class OTHER>
void percolateDown(SET<KEY, OTHER> a[], int hole, int size){
    int child;
    SET<KEY, OTHER> tmp = a[ hole ];
	    
    for( ; hole * 2 + 1 < size; hole = child){
       child = hole * 2 + 1;
	   if( child != size - 1 && a[ child + 1 ].key > a[ child ].key )
	         child++;
	    if( a[ child ].key > tmp.key )   a[ hole ] = a[ child ];
	    else    break;
    }
    a[ hole ] = tmp;
}

10.3 交换排序

  • 冒泡排序:O(n2)
    第i次起泡需要n-i个次比较,最坏n-i次交换,最坏需要n-1次起泡。
  • 快速排序:分治法,递归实现,
    在待排序的序列中选择一个标准元素,把所有数据元素分成两组
    如何选择标准元素?
    序列第一个元素:对于增序列或者减序列会变成直接选择排序
    随机选一个元素
    或者选择序列的中间值
    在这里插入图片描述
    时间复杂度:最坏是O(n2),平均情况和最好情况是O(nlogn)

10.4 归并排序:

分治法:先划分再归并,没有最好或者最坏的情况

10.5 基数排序:非比较的算法

设待排序的元素组成一个不带头节点的单链表,每个口袋也用一个不带头节点,可以达到近似线性的时间复杂度

template <class OTHER>
struct node {
    SET<int, OTHER> data;
    node *next;
 
    node() { next = NULL; }
    node(SET<int, OTHER> d): data(d)
          { next = NULL; }
};

template <class OTHER>
void bucketSort(node<OTHER> *&p){  // p是链表头
    node<OTHER> *bucket[10], *last[10], *tail ;        
    int i, j, k, base = 1, max = 0, len = 0;
    for (tail = p; tail != NULL; tail = tail->next)        // 找最大键值
	  if (tail->data.key > max) max = tail->data.key;

    // 寻找最大键值的位数
    if (max == 0) len = 0;
    else while (max > 0) { ++len; max /= 10; }
 for (i = 1; i <= len; ++i) {                 // 执行len次的分配与重组
       for (j = 0; j <= 9; ++j) bucket[j] = last[j] = NULL;      
       while (p != NULL) {                                  // 执行一次分配
  	      k = p->data.key / base % 10;                    
	      if (bucket[k] == NULL)  bucket[k] = last[k] = p;
	      else last[k] = last[k]->next = p; 
	      p = p->next;
      }
      p = NULL;                                             // 重组后的链表头
      for (j = 0; j <= 9; ++j) {                                  // 执行重组
 	     if (bucket[j] == NULL) continue;
	     if (p == NULL)  p = bucket[j]; 
	     else tail->next = bucket[j];
	     tail = last[j];
      }
      tail->next = NULL;                   // 表尾置空
      base *= 10;                          // 为下一次分配做准备
    }
}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值