数据结构与算法(八) - 堆、图、字符串匹配算法(BF,RK)

数据结构与算法(八)-堆、图、字符串匹配算法

1.堆(Heap)

1.1 堆的定义

1、堆是一颗完全二叉树;(处理)

2、堆中的某个结点的值总是大于等于或小于等于子树的任意节点的值。

3、堆中每个结点的子树都是堆树。

1.1.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;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值