美文网首页
基础数据结构,动手写一遍

基础数据结构,动手写一遍

作者: 啦啦哇哈哈 | 来源:发表于2018-10-25 21:32 被阅读0次

    动态Array

    从最基本的动态Array开始实现(实际上可以理解成自己动手实现了个ArrayList)总共实现下面这些方法:



    底层还是使用数组,不过为了实现动态缩容扩容,我们包装了一些方法在里面:

    • 字段
        public class Array<E>{
            //底层还是用数组实现
            private E[] data;
            //size记录data中真实存在的元素个数
            private int size;
             ...
        }
    
    • 构造器
        public Array() {
            //默认实现给10个空间
            this(10);
        }
        
        public Array(E[] data) {//这块是拷贝,不是让底层的data直接指向传入的参数
            //这是Java不允许泛型数组,所以要Object数组强转
            this.data = (E[])new Object[data.length];
            for(int i = 0; i < data.length; i++) {
                this.data[i] = data[i];
            }
            size = data.length;
        }
        
        public Array(int Capacity) {
            //这是Java不允许泛型数组,所以要Object数组强转
            data = (E[]) new Object[Capacity];
            size = 0;
        }
    
    • 封装一个异常检查
      用于下面各种方法中的Index或者findelement的检查,
        private void checkIndexRange(int index) {
            if (index < 0 || index >= data.length) {
                throw new IllegalArgumentException();
            }
        }
    
    • 扩容函数
      我们的扩容策略会在下面add和remove中具体介绍,这里扩容函数就是New一个新容量的数组,然后把原数组的值复制进去就可以了。
        private void resize(int newCapacity) {
            E[] newArray = (E[]) new Object[newCapacity];
            
            for(int i = 0; i < size; i++) {
                newArray[i] = data[i];
            }
            
            data = newArray;
        }
    
    • 一系列get和判空以及contains和find和set
        public int getSize() {
            return size;
        }
        
        public int getCapacity() {
            return data.length;
        }
        
        public boolean isEmpty() {
            return size == 0;
        }
        
        public E get(int index) {
            checkIndexRange(index);
            return data[index];
        }
        
        public E getFirst() {
            return get(0);
        }
        
        public E getLast() {
            return get(size - 1);
        }
        
        public boolean contains(E element) {
            for(int i = 0; i < size; i++) {
                if (Objects.equals(data[i], element)) {
                    return true;
                }
            }
            
            return false;
        }
    
        public int find(E element) {
            for(int i = 0; i < size; i++) {
                if (Objects.equals(data[i], element)) {
                    return i;
                }
            }
            
            return -1;
        }
    
        public void set(int index, E element) {
            checkIndexRange(index);
            
            data[index] = element;
        }
    
    • 增加和扩容策略
      我们在增加时候检查容量,采用最简单的扩容策略,在size等于最大容量时候,扩容为原来size的二倍。
        public void add(int index, E element) {
            checkIndexRange(index);
            
            if (size == data.length) {
                resize(2 * size);
            }
            
            for(int i = size; i > index; i--) {
                data[i] = data[i-1];
            }
            
            data[index] = element;
            
            size ++;
        }
    
        public void addFirst(E element) {
            add(0, element);
        }
        
        public void addLast(E element) {
            add(size, element);
        }
    
    • 删除和缩容策略
      每次在删除元素时候检查size,缩容策略也很简单,最直白的策略就是size == data.length/2时候,又给缩回去,但是这样就会导致一种极端情况,如果数组的元素数目在data.length/2周围反复横跳add/remove,这样就会不停的缩容,扩容,改变容量的复杂度都是O(n),这毫无疑问是一个负担。所以改变一下缩容的策略,在size== data.length/4时候去缩容到原来容量的一半。
        public E remove(int index) {
            checkIndexRange(index);
            //先保存下来要删除(返回)的值
            E ret = data[index];
            
            //然后从index开始把后一个元素赋值给前一个
            for(int i = index; i < size - 1; i++) {
                data[i] = data[i+1];
            }
            
            //最后一个元素赋为Null
            data[size - 1] = null;
            //别忘了维护size
            size --;
            
            //size减小之后如果到了四分之一长度,那就缩容为原来容量的一半,但是同时要注意 data.length/2不是0
            if (size == data.length/4 && data.length / 2 != 0) {
                resize(data.length / 2);
            }
            
            return ret;
        }
        
        public E removeFirst() {
            return remove(0);
        }
        
        public E removeLast() {
            return remove(size);
        }
        
        public E removeElement(E element) {
            int index = find(element);
            
            return remove(index);
        }
    

    这样就基本完成了一个伪的ArrayList,集合框架的源码肯定写的更强一些,也考虑的更周到一些,后面在总结集合源码时候会进行分析。

    PriorityQueue

    首先基于上面实现的Array为底层,实现一个heap。
    需要有下面的方法:


    • 字段
      就只有一个底层的Array,我们用Array直接来模拟堆这个二叉树。需要理解的基本概念是是个节点对应下标如果是i(从0开始),那么它的左孩子就是2 * i+1,右孩子就是2 * i+2,而它的父亲节点是(i - 1) / 2。如果下标从1开始会有不同结果,自行推导即可。
        private Array<E> data;
    
    • 构造器
      先写两个,最后一个最后介绍。
        public MaxHeap() {
            this.data = new Array<>();
        }
        
        public MaxHeap(int Capacity) {
            this.data = new Array<>(Capacity);
        }
        
    
    • 判空和长度以及获取左右孩子和父亲节点
        public boolean isEmpty() {
            return data.isEmpty();
        }
        public int size() {
            return data.getSize();
        }
        private int parent(int index) {
            return (index-1)/2;
        }
    
        private int leftChild(int index) {
            return 2 * index + 1;
        }
        private int rightChild(int index) {
            return 2 * index + 2;
        }
        
    
    • 最核心的shiftUp和shiftDown 都是logn
      shiftUp,就是上浮,当新加入一个节点时候我们把一个值和它的父亲相比,如果它要比父亲节点的值更大,那么就交换节点,以此类推往上不断考察,直到父亲节点比他的值大为止。
      shiftDown则反之,是下沉,这个方法在取出最大值,和Heapify的过程中要用到。是把当前节点和儿子节点相比,不过需要考察两个节点,我们要把当前节点和左右儿子中更大的那个交换,而如果当前节点比左右儿子都大,那就下沉结束了。
        private void shiftDown(int index) {
            while(leftChild(index)< size()) {
                int j = leftChild(index);
                //第一步找到左右孩子中更大的那一个
                if(j + 1 < size() && data.get(j+1).compareTo(data.get(j))>0) {
                    j = rightChild(index);
                }
                //j现在代表的不管是左还是右,反正都是最大的
                //如果当前节点比左右孩子更大的那个还要大,那就说明不再需要shiftDown了,不用再交换
                if (data.get(index).compareTo(data.get(j))>0) {
                    break;
                }
                
                //如果上面的循环没有跳出去,那就说明肯定是孩子比他大的,不管左还是右,只管jjjjjj,交换一下即可
                data.swap(index, j);
                
                //再把指针挪到j上面
                index = j;
            }
        }
        
        private void shiftUp(int index) {
            while(parent(index) >= 0 && data.get(index).compareTo(data.get(parent(index)))>0) {
                data.swap(index, parent(index));
                index = parent(index);
            }
        }
    

    写的代码上来看,shiftUp会更加简单一些,毕竟只需要比较一个节点,shiftDown写的时候,就是找到儿子中最大的那一个,并记录下标,然后把当前节点和最大的那个比一下,这是比较优秀的写法。

    • 添加节点
        /* 
         * 添加过程是先在数组末尾添加新元素,
         * 然后为了保持堆的特性,要对最后添加的新元素进 
         * 行shiftUp操作
         */ 
        public void add(E e) {
            data.addLast(e);
            shiftUp(size() - 1);
        }
    
    • 拿出最大元素
       /*
        * 拿出最大元素之后依然要保持堆的特性,
        *所以这里的方法是,先把最后一个节点值交换到第0位 
        *最大的位置,然后把最后一个位置删除掉,然后再对 
        *最大节点进行下沉操作维护堆的特性。
        */  
        public E extractMax() {
            E max = findMax();
            data.swap(0, size()-1);
            data.removeLast();
            
            shiftDown(0);
            return max;
        }
        
        public E findMax() {
            return data.getFirst();
        }
    
    • 第三个构造器——Heapify
      最后我们再来谈一下第三个构造器的故事......从Heapify这个名字大概能猜到,我们这个构造器是把一个数组“堆化”,思想是把数组拷进来先放在“树”上,这时候它是按照原来数组的顺序依次放进去的,不是一个堆,我们从第一个非叶子节点开始到根节点依次shiftDown,就可以把这个数组变成一个堆。这个复杂度是O(n),暂时想不通,后面查找资料学习后来填坑。
        //这个构造函数对应的过程叫Heapify
        public MaxHeap(E[] array) {
            //先把数组弄进来
            data = new Array<>(array);
            //从第一个非叶子节点开始 shiftDown
            for(int i = parent(size() - 1); i >= 0; i--) {
                shiftDown(i);
            }
        }
    

    这里基本上这个堆的结构是实现了,我们在它之上包装上PriorityQueue的接口就好了。

    public class PriorityQueue<E extends Comparable<E>> implements Queue<E> {
        private MaxHeap<E> maxHeap;
        public PriorityQueue() {
            maxHeap = new MaxHeap<>();
        }
        @Override
        public int getSize() {
            return maxHeap.size();
        }
    
        @Override
        public boolean isEmpty() {
            return maxHeap.isEmpty();
        }
    
        @Override
        public void enqueue(E e) {
            maxHeap.add(e);
        }
    
        @Override
        public E dequeue() {
            return maxHeap.extractMax();
        }
    
        @Override
        public E getFront() {
            return maxHeap.findMax();
        }
    
        
    }
    

    Java的util包下面的PriorityQueue是个最小堆,不过可以通过构造器传入自定义的Comparator来决定这个PriorityQueue的判定标准。

    BST

    二分搜索树是在是再常见不过了。
    首先需要定义一个节点类,我们这里放成内部类。

    • 节点内部类
    public class BinarySearchTree<E> {
        class Node{
            public Node left,right;
            public E e;
            
            public Node(E e){
                this.e = e;
                left = null;
                right = null;
            }
        }
              .....
        
    }
    
    • 字段
      一个树根节点treeRoot,和一个节点数size。
        private Node treeRoot;
        private int size;
    
    • 构造器
        public BinarySearchTree() {
            treeRoot = null;
            size = 0;
        }
    
    • size()和判空
        public int size() {
            return size;
        }
    
        public boolean isEmpty() {
            return size == 0;
        }
    
    • 添加节点
      我们对外提供一个public的void add(E e)方法,然后内部实现是一个返回值为Node的方法,它返回传入的root节点,并递归去找到需要添加的节点的位置,大于当前节点的值到右子树去找,小于当前节点的值到左子树去找。
        public void add(E e) {
            treeRoot = add(treeRoot, e);
        }
        
        private Node add(Node root, E e) {
            if (root == null) {
                size++;
                return new Node(e);
            }
            
            if (root.e.compareTo(e) == 0) {
                return root;
            }
            
            if (e.compareTo(root.e)>0) {
                root.right = add(root.right, e);
            }else {
                root.left = add(root.left, e);
            }
            
            return root;
        }
    
    • contains
      和add的写法比较相似。递归去找e的位置。
        public boolean contains(E e) {
            return contains(treeRoot,e);
        }
        
        private boolean contains(Node root, E e) {
            if (root == null) {
                return false;
            }
            
            if (root.e.compareTo(e)==0) {
                return true;
            }
            
            if (e.compareTo(root.e) > 0) {
                return contains(root.right,e);
            } else {
                return contains(root.left, e);
            }
        }   
    
    • 先序、中序、后序和层序遍历(还有一种在剑指上看到的之字形遍历,都可以通过栈模拟一下)
      先中后序遍历的递归实现。
        public void preOrder() {
            System.out.println("preOrder:");
            preOrder(treeRoot);
        }
        
        private void preOrder(Node root) {
            if (root == null) {
                return ;
            }
            
            System.out.println(root.e);
            preOrder(root.left);
            preOrder(root.right);
        }
    
        public void inOrder() {
            System.out.println("inOrder:");
            inOrder(treeRoot);
        }
        
        private void inOrder(Node root) {
            if (root == null) {
                return ;
            }
            
            inOrder(root.left);
            System.out.println(root.e);
            inOrder(root.right);    
        }
        
        public void postOrder() {
            System.out.println("postOrder:");
            postOrder(treeRoot);
        }
        
        private void postOrder(Node root) {
            if (root == null) {
                return ;
            }
            
            postOrder(root.left);
            postOrder(root.right);
            System.out.println(root.e);
        }
    

    下面是先序的非递归实现,我们是用一个Stack去模拟系统的栈,从树根入栈开始,取栈顶先加右再加左入栈,直到栈取空为止。

        private void preOrderNotRecursively(Node root) {
            if (root == null) {
                return ;
            }
            
            Stack<Node> stack = new Stack<>();
            
            stack.push(root);
            
            while(!stack.isEmpty()) {
                Node node = stack.pop();
                
                System.out.println(node.e);
                
                if (node.right != null) {
                    stack.push(node.right);
                }
                
                if (node.left != null) {
                    stack.push(node.left);
                }
            }
        }
    

    然后再来层序遍历,这种依赖于队列来实现。从根节点入队开始,取出队首节点访问,先入左再入右。直到队空为止。

        public void levelOrder() {
            System.out.println("leverOrder:");
            levelOrder(treeRoot);
        }
        
        private void levelOrder(Node root) {
            if (root == null) {
                return ;
            }
            
            Queue<Node> queue = new LinkedList<>();
            
            queue.offer(root);
            
            while(!queue.isEmpty()) {
                Node node = queue.poll();
                
                System.out.println(node.e);
                
                if (node.left != null) {
                    queue.offer(node.left);
                }
                
                if (node.right != null) {
                    queue.offer(node.right);
                }
            }
        }
    

    接下来的一系列方法,都是为remove服务的,我们一步步来,bst中的remove方法确实要复杂于那些add操作。

    • get最大和最小递归和非递归都写了
        public E getMax() {
            if (isEmpty()) {
                throw new NullPointerException();
            }
            return getMax(treeRoot).e;
        }
        private Node getMax(Node root) {
            if (root.right == null) {
                return root;
            }
            
            return getMax(root.right);
        }
        
        private Node getMaxNotRecursively(Node root) {
            if (root == null) {
                return null;
            }
            
            while(root.right != null) {
                root = root.right;
            }
            
            return root;
        }
        
        public E getMin() {
            if (isEmpty()) {
                throw new NullPointerException();
            }
            return getMin(treeRoot).e;
        }
        
        private Node getMin(Node root) {
            if(root.left == null) {
                return root;
            }
            
            return getMin(root.left);
        }
        
        private Node getMinNotRecursively(Node root) {
            if (root == null) {
                return null;
            }
            
            while(root.left != null) {
                root = root.left;
            }
            
            return root;
        }
        
    
    • removeMax和removeMin
        public E removeMax() {
            E ret = getMax();
            treeRoot = removeMax(treeRoot);
            return ret;
        }
        
        /*
         * 删除以root为根的子树的最大节点
         * 返回删除最大节点后的子树的根
         * 删除的过程是用该节点的左孩子直接代替它
         */
        private Node removeMax(Node root) {
            if (root.right == null) {
                Node leftNode = root.left;
                root.left = null;
                return leftNode;
            }
            
            root.right = removeMax(root.right);
            return root;
        }
        public E removeMin() {
            E ret = getMin();
            treeRoot = removeMin(treeRoot);
            return ret;
        }
        
        /*
         * 删除以root为根的子树中的最小节点
         * 返回删除最小节点后的子树的根
         * 删除的过程是一直往左走,找到最左节点,用该节点的右孩子代替它
         */
        private Node removeMin(Node root) {
            if (root.left == null) {
                Node rightNode = root.right;
                root.right = null;
                return rightNode;
            }
            
            root.left = removeMin(root.left);
            return root;
        }
    
    
    • removeNode删除任意一个节点
      我们上面做的所有工作都是为了它。
        /*
         * 删除以root为根节点的子树中值为e的节点
         * 返回删除目的节点后的子树的根
         * 删除的过程就是根据e的值和当前节点的值的比较结果
         * 向左或者向右去走,直到找到目的节点
         * 这时候有三种情况。
         * 1.左子树为空,那就直接用它的右孩子去代替这个节点
         * 2.右子树为空,那就直接用它的左孩子去代替这个节点
         * 3.左右子树都不为空,我们找它的后继节点successor,
         * 也就是右子树中的最小节点,用这个节点来代替它(或者找
         * 前驱节点,左子树中的最大节点,用这个节点来代替它),还要
         * 记的把后继(前驱)节点删除掉。
         */
        private Node removeNode(Node root, E e) {
            
            if (root == null) {
                return null;
            }
            
            if (e.compareTo(root.e)>0) {
                root.right = removeNode(root.right,e);
                return root;
            }
            
            if (e.compareTo(root.e)<0) {
                root.left = removeNode(root.left, e);
                return root;
            }
            
            if (root.left == null) {
                Node rightNode = root.right;
                root.right = null;
                size--;
                return rightNode;
            }
            
            if (root.right == null) {
                Node leftNode = root.left;
                root.left = null;
                size--;
                return leftNode;
            }
            
            //左右子树都不为空,找到后继节点——右子树中的最小值
            Node successor = getMin(root.right);
            
            successor.right = removeMin(root.right);
            successor.left = root.left;
            
            root.left = null;
            root.right = null;
            
            
            return successor;
        }
    

    相关文章

      网友评论

          本文标题:基础数据结构,动手写一遍

          本文链接:https://www.haomeiwen.com/subject/coaqtqtx.html