【项目总结】基于SpringBoot+Ansj分词+正倒排索引的Java文档搜索引擎项目总结

news2024/12/23 19:52:38

文章目录

  • 项目介绍(开发背景)
  • 主要用到的技术点
    • 前端
    • 后端
      • Ansj分词
      • 实现索引模块
        • 实现Parser类
        • 实现Index类
        • 完善Parser类
        • 优化制作索引速度
      • 实现搜索模块
        • 实现DocSearcher类
        • 处理暂停词
  • 项目编写过程中遇到的困难点
  • 上传部署
  • 总结

项目介绍(开发背景)

        相信很多小伙伴在学习Java的过程中都会参考Java官方文档,但是这个文档存在一个问题,就是不支持用户对某个关键词进行搜索,只能够通过某个包去找到其中的某个类进行查看,显然这样的效率是比较低的。

在这里插入图片描述
        虽然也有很多是离线的api文档是支持搜索功能的,但是我想做一个类似与百度的搜索界面,点击跳转到详细文档的效果。对此,我参考了百度搜索的页面,初步得到一个结论:搜索引擎是先输入一个查询词,会得到若干个结果,每个结果都会包含标题、部分描述展示、url等,点击标题即可完成页面跳转。

主要用到的技术点

前端

        这个项目前端代码是比较简单的,只有一个页面(因为本项目只做的是搜索引擎实现的逻辑,得到结果后跳转的是官方文档详细页),数据的请求也是使用jQuery来实现的。

后端

        后端部分是整个搜索引擎项目的核心部分,具体开发的流程如下:

Ansj分词

        我们在输入查询词的时候,不仅仅只会是输入一个词,更有可能的是会查询一个句子,那么这时候就需要进行一个分词操作了。
        我们可以选择使用一个开源的分词库,我这里选择的是Ansj,简单来说,其实就是导入一个依赖包即可:

<dependency>
    <groupId>org.ansj</groupId>
   	<artifactId>ansj_seg</artifactId>
    <version>5.1.6</version>
</dependency>

实现索引模块

实现Parser类

        Parser类负责读取html文件,制作并生成索引数据(输出到文件中),主要的步骤:1. 从制定的路径中枚举出所有文件;2. 读取每一个文件,并从文件中解析出标题、正文内容、url。

//Description: 解析api文档的内容,完成索引的制作
public class Parser {
    //指定加载文档的路径
    private static final String INPUT_PATH = "D:/java_code/java-api/java8/docs/api/";

    private Index index = new Index();

    public void run(){
        //根据指定的路径,枚举出该路径下的所有html文件(包括子目录中的所有文件)
        List<File> fileList = new ArrayList<>();
        enumFile(INPUT_PATH, fileList);
        //打开对应的文件,读取文件内容,进行解析并构建索引
        long begin = System.currentTimeMillis();
        for(File file : fileList){
            //解析html文件
            System.out.println("开始解析:" + file.getAbsolutePath());
            parseHTML(file);
        }
        long end = System.currentTimeMillis();
        System.out.println("遍历文件加入到索引中消耗的时间:" + (end - begin) + "ms");
    }

    private void parseHTML(File file) {
        //解析标题、正文、url
        String title = parseTitle(file);
        //String content = parseContent(file);
        String content = parseContentByRegex(file);
        String url = parseUrl(file);
    }

    //解析url
    private String parseUrl(File file) {
        String part1 = "https://docs.oracle.com/javase/8/docs/api/";
        String part2 = file.getAbsolutePath().substring(INPUT_PATH.length());
        return part1 + part2;
    }

    //解析标题
    private String parseTitle(File file) {
        String name = file.getName();
        return name.substring(0, name.length() - ".html".length());
    }

    //解析正文
    private String parseContent(File file) {
        try {
            //手动将缓冲区设置成1M大小
            BufferedReader bufferedReader = new BufferedReader(new FileReader(file), 1024 * 1024);
            boolean flag = true;
            StringBuilder content = new StringBuilder();
            while(true){
                int t = bufferedReader.read();
                if(t == -1){
                    break;
                }
                char ch = (char)t;
                if(flag){
                    if(ch == '<'){
                        flag = false;
                        continue;
                    }
                    if(ch == '\n' || ch == '\r'){
                        ch = ' ';
                    }
                    content.append(ch);
                }else{
                    if(ch == '>'){
                        flag = true;
                    }
                }
            }
            bufferedReader.close();  //记得文件的关闭
            return content.toString();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return "";
    }

	//递归完成目录的枚举
    private void enumFile(String inputPath, List<File> fileList) {
        File rootPath = new File(inputPath);
        File[] files = rootPath.listFiles();
        for(File file : files){
            if(file.isDirectory()){
                enumFile(file.getAbsolutePath(), fileList);
            }else{
                if(file.getAbsolutePath().endsWith(".html")) {
                    fileList.add(file);
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Parser parser = new Parser();
        parser.runByThread();
    }
}
实现Index类

        Index类负责构建索引数据结构,其中需要包含的方法有:getDocInfo(根据docId查正排)、getInverted(根据关键词查倒排)、addDoc(往索引中新增一个文档,构建正排索引和倒排索引)、save(往磁盘中写索引数据)、load(从磁盘中加载索引数据)。

        整个搜索的核心思路其实就在这个类中,对于搜索的思路主要有两种:一种是直接进行暴力搜索,每次处理搜索请求的时候,拿着查询词到所有网页中都搜索一遍,并检查每个网页中是否包含查询词字符串,但是随着文档数量不断的增加,这种方法所需要的开销是非常大的,而对于搜索引擎来说,效率往往都是放在第一位的,因此该方法是不可行的;另外一种是建立倒排索引,倒排索引是一种专门针对搜索引擎场景所设计的数据结构,可以通过文档的信息来制定排序规则。

        从构建倒排索引和根据关键词查倒排这两个步骤中可以看出一个问题,那就是倒排索引需要使用什么规则来构建呢?直接按照插入文档的顺序来构建显然是不行的,我们在百度搜索的时候会看到排在前面的都是比较重要的或者是搜索次数比较多的,因此,我们在构建倒排索引的时候不能仅仅按照插入顺序,而是应该自定义一个权重,我在这个项目中定义的权重是按照搜索词在文档中出现的次数(标题出现+10,正文出现+1的规则)来实行的。以下是定义权重的类:

public class Weight {
    private int docId;
    private int weight;

    public int getDocId() {
        return docId;
    }

    public void setDocId(int docId) {
        this.docId = docId;
    }

    public int getWeight() {
        return weight;
    }

    public void setWeight(int weight) {
        this.weight = weight;
    }
}

        构建出Index类的基本骨架:

public class Index {
    private static String INDEX_PATH = "D:/java_code/java-api/java8/";

    private ObjectMapper objectMapper = new ObjectMapper();

    private List<DocInfo> forwardIndex = new ArrayList<>();  //正排索引
    private Map<String, List<Weight>> invertedIndex = new HashMap<>();  //倒排索引

    //给定一个docId,通过正排索引查询到文档的详细信息
    public DocInfo getDocInfo(int docId){
        //TODO
    }

    //给定一个词,通过倒排索引查询哪些文档与这个词相关联
    public List<Weight> getInverted(String term){
        //TODO
    }

    //往索引中新增一个文档
    public void addDoc(String title, String url, String content){
        //TODO
    }

    //把内存中的索引结构保存到磁盘中
    public void save(){
        //TODO
    }

    //把磁盘中的索引数据加载到内存中
    public void load(){
        //TODO
    }
}

        查正排&查倒排:

    //给定一个docId,通过正排索引查询到文档的详细信息
    public DocInfo getDocInfo(int docId){
        return forwardIndex.get(docId);
    }

    //给定一个词,通过倒排索引查询哪些文档与这个词相关联
    public List<Weight> getInverted(String term){
        return invertedIndex.get(term);
    }

        实现addDoc(构建正排索引和倒排索引):

    //往索引中新增一个文档
    public void addDoc(String title, String url, String content){
        DocInfo docInfo = buildForward(title, url, content);
        buildInverted(docInfo);
    }

        正排索引的构建是比较简单的,直接按照插入顺序进行构建即可:

    private DocInfo buildForward(String title, String url, String content) {
        DocInfo docInfo = new DocInfo();
        docInfo.setTitle(title);
        docInfo.setUrl(url);
        docInfo.setContent(content);
        docInfo.setDocId(forwardIndex.size());
        forwardIndex.add(docInfo);
        return docInfo;
    }

        倒排索引需要计算每个词的权重,我们这里可以使用哈希表来进行存储。

public class WordCnt {
    private int titleCount;
    private int contentCount;

    public int getTitleCount() {
        return titleCount;
    }

    public void setTitleCount(int titleCount) {
        this.titleCount = titleCount;
    }

    public int getContentCount() {
        return contentCount;
    }

    public void setContentCount(int contentCount) {
        this.contentCount = contentCount;
    }
}
    private void buildInverted(DocInfo docInfo) {
        //针对标题和正文进行分词(需要分开计算),并统计每个词出现的次数
        Map<String, WordCnt> wordCntMap = new HashMap<>();
        List<Term> terms = ToAnalysis.parse(docInfo.getTitle()).getTerms();
        for(Term term : terms){
            String word = term.getName();
            if(!wordCntMap.containsKey(word)){
                WordCnt wordCnt = new WordCnt();
                wordCnt.setTitleCount(1);
                wordCnt.setContentCount(0);
                wordCntMap.put(word, wordCnt);
            }else{
                WordCnt wordCnt = wordCntMap.get(word);
                wordCnt.setTitleCount(wordCnt.getTitleCount() + 1);
            }
        }
        terms = ToAnalysis.parse(docInfo.getContent()).getTerms();
        for(Term term : terms){
            String word = term.getName();
            if(!wordCntMap.containsKey(word)){
                WordCnt wordCnt = new WordCnt();
                wordCnt.setTitleCount(0);
                wordCnt.setContentCount(1);
                wordCntMap.put(word, wordCnt);
            }else{
                WordCnt wordCnt = wordCntMap.get(word);
                wordCnt.setContentCount(wordCnt.getContentCount() + 1);
            }
        }
        for(Map.Entry<String, WordCnt> entry : wordCntMap.entrySet()){
            List<Weight> invertedList = invertedIndex.get(entry.getKey());
            if(invertedList == null){
                List<Weight> list = new ArrayList<>();
                Weight weight = new Weight();
                weight.setDocId(docInfo.getDocId());
                weight.setWeight(entry.getValue().getTitleCount() * 10 + entry.getValue().getContentCount());
                list.add(weight);
                invertedIndex.put(entry.getKey(), list);
            }else{
                Weight weight = new Weight();
                weight.setDocId(docInfo.getDocId());
                weight.setWeight(entry.getValue().getTitleCount() * 10 + entry.getValue().getContentCount());
                invertedList.add(weight);
            }
        }
    }

        单看这段代码可以会有点绕,可以先看下图:
在这里插入图片描述
        也就是说都有其对应的文章(可以多篇),这其实就类似于是一个哈希桶,当查询词是某个词的时候,就可以快速列出对应词下的文章,这就是倒排索引,当然,每一个节点中都包含了文章的信息(如词出现的次数等),方便后面对这些权重进行自定义排序。

        实现save往磁盘中写索引数据(通过json格式来生成正排索引和倒排索引两个文件):

    //把内存中的索引结构保存到磁盘中
    public void save(){
        //使用两个文件分别表示正排和倒排
        //判断索引对应的文件是否存在,不存在就创建
        long begin = System.currentTimeMillis();
        File indexPathFile = new File(INDEX_PATH);
        if(!indexPathFile.exists()){
            indexPathFile.mkdirs();
        }
        File forwardIndexFile = new File(INDEX_PATH + "forward.txt");
        File invertedIndexFile = new File(INDEX_PATH + "inverted.txt");
        try {
            objectMapper.writeValue(forwardIndexFile, forwardIndex);
            objectMapper.writeValue(invertedIndexFile, invertedIndex);
        } catch (IOException e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println("保存索引成功. 消耗时间:" + (end - begin) + "ms");
    }

        实现load将索引文件中的内容加载到内存中:

    //把磁盘中的索引数据加载到内存中
    public void load(){
        long begin = System.currentTimeMillis();
        File forwardIndexFile = new File(INDEX_PATH + "forward.txt");
        File invertedIndexFile = new File(INDEX_PATH + "inverted.txt");
        try {
            forwardIndex = objectMapper.readValue(forwardIndexFile, new TypeReference<List<DocInfo>>() {});
            invertedIndex = objectMapper.readValue(invertedIndexFile, new TypeReference<Map<String, List<Weight>>>() {});
        } catch (IOException e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println("数据加载成功. 消耗时间:" + (end - begin) + "ms");
    }
完善Parser类

        接下来,我们可以在Parser类中实例化Index对象,在每次解析出信息的时候就调用Index对象添加到索引中,同时将内存中的索引数据保存到指定文件中:

	private Index index = new Index();
    private void parseHTML(File file) {
        //解析标题、正文、url
        String title = parseTitle(file);
        //String content = parseContent(file);
        String content = parseContentByRegex(file);
        String url = parseUrl(file);
        //将解析出的信息加入到索引中
        index.addDoc(title, url, content);
    }
    public void run(){
        //根据指定的路径,枚举出该路径下的所有html文件(包括子目录中的所有文件)
        List<File> fileList = new ArrayList<>();
        enumFile(INPUT_PATH, fileList);
        //打开对应的文件,读取文件内容,进行解析并构建索引
        long begin = System.currentTimeMillis();
        for(File file : fileList){
            //解析html文件
            System.out.println("开始解析:" + file.getAbsolutePath());
            parseHTML(file);
        }
        long end = System.currentTimeMillis();
        System.out.println("遍历文件加入到索引中消耗的时间:" + (end - begin) + "ms");
        //内存中构造的索引数据结构保存到指定文件中
        index.save();
    }
优化制作索引速度

        索引的制作是会比较耗时间的,特别是数据量大的时候,那么需要如何优化这个过程呢?显然,我们可以通过多线程来进行优化:

    //实现多线程制作索引
    public void runByThread() throws InterruptedException {
        //根据指定的路径,枚举出该路径下的所有html文件(包括子目录中的所有文件)
        List<File> fileList = new ArrayList<>();
        enumFile(INPUT_PATH, fileList);
        //打开对应的文件,读取文件内容,进行解析并构建索引(引入线程池)
        CountDownLatch countDownLatch = new CountDownLatch(fileList.size());
        ExecutorService executorService = Executors.newFixedThreadPool(4);
        long begin = System.currentTimeMillis();
        for(File file : fileList){
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    //解析html文件
                    System.out.println("开始解析:" + file.getAbsolutePath());
                    parseHTML(file);
                    countDownLatch.countDown();
                }
            });
        }
        countDownLatch.await();
        long end = System.currentTimeMillis();
        System.out.println("遍历文件加入到索引中消耗的时间:" + (end - begin) + "ms");
        executorService.shutdown();  //手动销毁线程池中的线程
        //内存中构造的索引数据结构保存到指定文件中
        index.save();
    }

        我这里创建的是4个线程(已经可以提升很高的效率了),需要注意的是:在使用多线程的时候需要检查代码中是否存在写操作,如果存在,需要对这个操作整体进行加锁操作。我们在检查代码的时候就发现有几个地方是需要修改的:

(1)在建立倒排索引的时候,我们需要不断地对出现词次数进行修改,所以在出现修改操作部分的代码都需要加上锁:

    private void buildInverted(DocInfo docInfo) {
        //针对标题和正文进行分词(需要分开计算),并统计每个词出现的次数
        Map<String, WordCnt> wordCntMap = new HashMap<>();
        List<Term> terms = ToAnalysis.parse(docInfo.getTitle()).getTerms();
        for(Term term : terms){
            synchronized (locker1){
                String word = term.getName();
                if(!wordCntMap.containsKey(word)){
                    WordCnt wordCnt = new WordCnt();
                    wordCnt.setTitleCount(1);
                    wordCnt.setContentCount(0);
                    wordCntMap.put(word, wordCnt);
                }else{
                    WordCnt wordCnt = wordCntMap.get(word);
                    wordCnt.setTitleCount(wordCnt.getTitleCount() + 1);
                }
            }
        }
        terms = ToAnalysis.parse(docInfo.getContent()).getTerms();
        for(Term term : terms){
            synchronized (locker2){
                String word = term.getName();
                if(!wordCntMap.containsKey(word)){
                    WordCnt wordCnt = new WordCnt();
                    wordCnt.setTitleCount(0);
                    wordCnt.setContentCount(1);
                    wordCntMap.put(word, wordCnt);
                }else{
                    WordCnt wordCnt = wordCntMap.get(word);
                    wordCnt.setContentCount(wordCnt.getContentCount() + 1);
                }
            }
        }
        for(Map.Entry<String, WordCnt> entry : wordCntMap.entrySet()){
            synchronized (locker3){
                List<Weight> invertedList = invertedIndex.get(entry.getKey());
                if(invertedList == null){
                    List<Weight> list = new ArrayList<>();
                    Weight weight = new Weight();
                    weight.setDocId(docInfo.getDocId());
                    weight.setWeight(entry.getValue().getTitleCount() * 10 + entry.getValue().getContentCount());
                    list.add(weight);
                    invertedIndex.put(entry.getKey(), list);
                }else{
                    Weight weight = new Weight();
                    weight.setDocId(docInfo.getDocId());
                    weight.setWeight(entry.getValue().getTitleCount() * 10 + entry.getValue().getContentCount());
                    invertedList.add(weight);
                }
            }
        }
    }

(1)在建立正排索引的时候,我们也需要不断地对id进行修改,所以在出现修改操作部分的代码都需要加上锁:

    private DocInfo buildForward(String title, String url, String content) {
        DocInfo docInfo = new DocInfo();
        docInfo.setTitle(title);
        docInfo.setUrl(url);
        docInfo.setContent(content);
        synchronized (locker4){
            docInfo.setDocId(forwardIndex.size());
            forwardIndex.add(docInfo);
        }
        return docInfo;
    }

实现搜索模块

实现DocSearcher类

        简单介绍一下这个类需要实现的功能,这个类主要负责实现搜索功能,得到用户输入的查询词后,对查询词进行分词处理,之后对每一个分词都查找一下之前存的倒排索引,得到一个倒排哈希桶,将这些结构合并到一个结果集中,并对其中的这些结果按照指定的权重降序排序后返回结果即可。

    public List<Result> search(String query){
        //分词
        List<Term> oldTerms = ToAnalysis.parse(query).getTerms();
        List<Term> terms = new ArrayList<>();
        //过滤
        for(Term term : oldTerms){
            terms.add(term);
        }
        //触发
        List<List<Weight>> termResult = new ArrayList<>();
        for(Term term : terms){
            String word = term.getName();
            List<Weight> invertedList =  index.getInverted(word);
            if(invertedList == null){
                continue;
            }
            termResult.add(invertedList);
        }
        //合并
        List<Weight> allTermResult = mergeResult(termResult);
        //排序(降序)
        allTermResult.sort((o1, o2) -> {
            return o2.getWeight() - o1.getWeight();
        });
        //包装
        List<Result> results = new ArrayList<>();
        for(Weight weight : allTermResult){
            DocInfo docInfo = index.getDocInfo(weight.getDocId());
            Result result = new Result();
            result.setTitle(docInfo.getTitle());
            result.setUrl(docInfo.getUrl());
            result.setDesc(GenDesc(docInfo.getContent(), terms));
            results.add(result);
        }
        return results;
    }

        对于其中的合并操作,我们可以使用优先级队列来辅助实现(其实这就涉及到多路归并算法的思想):

    private List<Weight> mergeResult(List<List<Weight>> source) {
        //针对每一行进行排序(按照id升序排序)
        for(List<Weight> curRow : source){
            curRow.sort(new Comparator<Weight>(){
                @Override
                public int compare(Weight o1, Weight o2) {
                    return o1.getDocId() - o2.getDocId();
                }
            });
        }
        //使用优先级队列对每一行进行合并
        List<Weight> target = new ArrayList<>();
        PriorityQueue<Pos> queue = new PriorityQueue<>(new Comparator<Pos>() {
            @Override
            public int compare(Pos o1, Pos o2) {
                return source.get(o1.row).get(o1.col).getDocId() - source.get(o2.row).get(o2.col).getDocId();
            }
        });
        for(int row = 0; row < source.size(); row++){
            queue.offer(new Pos(row, 0));
        }
        while(!queue.isEmpty()){
            Pos minPos = queue.poll();
            Weight curWeight = source.get(minPos.row).get(minPos.col);
            if(target.size() > 0){
                Weight lastWeight = target.get(target.size() - 1);
                if(lastWeight.getDocId() == curWeight.getDocId()){
                    lastWeight.setWeight(lastWeight.getWeight() + curWeight.getWeight());
                }else{
                    target.add(curWeight);
                }
            }else{
                target.add(curWeight);
            }
            Pos newPos = new Pos(minPos.row, minPos.col + 1);
            if(newPos.col >= source.get(newPos.row).size()){
                continue;
            }
            queue.offer(newPos);
        }
        return target;
    }

        以下这段代码表示需要截取内容的部分简介显示到页面上:

    private String GenDesc(String content, List<Term> terms) {
        int firstPos = -1;
        for(Term term : terms){
            String word = term.getName();
            content = content.toLowerCase().replaceAll("\\b" + word + "\\b", " " + word + " ");
            firstPos = content.indexOf(" " + word + " ");
            if(firstPos >= 0){
                break;
            }
        }
        if(firstPos == -1){
            return content.substring(0, Math.min(60, content.length())) + "...";
        }
        String desc = "";
        int descBeg = firstPos < 30 ? 0 : firstPos - 30;
        desc = content.substring(descBeg, Math.min(descBeg + 100, content.length())) + "...";
        for(Term term : terms){
            String word = term.getName();
            desc = desc.replaceAll("(?i) " + word + " ", "<i>" + " " + word + " " + "</i>");
        }
        return desc;
    }
处理暂停词

        我们在网上搜索到一些常见的暂停词后将它们保存到一个文件夹中,每次从文件中获取这些暂停词,看是否有存在于查询词中的,若有,直接跳过即可。

    private static String STOP_WORD_PATH = "D:/java_code/java-api/java8/stop_word.txt";
    private Set<String> stopWords = new HashSet<>();

        将暂停词从磁盘加载到内存中:

    public void loadStopWords(){
        try {
            BufferedReader bufferedReader = new BufferedReader(new FileReader(STOP_WORD_PATH));
            while(true){
                String line = bufferedReader.readLine();
                if(line == null){
                    break;
                }
                stopWords.add(line);
            }
            bufferedReader.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

        之后就是补充上面search方法中的代码,在查询词中发现暂停词时,直接进行过滤操作:

		//过滤
        for(Term term : oldTerms){
            if(stopWords.contains(term.getName())){
                continue;
            }
            terms.add(term);
        }

项目编写过程中遇到的困难点

        这个项目是基础版的搜索引擎,所以总体来说是中规中矩的,没有特别难解决的问题,唯一一个比较难编写的是最后的使用多路归并算法来实现权重的合并。

上传部署

        将打包后的jar包以及正排、倒排索引和暂停词放在同一个目录底下,输入命令:java -jar [jar包名]就可以打开SpringBoot项目;持久运行需要输入命令:nohup java -jar [jar包名] &
在这里插入图片描述

总结

        简单总结一下这个Java文档搜索引擎项目:
        有点:使用了主流的SpringBoot框架进行开发,运用多线程来提高代码的运行效率,包含了基本搜索引擎中所需要的数据结构和算法。
        缺点:扩展性弱,这个搜索引擎只能够针对Java文档进行搜索,比较合理的实现是需要搭配爬虫实现对页面数据的获取,当然,这个难度也是比较大的。
        关于本项目的全部代码我都放在了我的个人Gitee账户下,有需要的可以点击查看:Java文档搜索引擎项目存放代码。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1185275.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【紫光同创国产FPGA教程】【PGC1/2KG第六章】密码锁实验例程

本原创教程由深圳市小眼睛科技有限公司创作&#xff0c;版权归本公司所有&#xff0c;如需转载&#xff0c;需授权并注明出处 适用于板卡型号&#xff1a; 紫光同创PGC1/2KG开发平台&#xff08;盘古1K/2K&#xff09; 一&#xff1a;盘古1K/2K开发板&#xff08;紫光同创PGC…

排序算法的空间复杂度和时间复杂度

一、排序算法的时间复杂度和空间复杂度 排序算法 平均时间复杂度 最坏时间复杂度 最好时间复杂度 空间复杂度 稳定性 冒泡排序 O(n) O(n) O(n) O(1) 稳定 直接选择排序 O(n) O(n) O(n) O(1) 不稳定 直接插入排序 O(n) O(n) O(n) O(1) 稳定 快速排序 O(n…

node插件MongoDB(一)——MongoDB的下载和安装

文章目录 前言一、MongoDB的下载和安装1. 下载(1) 打开官网(2) 选择版本(3) 选择电脑系统和安装格式后点击下载(4) 将文件解压放到C:\Program Files文件目录下(5) 在c盘下创建文件夹(6) 启动服务端程序(7) 服务端程序启动成功效果(8) 在浏览器中输入127.0.0.1:27017查看效果&am…

linux下IO模及其特点及select

ftp实现 模拟FTP核心原理&#xff1a;客户端连接服务器后&#xff0c;向服务器发送一个文件。文件名可以通过参数指定&#xff0c;服务器端接收客户端传来的文件&#xff08;文件名随意&#xff09;&#xff0c;如果文件不存在自动创建文件&#xff0c;如果文件存在&#xff0c…

nacos应用——占用内存过多问题解决(JVM调优初步)

问题描述 最近搞了一台1年的阿里云服务器&#xff0c;安装了一下常用的MySQL&#xff0c;Redis&#xff0c;rabbitmq&#xff0c;minio&#xff0c;然后有安装了一下nacos&#xff0c;结果一启动nacos内存占用就很高&#xff0c;就比较限制我继续安装其他镜像或者启动别的服务…

龙迅LT8911EXB功能概述 MIPICSI/DSI TO EDP

LT8911EXB 描述&#xff1a; Lontium LT8911EXB是MIPIDSI/CSI到eDP转换器&#xff0c;单端口MIPI接收器有1个时钟通道和4个数据通道&#xff0c;每个数据通道最大运行2.0Gbps&#xff0c;最大输入带宽为8.0Gbps。转换器解码输入MIPI RGB16/18/24/30/36bpp、YUV422 16/20/24bp…

84 柱状图中的最大的矩形(单调栈)

题目 柱状图中的最大的矩形 给定 n 个非负整数&#xff0c;用来表示柱状图中各个柱子的高度。每个柱子彼此相邻&#xff0c;且宽度为 1 。 求在该柱状图中&#xff0c;能够勾勒出来的矩形的最大面积。 示例 1: 输入&#xff1a;heights [2,1,5,6,2,3] 输出&#xff1a;10 …

webpack的简单使用

什么是webpack&#xff08;去官网看详细的API&#xff09; 本质上&#xff0c;webpack 是一个用于现代 JavaScript 应用程序的 静态模块打包工具。当 webpack 处理应用程序时&#xff0c;它会在内部从一个或多个入口点构建一个 依赖图(dependency graph)&#xff0c;然后将你项…

ATFX汇市:欧元区9月PPI年率降幅扩大至12.4%,EURUSD反弹不够流畅

ATFX汇市&#xff1a;2023年9月欧元区生产者物价指数PPI同比下降12.4%&#xff0c;上月下降11.5%&#xff0c;而市场普遍预期下降12.5%&#xff0c;表现较差。12.4%的降幅创出PPI数据有记录以来的最低值&#xff0c;可见欧元区生产端的通缩形势严峻。跌幅最大的品种是能源&…

进程(3)——进程优先级与环境变量【Linux】

进程&#xff08;3&#xff09;——进程优先级与环境变量【Linux】 一. 进程如何在cpu中如何执行1.1进程在CPU中的特性1.2 寄存器1.2.1 进程的上下文 二. 进程优先级2.1 如何查看进程优先级2.2 修改进程的优先级2.2.1 NI值2.2.2 修改方法 三. 环境变量3.1 什么是环境变量&#…

二十、泛型(4)

本章概要 补偿擦除 创建类型的实例泛型数组 补偿擦除 因为擦除&#xff0c;我们将失去执行泛型代码中某些操作的能力。无法在运行时知道确切类型&#xff1a; //无法编译 public class Erased<T> {private final int SIZE 100;public void f(Object arg) {// error…

手机玻璃盖板为什么需要透光率检测

手机盖板&#xff0c;也称为手机壳或保护套&#xff0c;是一种用于保护手机外观和延长使用寿命的装置。它们通常由塑料、硅胶、玻璃或金属等材料制成&#xff0c;并固定在手机外壳上,其中任何一个工序出现差错&#xff0c;都有可能导致手机盖板产生缺陷&#xff0c;例如漏油、透…

维控PLC——LX1S :编程口通讯协议

文章目录 说明通讯帧通讯命令字通讯数据地址维控 LX1S通讯协议举例 说明 该协议适用于维控LX1S系列PLC&#xff0c;关于维控LX2N的协议将在后面描述。 通讯帧 通讯采用ASCII码&#xff0c;校验方式采用和校验。 请求帧格式:报文开始命令字地址&#xff08;有些无&#xff09…

热门的免费报表软件,建议收藏!

目前&#xff0c;随着企业对数据越来越重视&#xff0c;报表软件的应用越来越广泛。企业报表的需求越来越多变&#xff0c;就需要好用的免费报表软件&#xff0c;报表软件必须具备简捷、专业、灵活的特点&#xff0c;这里就给大家测评几款免费报表软件&#xff0c;供大家做参考…

机器学习模板代码(期末考试复习)自用存档

机器学习复习代码 利用sklearn实现knn import numpy as np import pandas as pd from sklearn.neighbors import KNeighborsClassifier from sklearn.model_selection import GridSearchCVdef model_selection(x_train, y_train):## 第一个是网格搜索## p是选择查找方式:1是欧…

JVM之jps虚拟机进程状态工具

jps虚拟机进程状态工具 1、jps jps&#xff1a;(JVM Process Status Tool)&#xff0c;虚拟机进程状态工具&#xff0c;可以列出正在运行的虚拟机进程&#xff0c;并显示虚拟机执 行主类&#xff08;Main Class&#xff0c;main()函数所在的类&#xff09;的名称&#xff0c…

公司来了个00后,起薪就是18K,不愧是卷王。。。

前言 都在传00后躺平、整顿职场&#xff0c;但该说不说&#xff0c;是真的卷&#xff0c;感觉我都要被卷废了... 前段时间&#xff0c;公司招了一个年轻人&#xff0c;其中有一个是00后&#xff0c;工作才一年多&#xff0c;直接跳槽到我们公司&#xff0c;薪资据说有18K&…

Java自学第6课:电商项目(1)

从本课开始&#xff0c;我们跟着项目一起来敲代码。通过项目来学习Java和Java web 1 开始 首先了解要做什么项目&#xff0c;这里选择B2C电商。 需求分析很重要&#xff0c;所以要了解甲方业务流程。 之后配置开发环境&#xff0c;选择开发工具。 然后就是搭建开发环境&…

【16】c++11新特性 —>弱引用智能指针weak_ptr(1)

定义 std::weak_ptr&#xff1a;弱引用的智能指针&#xff0c;它不共享指针&#xff0c;不能操作资源&#xff0c;是用来监视 shared_ptr 中管理的资源是否存在。 use_count #include <iostream> #include <memory> using namespace std;int main() {shared_ptr…

最终前端后端小程序还有nginx配置

前端 前端 build 代码及其 放置位置 后端 nginx.conf 配置 user root;worker_processes auto; error_log /var/log/nginx/error.log notice; pid /var/run/nginx.pid;events {worker_connections 1024; }http {include /etc/nginx/mime.types;default_type a…