Java算法提高之LeetCode刷题Java 杂谈

数据结构之集合与映射(一)

2019-07-31  本文已影响10人  Ice_spring

本篇主要内容:集合及其应用

集合Set

数学上定义集合有最基本的三个性质:确定性、互异性和无序性。所谓确定性就是一个元素是否在这个集合中是确定的,互异性是一个集合中不能有相同的元素,无序性是打乱集合中元素的顺序集合还是这个集合。
在一般的数据结构教材中很少有讲解集合(Set)这种数据结构,不过理解集合这种数据结构是很必要的,它是一种高阶的数据结构,可以基于链表,二分搜索树等来实现。
在java语言中实现集合这种数据结构,首先写一个集合接口,集合可以由不同的方式实现:

'''Set.java'''
public interface Set<E> {
    void add(E e);
    void remove(E e);
    boolean contains(E e);
    int getSize();
    boolean isEmpty();
}

基于链表的实现
java提供的有链表类,不过为了加深对一些操作的理解,这里我们使用的是自己写的链表类,它能实现增删查改的基本功能,而且支持泛型:

'''LinkedList.java'''
public class LinkedList<E> {
    private class Node{
        //链表节点是链表内部类而且私有,用户不需知道底层如何,屏蔽实现细节
        public E e;
        public Node next;
        public Node(E e,Node next){
            this.e = e;
            this.next = next;
        }
        public Node(E e){
            this(e,null);
        }
        public Node(){
            this(null,null);
        }
        @Override
        public String toString(){
            return e.toString();
        }
    }

    private Node dummyhead;
    private int size;

    public LinkedList(){
        dummyhead = new Node(null,null);
        size = 0;
    }

    //获取链表中元素的个数
    public int getSize(){
        return size;
    }
    //返回链表是否为空
    public boolean isEmpty(){
        return size == 0;
    }
    //在链表的index(0-based)位置添加新的元素e
    //链表中不是常用操作,用作测试和练习
    public void add(int index, E e){
        if(index < 0||index > size)
            throw new IllegalArgumentException("Add failed, illegal index");
        else{
            Node prev =dummyhead;
            for(int i = 0;i<index;i++)
                prev = prev.next;

            Node node = new Node(e);
            node.next = prev.next;
            prev.next = node;
            //prev.next = new Node(e,prev.next);

            size ++;
        }
    }
    //链表头添加元素
    public void addFirst(E e){
        add(0,e);
    }
    //链表末尾添加元素e
    public void addLast(E e){
        add(size,e);
    }
    //链表中不是常用操作,用作测试和练习
    //获得链表的第index个元素
    public E get(int index){
        if(index < 0||index > size)
            throw new IllegalArgumentException("Add failed, illegal index");
        Node cur = dummyhead.next;
        for(int i=0;i<index;i++)
            cur = cur.next;
        return cur.e;
    }
    //获得链表的第一个元素
    public E getFirst(){
        return get(0);
    }
    //获得链表的最后一个元素
    public E getLast(){
        return get(size-1);
    }
    //修改index位置的元素为e
    //测试练习用
    public void set(int index,E e){
        //链表中不是常用操作,用作测试和练习
        if(index < 0||index > size)
            throw new IllegalArgumentException("Add failed, illegal index");
        Node cur = dummyhead.next;
        for(int i=0;i<index;i++)
            cur = cur.next;
        cur.e = e;
    }
    //查找链表是否存在元素e
    public boolean contains(E e){
        Node cur = dummyhead.next;
        while(cur != null){
            if(cur.e.equals(e))
                return true;
            cur = cur.next;
        }
        return false;
    }
    //删除index位置的元素,不常用,做练习和测试
    public E remove(int index){
        if(index < 0||index > size)
            throw new IllegalArgumentException("Add failed, illegal index");
        Node prev = dummyhead;
        for(int i=0;i<index;i++)
            prev = prev.next;
        Node retNode = prev.next;
        prev.next = retNode.next;
        retNode.next = null;
        size --;

        return retNode.e;
    }
    //删除链表第一个元素
    public E removeFirst(){
        return remove(0);
    }
    //删除链表最后一个元素
    public E removeLast(){
        return remove(size-1);
    }
    //从链表中删除元素e
    public void removeElement(E e){
        Node prev = dummyhead;
        while(prev.next!=null){
            if(prev.next.e.equals(e))
                break;
            prev = prev.next;
        }
        if(prev.next!=null){
            Node delNode = prev.next;
            prev.next = delNode.next;
            delNode.next = null;
            size --;
        }
    }
    @Override
    public String toString(){
        StringBuilder res = new StringBuilder();
        Node cur = dummyhead.next;
        while(cur != null){
            res.append(cur + "->");
            cur = cur.next;
        }
        res.append("NULL");
        return res.toString();
    }
}

然后基于链表实现我们的集合,集合支持泛型:

public class LinkedListSet<E> implements Set<E> {
    private LinkedList<E> list;
    public LinkedListSet(){
        list = new LinkedList<>();
    }
    @Override
    public int getSize(){
        return list.getSize();
    }
    @Override
    public boolean isEmpty(){
        return list.isEmpty();
    }
    @Override
    public boolean contains(E e){
        return list.contains(e);
    }
    @Override
    public void add(E e){
        if(!list.contains(e))
            list.addFirst(e);
    }
    @Override
    public void remove(E e){
        list.removeElement(e);
    }
}

可以发现在实现链表后,集合中的增删查操作只需调用它底层的链表相应操作就行。
基于BST的实现
BST的实现我们在《算法之美》系列文章递归的妙用中实现过了,这里不再重复,下面给出基于BST的Set:

public class BSTSet<E extends Comparable<E>> implements Set<E> {
    private BST<E> bst;
    public BSTSet(){
        bst = new BST<>();
    }
    @Override
    public int getSize(){
        return bst.size();
    }
    @Override
    public boolean isEmpty(){
        return bst.isEmpty();
    }
    @Override
    public void add(E e){
        bst.add(e);
    }
    @Override
    public boolean contains(E e){
        return bst.contains(e);
    }
    @Override
    public void remove(E e){
        bst.remove(e);
    }
}

虽然两种方式都能实现Set这种数据结构,但是两者的性能存在巨大差异,基于链表实现的Set性能要远远低于基于BST实现的Set,这是为什么呢?这涉及到链表和BST的时间复杂度分析:

操作\方式 链表Set BSTSet
O(n) O(h)
O(n) O(h)
O(n) O(h)

对于我们实现的链表Set来说,不难得出它的增删查时间复杂度都是O(n),这里要说明的是本来链表的添加操作在头部时间复杂度仅为O(1),不过集合要求互异性,所以添加元素时还要检查该元素是否已经存在,故最终添加操作的时间复杂度是O(n)。而对于BSTSet,它的时间复杂度和BST的深度有关,最坏的情况下BST会退化为链表,时间复杂度也会是O(n),不过平均下来,BST的高度是log_2n,故BSTSet的平均时间复杂度是O(log_2n),这是远远优于O(n)的。如果Set是基于平衡二叉树来实现,则最坏情况复杂度也是O(log_2n),这实际上就是java中Set的实现方式。


集合的应用

文本词数统计

文本词数统计有很广泛的应用,比如词数统计可以用于书的难度分级,一本有10000个不同单词的书和一本有3000个不同单词的书显然不是一个难度级别。
现在我们就基于集合来实现一个txt英文小说词数统计的小程序。首先编写一个文件读取类,这个类用于做分词,将txt文本中英文单词提取出来并全部转为小写保存到String数组中:

import java.io.FileInputStream;
import java.util.ArrayList;
import java.util.Scanner;
import java.util.Locale;
import java.io.File;
import java.io.BufferedInputStream;
import java.io.IOException;
public class FileOperation {

    // 读取filename中的内容,并将其中包含的所有词语放进words中
    public static boolean readFile(String filename, ArrayList<String> words){

        if (filename == null || words == null){
            System.out.println("filename is null or words is null");
            return false;
        }
        // 文件读取
        Scanner scanner;
        try {
            File file = new File(filename);
            if(file.exists()){
                FileInputStream fis = new FileInputStream(file);
                scanner = new Scanner(new BufferedInputStream(fis), "UTF-8");
                scanner.useLocale(Locale.ENGLISH);
            }
            else
                return false;
        }
        catch(IOException ioe){
            System.out.println("Cannot open " + filename);
            return false;
        }

        // 简单分词
        if (scanner.hasNextLine()) {

            String contents = scanner.useDelimiter("\\A").next();

            int start = firstCharacterIndex(contents, 0);
            for (int i = start + 1; i <= contents.length(); )
                if (i == contents.length() || !Character.isLetter(contents.charAt(i))) {
                    String word = contents.substring(start, i).toLowerCase();
                    words.add(word);
                    start = firstCharacterIndex(contents, i);
                    i = start + 1;
                } else
                    i++;
        }

        return true;
    }
    // 寻找字符串s中,从start的位置开始的第一个字母字符的位置
    private static int firstCharacterIndex(String s, int start){

        for( int i = start ; i < s.length() ; i ++ )
            if( Character.isLetter(s.charAt(i)) )
                return i;
        return s.length();
    }
}

这只是一个简单的分词过程,全部转换为小写后单词不同就算不同。有学过自然语言处理的可以更加细化一下分词过程,比如将名词的单复数算为一个单词,动词的原型ing、ed、第三人称单数算作一个单词......不过这就比较麻烦了,本文不再进行这样的处理。接下来使用我们的集合对Little Prince这部童话的英文版进行词数统计:

'''Main.java'''
import java.util.ArrayList;
//BST比链表实现的集合更加高效
public class Main {
    private static double testSet(Set<String> set,String filename){
        long startTime = System.nanoTime();
        System.out.println("Little Prince");
        ArrayList<String> words = new ArrayList<>();
        if ((FileOperation.readFile(filename, words))) {
            System.out.println("Total words: " + words.size());
            for (String word : words)
                set.add(word);
            System.out.println("Total different words: " + set.getSize());
        }
        long endTime = System.nanoTime();
        return (endTime - startTime)/1000000000.0;
    }
    public static void main(String[] args) {
        String filename = "little-prince.txt";
        LinkedListSet<String> linkedListSet = new LinkedListSet<>();
        double time2 = testSet(linkedListSet,filename);
        System.out.println(time2+"\n");

        BSTSet<String> bstSet = new BSTSet<>();
        double time1 = testSet(bstSet,filename);
        System.out.println(time1+"\n");

        }
}

在上面的代码中,我们用链表Set和BSTSet都进行了词数统计任务,并对它们的过程计时,运行结果如下:

词数统计

可以看到两种方式都给出了正确的词数统计,而且BSTSet方式明显用时更少。

LeetCode804——唯一摩尔斯密码词

接下来用集合来解决一道LeetCode上的题目,题目描述如下:

LeetCode804-1 LeetCode804-2

这里我们不再用自己的Set,而是用java中提供的TreeSet(底层是平衡二叉树,性能更好)来实现我们的Solution:

import java.util.TreeSet;

class Solution {
    public int uniqueMorseRepresentations(String[] words) {
        String[] codes = {".-","-...","-.-.","-..",".","..-.","--.",
                "....","..",".---","-.-",".-..","--","-.","---",".--.",
                "--.-",".-.","...","-","..-","...-",".--","-..-","-.--","--.."};
        TreeSet<String> set = new TreeSet<>();
        for(String word:words){
            StringBuilder res = new StringBuilder();
            for(int i=0;i<word.length();i++)
                res.append(codes[word.charAt(i)-'a']);//当前字符对应的摩斯码存入res
            set.add(res.toString());
        }
        return set.size();
    }
}

提交,获得通过!

上一篇 下一篇

猜你喜欢

热点阅读