数据结构与算法(八)-堆、图、字符串匹配算法
1.堆(Heap)
1.1 堆的定义
1、堆是一颗完全二叉树;(处理)
2、堆中的某个结点的值总是大于等于或小于等于子树的任意节点的值。
3、堆中每个结点的子树都是堆树。
1.1.1 堆的分类
- 大顶堆:堆中的任意节点值大于等于子树的任意节点值
- 小顶堆:堆中的任意节点值小于等于子树的任意节点值
1.1.2 堆的存储结构
堆的存储一般使用数组来进行存储,数组顺序存储堆。因为堆是完全二叉树,所以它满足i
(i>1)节点的父节点下标为(i-1)/2
,同时i
节点的左子节点下标为2*i+1
,右子节点下标为2*i + 2
1.2 堆的基本操作
1.2.1 建立
如果以数组存储元素时,一个数组具有对应的树表示形式,但树并不满足堆的条件,需要重新排列元素,可以建立“堆化”的树。
如果插入或删除
后的数据没有满足堆的特性(大顶堆、小顶堆),就得对堆进行调整,这个过程就叫做堆化。堆化有两种:自底往上和自顶向下
1.2.2 插入
堆中插入一个元总是在堆尾部
插入元素(数组末尾),那么为了使得插入数据后的堆仍然是符合堆特性,就得使用自底向上
的方法
自底向上:以大顶堆为例。当插入数据时,把新数据插入到数组的末尾,通过计算公式得到新数据的父节点位置,把当前数据与父节点的数据进行比较,大于父节点数据就与父节点位置进行互换,然后再把父节点数据作为当前节点数据继续堆化。
1.2.3 删除
堆中删除一个元总是删除堆顶
元素,那么为了使得被删除后的堆仍然是完全二叉树,就得使用自顶向下
的方法
自顶向下:以大顶堆为例。当删除数据时,把堆顶元素与堆尾元素进行互换,然后除去最后一个元素,再对剩下的元素进行堆化操作
1.3 堆操作的实现
对应的方法和操作如下:
public class Heap {
/**
* 创建堆
*/
//创建一个存储堆中元素的数组
private int[] data;
//堆中存储数据最大个数
private int size;
//堆中已经存储元素的个数
private int count;
/**
* 构造一个使用数组进行存储的堆
* @param initCapacity
*/
public Heap(int initCapacity) {
this.data = new int[initCapacity];
this.size = initCapacity;
this.count = 0;
}
/**
* 向堆中插入元素 [需要交换数据 定义一个交换方法trans()]
* @return
*/
public boolean insert(int data){
if(count >= size){
return false;
}
this.data[count++] = data;
//堆化操作
this.heapBottomUp(this.data,count);
return true;
}
/**
* 自底向上堆化 大顶堆 插入
* @param data
* @param end 新插入元素的下标
*/
private void heapBottomUp(int[] data,int end){
int index = end;
while (index / 2 > 0 && data[index / 2] < data[index]){
//根节点小于子节点
trans(data,index/2,index);
index /= 2;
}
}
/**
* 删除堆顶元素
* @return
*/
public int removeTop(){
int max = data[0];
data[0] = data[--count];//把第一个数据放到末尾
this.heapTopDown(data,0,count);
return max;
}
/**
* 自顶向下堆化 大顶堆 删除
* @return
*/
private void heapTopDown(int[] data, int begin, int end) {
int tmp = data[begin];
//i = 2*begin+1 是左子节点
for (int i = 2 * begin+1; i < end ; i = i * 2+1) {
if(i + 1 < end && data[i] < data[i+1]){
i++;//左右子节点的较大值是右子节点
}
if(data[i] > tmp){
data[begin] = data[i];
begin = i;
} else
break;
}
//for循环后,已经将以begin为父节点的树的最大值,放在了顶部
data[begin] = tmp;
}
/**
* 交换数组中下标为i和j的两个元素
* @param arr
* @param i
* @param j
*/
private void trans(int[] arr,int i,int j){
int tmp = arr[i];
arr[i] = arr[i];
arr[j] = tmp;
}
}
堆是完全二叉树,而完全二叉树的时间复杂与树的高度有关,所以它的时间复杂为O(logn)
1.3 堆排序
下面仅仅提供代码,对堆排序感兴趣参看:
https://blog.csdn.net/yeahPeng11/article/details/117912723
public class HeapSort {
public static void main(String[] args) {
HeapSort heap = new HeapSort();
int[] arr = {0,9,5,6,2,7,1,2};
System.out.println(Arrays.toString(arr));
heap.heapSort(arr);
System.out.println(Arrays.toString(arr));
}
/**
* 堆排序
* 1.堆化处理
* 2.数据交换
*
* @param arr
*/
public void heapSort(int[] arr) {
//1.建堆
buildHeap(arr,arr.length);//
//2.排序
sort(arr,arr.length);
}
/**
* 建堆操作
*/
private void buildHeap(int[] data,int length) {
//自顶向下
for (int i = length / 2 - 1; i >= 0; i--) {
heapTopDown(data, i, length);//i表示待调整的位置
}
}
/**
* 排序
*
* @param arr
*/
private void sort(int[] arr,int length) {
for (int i = length-1; i>0; i--) {
trans(arr,0,i);
heapTopDown(arr,0,i);
}
}
/**
* 自定向下的堆化操作 大顶堆
*
* @param data
* @param begin
* @param length
*/
private void heapTopDown(int[] data, int begin, int length) {
int tmp = data[begin];
//i = 2*begin+1 是左子节点
for (int i = 2 * begin+1; i < length ; i = i * 2+1) {
if(i + 1 < length && data[i] < data[i+1]){
i++;//左右子节点的较大值是右子节点
}
if(data[i] > tmp){
data[begin] = data[i];
begin = i;
} else
break;
}
//for循环后,已经将以begin为父节点的树的最大值,放在了顶部
data[begin] = tmp;
}
/**
* 交换数组中下标为i和j的两个元素
*
* @param arr
* @param i
* @param j
*/
private void trans(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
}
2.图
2.1 图的定义
图中每一个元素称为顶点(vertex),图中的一个顶点可以与其他任意顶点建立连接关系,这种建立的关系叫做边(edge)。每个顶顶点连接其他的顶点的边数称为度(degree)。比如说明B的度为2,A的度为3。
图又分为有向图和无向图,上面图片中左边的是无向图,右边的是有向图。微博用户之间的关注就是图的模型。而再有向图中又有**入度(in-degree)和出度(out-degree)**之分,入度表示指向顶点的边,出度表示从这个顶点出发有多少条边。
在关于好友亲密度的关系中,就使用到另外一种图,带权图(weight graph),每条边都有一个权重(weight)
2.2 图的存储方式
图有多种存储方式:邻接矩阵、邻接表、十字链表、邻接多重表、便集数等,这里介绍邻接矩阵和邻接表。
2.2.1 邻接矩阵
图的邻接矩阵存储方式是用两个数组来表示图。一个一维数组存储图中顶点信息,一个二维数组(称为邻接矩阵)存储图中的边或弧的信息。设图G有n个顶点,则邻接矩阵是一个nxn的方阵,定义为:
无向图
有向图
带权图
其中的∞表示没有权值
2.2.2 邻接表
邻接矩阵的一个n*n的矩阵,就比如上面的带权图,就有很多空间被浪费了,为解决一个空间浪费问题,就引出了邻接表
邻接表是数组和链表的结合,图中顶点用一维数组
存储。图中指向关系使用链表
存储。
也可以使用单链表存储图中元素,但是对于读取顶点信息而言,数组效率更优。
使用单链表存储邻接点的原因:邻接点个数不定,减少内存浪费。
存储指向关系的链表,在无向图中称为vi
的边表
,在有向图中称为vi
的出边表
无向图
有向图/带权图
2.3 图的存储实现
下面以为邻接表无向图为例:
public class AdjacencyList {
/**
* 标识图中顶点个数
*/
private int points;
/**
* 邻接表
* LinkedList<Integer>的数组
* 也可写成LinkedList<Integer>[] adjacencyList
*/
private LinkedList<Integer> adjacencyList[];
public AdjacencyList(int points) {
this.points = points;
//初始化数组
adjacencyList = new LinkedList[this.points];
//初始化数组中每一个槽位上的链表
for (int i = 0; i < this.points; i++) {
adjacencyList[i] = new LinkedList<Integer>();
}
}
/**
* 图中添加顶点(和边)
*/
public void addPoint(int s,int t){
//无向表 互相存储指向
adjacencyList[s].add(t);
adjacencyList[t].add(s);
}
}
2.4 图的遍历(搜索算法)
图的遍历又称为搜索算法,从图中某一顶点出发遍历图中其余顶点,每一个顶点仅被访问一次。搜索算法分为深度优先搜索算法和广度优先搜索算法。下面以为邻接表为例。
先定义一个打印路径的方法
/**
* 打印从begin-target线路的方法
* @param prev 记录路径的数组
* @param begin 开始顶点
* @param target 目标顶点
*/
private void print(int[] prev,int begin,int target){
if(prev[target] != -1 && begin != target)
print(prev,begin,prev[target]);
System.out.print(target+">>");
}
深度优先搜索算法
DFS,Deep-Frist-Search,它从图中某个顶点X
出发,访问此顶点X
,然后从X
的未被访问的邻接点发出深度优先遍历图,直至图中所有的和X
有路径相通的顶点都被访问到(有一个回溯换路线的过程)。
coding:
/**
* 标记是否找到target
*/
private boolean found = false;
/**
* DFS 深度优先
* @param begin 开始顶点
* @param target 目标顶点
*/
public void dfs(int begin,int target){
if(begin == target) return;
//标记某元素是否为访问
boolean[] visited = new boolean[this.points];
visited[begin] = true;
//定义一个数组,记录从初始顶点到目标顶点之间的线路
int[] prev = new int[this.points];
Arrays.fill(prev,-1);
//递归调用
returnDFS(begin,target,visited,prev);
//打印线路
print(prev,begin,target);
}
/**
* 查找顶点point到target的线路
* @param point 初始顶点
* @param target 目标顶点
* @param visited 已被访问的顶点数组
* @param prev 顶点线路数组
*/
private void returnDFS(int point,int target,boolean[] visited,int[] prev){
if(found) return;//已经找到直接返回
if(point == target){
found = true;
return;
}
//获取与当前顶点相连接的所有顶点
for (int i = 0; i < adjacencyList[point].size(); i++) {
//获取顶点point相连的顶点
Integer p_context = adjacencyList[point].get(i);
if(!visited[p_context]){
//记录p_context之前的顶点point
prev[p_context] = point;
visited[p_context] = true;
//就此顶点向下递归
returnDFS(p_context,target,visited,prev);
}
}
}
广度优先搜索算法
BFS,Breath-First-Search,图的深度优先遍历类似于树的前序遍历,那么图的广度优先遍历就类似于树的层序遍历。
得到是两顶点的最短路径之一,但是不是唯一的。
/**
* BFS 广度优先搜索算法
* @param begin 起始顶点
* @param target 目标顶点
*/
public void bfs(int begin,int target){
if(begin == target) return;
/**
* 记录boolean数组,记录顶点是否被访问过
*/
boolean[] visited = new boolean[this.points];
//起始顶点被访问过
visited[begin] = true;
/**
* 定义一个队列 存储已经被访问过,但是还有相邻顶点没被访问
*/
Queue<Integer> queue = new LinkedList<>();
queue.add(begin);
/**
* 定义一个数组来存储begin-target路线
* 就是我从哪给点来的
*/
int[] prev = new int[this.points];
//初始化线路为-1
Arrays.fill(prev,-1);
/**
* 循环访问队列中没有被访问的顶点
*/
while (!queue.isEmpty()){
//取出访问过的但是有相邻未访问过的顶点
Integer p = queue.poll();
//遍历这个顶点的相邻顶点(此相邻顶点未被访问)
for (int i = 0; i < adjacencyList[p].size(); i++) {
//取出相邻顶点
Integer p_edge = adjacencyList[p].get(i);
//判断相邻顶点是否被访问过
if(!visited[p_edge]){//未被访问s
//记录访问路线
prev[p_edge] = p;
//如果该顶点与目标顶点相等,就打印访问路线
if(p_edge == target){
//TODO 打印访问路径
print(prev,begin,target);
return;
}
//标记p为已经访问过的顶点
visited[p] = true;
//把相邻顶点存入队列
queue.add(p_edge);
}
}
}
}
3.字符串匹配算法
Java中提供的indexOf(),starWith(),endWith()这些方法底层就依赖于字符串匹配算法
下面涉及到几种经典的字符串匹配算法:BF,RK。(BM,KMP太复杂…)
说明:
T(target):主串,目标串
P(patter):子串,目标串
3.1 BF算法
Brute Force,暴风算法,也称朴素匹配算法
。首先将匹配串和模式串左对齐,然后从左向右一个一个进行比较,如果不成功则模式串向右移动一个单位。每次匹配不成功的时候,前面匹配成功的信息都被当作废物丢弃了。
public class BF {
/**
* bf 在主串t中匹配子串p
*
* @param t 主串
* @param p 子串
* @return
*/
public int bf(String t, String p) {
if (t.length() == 0 || t == null || p.length() == 0 || p == null || t.length() < p.length())
return -1;
//将字符串转换成字符数组
char[] t_arr = t.toCharArray();
char[] p_arr = p.toCharArray();
//匹配过程
return match(t_arr, p_arr);
}
/**
* 匹配算法match
*
* @param t 主字符数组
* @param p 子字符数组
* @return
*/
private int match(char[] t, char[] p) {
int i = 0;//主串下标
int j = 0;//子串下标
int position = 0;//定位的位置
while (i < t.length && j < p.length) {
if (t[i] == p[j]) {
j++;//两者后移一位
i++;
} else {
i = i - j + 1;//主串回到第一个匹配位置
j = 0;//子串回到初始位置
}
}
if (i <= t.length) position = i - p.length;//回到匹配初始位置
else position = -1;
return position;
}
}
3.2 RK算法
RK算法则是将串整体作为一个特征,效率非常的nice!
Rabin-Karp,是BF算法的升级版,主要引入hash算法(自定义函数)。假设子串长度为m
,那么主串中任意连续的m
个字符的hash值与子串的hash值相等,那么就进行进一步匹配(进一步匹配时间复杂度O(1))。比如aabsee sds
和模式串 ees
,其中see
的hash值模式串相等,进行进一步匹配,不是就继续匹配到ees
(指针后移),从而匹配成功。
public class RK {
/**
* rk 字符串匹配算法
* @param t 主字符串
* @param p 子字符串
* @return 子串在主串中的第一个位置索引
*/
public int rk(String t,String p){
//可行性判断
if(t == null || t.length() == 0 || p.length() == 0 || p == null || p.length() > t.length())
return -1;
int hash = hash(p,26,31,0,p.length());//26个字符串 而K只要比26大即可
for (int i = 0; i < t.length(); i++) {
if(hash(t,26,31,i,p.length()) == hash && match(t,p,i))
return i;
}
return -1;
}
/**
* hash算法
* @param str 主串
* @param R 进制数大小 一般是26
* @param K 将字符串映射到k的范围 K只要大于26即可
* @param start 主串开始位置
* @param len 模式串长度
* @return 最终的hash值
*/
private int hash(String str,int R,int K,int start,int len){
int hash = 0;
for (int i = start; i < start+len; i++)
hash = (R*hash + str.charAt(i) % K);
return hash % K;
}
/**
* 匹配算法
* @param t 主串
* @param p 子串
* @param i 从主串下标i处开始比较
* @return
*/
private boolean match(String t,String p,int i){
for (int j = 0; j < p.length(); j++) {
if(p.charAt(j) != t.charAt(j+i))
return false;
}
return true;
}
}