美文网首页数据结构
Java 数据结构了解一下

Java 数据结构了解一下

作者: Marker_Sky | 来源:发表于2018-04-18 11:57 被阅读0次

    所谓数据结构,就是数据组合在一起并进行管理的方式,这些方式有表、栈和队列、树、图等。数据结构除了储存数据还管理着数据。
    算法呢,就是以某种方式得出所需要的一个数据或一组数据。实现方式可能是计算,或者遍历等。

    *以上仅为个人理解,有误请指出。

    一、线性表

    概念:线性表是最基本、最简单、也是最常用的一种数据结构。其中数据元素的排列方式是线性的。

    分类:按照元素储存结构可分为顺序表和链式表。

    1.1 顺序表

    元素的存储空间是连续的。比如数组。

    2.2 链表:

    元素的存储空间是离散的、单独的,元素之间的逻辑顺序是通过链表中的指针链接次序实现。链表也分为很多种,有单链表、双链表、循环链表,本文暂时分析单链表。

    单链表中的数据是以结点来表示,每个结点的构成:元素(数据)+指针(指示后继元素的存储位置),下面来实现单链表。

    2.2.1 单链表的创建和遍历
    public class LinkList {
        // 2.头结点和当前结点
        public Node<String> head;
        public Node<String> current;
    
        /**
         *  3.添加新结点
         * @param data 要添加的结点的数据
         */
        public void add(String data){
            if(head == null){// 头结点为null,说明链表中没有元素
                // 创建结点并把新结点赋值给当前结点
                head = new Node<String>(data);
                current = head;
            } else {
                // 创建新节点赋值给当前结点的下一个结点,让表连起来
                current.next = new Node<String>(data);
                // 当指针向后移动一位,保证当前结点指向最新的结点
                // 这样再有新数据就一直向后链接
                current = current.next;
            }
        }
    
        /**
         * 4.打印单链表中所有结点数据
         * @param node 从该结点开始打印
         */
        public void printLinkList(Node node){
            if(node == null){
                return;
            }
            current = node;
            while (current != null){
                System.out.print(current.data);
                current = current.next;
            }
        }
    
        // 1.包含两部分 data(数据),next(下一个结点指针)
        class Node<E>{
            E data;
            Node<E> next;
    
            public Node(E data) {
                this.data = data;
            }
        }
    }
    
    1. 创建结点。LinkList 表示单链表类,内部类 Node 表示单链表的元素。提供构造函数以便创建结点。
    2. 在单链表中创建变量头结点和当前结点,头结点主要用于记录首个结点信息,当前结点用于继续创建后续结点。
    3. 创建结点的函数:
      如果单链表中没有结点,则创建结点传入数据并赋值给头结点。这里头结点作为链表是否为空的重要属性,以及作为遍历单链表所有结点的起始结点。
      如果不为空链表,则创建新结点并赋值给当前的结点的下一个结点,作为连接用。然后将指针向后移动一位指向最新的结点,这样当再添加结点会赋值给最新的结点的 next 保证链接。
    4. 单链表的遍历:遍历的话需要传递某个结点作为参数,并赋值给当前结点,如果链表存在该结点则循环打印结点数据即可。如果想要遍历所有结点数据,就用到变量头结点 head 了,因为它是作为链表的第一个结点存在的,传入 head 就可以遍历所有的结点了。

    测试:

    public class Test {
        public static void main(String[] args){
            LinkList linkList = new LinkList();
            // 创建 LinkList 并添加数据
            // 这时链表为空,第一个元素会作为 head 保存。
            for (int i = 0; i < 10; i++){
                linkList.add("data"+i+"\n");
            }
            // 这时 head 已经有数据了,执行循环遍历即可
            linkList.printLinkList(linkList.head);
        }
    }
    

    结果:

    遍历单链表

    有关单链表的问题还有很多,参考:

    链表面试题Java实现【重要】

    相关问题简单实现:

    • 获取单链表长度:遍历之...
    /**
     * @return 获取链表长度
     */
    public int getLinkListLength(){
        if(head == null){
            return 0;
        }
        int length = 0;
        Node node = head;
        while (node != null){
            length++;
            node = node.next;
        }
        return length;
    }
    
    • 反转一个单链表
    /**
     * 反转链表,循环不会栈溢出
     * @param node
     * @return
     */
    public Node reverseList(Node node){
        if(node == null || node.next == null){
            return node;
        }
        Node current = node;
        Node next = null;
        Node reverseHead = null;
        while (current != null){
            // 1.记录当前结点的下一个结点用于指针后移
            next = current.next;
            // 4.下一轮循环时,旧的表的下一结点链接到新表头
            current.next = reverseHead;
            // 2.将当前结点赋值给新表头
            reverseHead = current;
            // 3.指针后移
            current = next;
        }
        return reverseHead;
    }
    

    这里的思路首先是循环,然后分别记录当前结点、下一个结点和反转后新链表的表头,接着就是循环的赋值:

    1. 记录当前结点的下一个结点,用于赋值给 current 实现指针后移。
    2. 将当前结点赋值给新表头,循环赋值不断替换,这样保证旧链表的最后一个结点是反转后链表的头结点。
    3. 指针后移,因为现在的 next 已经是刚才 current 的下一结点了。
    4. 下一轮循环时,旧的表的下一结点链接到新表头:因为第一轮循环 reverseHead 是 null,所以不用理会。到第二个循环时,这时的 reverseHead 经过第 1 步已经有值且值是初始结点,将 current.next 也就是第三个结点指向初始结点,然后再循环处理到最后实现反转。
    • 查找单链表中间结点
      一种思路是遍历获取链表长度再拿中间,这里记录不允许遍历的情况:
        /**
         * 查找单链表中间
         * @param head
         * @return
         */
        public Node findMidNode(Node head){
            if(head == null){
                return null;
            }
            // 定义两个
            Node first = head;
            Node second = head;
            // 两个同时移动,但是一个走一步另一个走两步
            while (second != null && second.next != null){
                first = first.next;
                second = second.next.next;
            }
            return first;
        }
    

    思路是定义两个结点,起点一样,循环一个走一步一个走两步,这样当走的最快的走不下去时第一个就是中间结点了。

    • 合并两个有序链表,合并后依然有序
      例如:
      链表1:1 > 2 > 3 > 4
      链表2:2 > 3 > 4 > 5
      合并后:1 > 2 > 2 > 3 > 3 > 4 > 4 > 5
    /**
     * 合并两个有序链表
     * @param head1 链表1 表头
     * @param head2 链表2 表头
     * @return
     */
    public Node mergeList(Node head1,Node head2){
        // 如果两个链表都为 null 返回 null
        if(head1 == null && head2 == null){
            return null;
        }
        // 如果其中一个为 null,返回不为 null 的就是要的表头了
        if(head1 == null){
            return head2;
        }
        if(head2 == null){
            return head1;
        }
    
        Node head = null;
        Node current = null;
        // 判断首位,确认新表头
        if((int)head1.data < (int)head2.data ){
            head = head1;
            current = head1;
            head1 = head1.next;
        } else {
            head = head2;
            current = head2;
            head2 = head2.next;
        }
    
        while (head1 != null && head2 != null){
            if((int)head1.data < (int)head2.data){
                current.next = head1; // 循环比较,把较小的值链接到新表后
                current = current.next;// 指针后移
                head1 = head1.next;
            } else {
                current.next = head2;
                current = current.next;
                head2 = head2.next;
            }
        }
        // 经过上面循环,head1 还有值说明 head2 循环完毕
        if(head1 != null){
            // 后面是有序的,所以只要链接到当前结点即可
            current.next = head1;
        }
        // 同理,head1 循环完毕
        if(head2 != null){
            current.next = head2;
        }
        return head;
    }
    

    自行理会吧...233333.

    二、栈

    特点:先进后出(后进先出)。也就是说,保存在栈中的元素会像摞书本一样一个一个压在栈中,取出来的时候会一本一本拿。

    栈模型(图片来源网络)

    Java Stack 类:

    Java Stack
    • push(): 往栈中添加数据
    • pop(): 返回栈顶的值并删除
    • peek(): 返回栈顶的值,但不删除
    • search(Object o): 返回元素 o 最后出现下标

    一个简单的基于数组的实现

    public class Stack {
        // 记录栈顶坐标,同时也可依据该值获取栈顶元素
        private int top = -1;
        private Object[] data;
    
        public Stack(int capacity) throws Exception {
            if(capacity < 0){
                throw new Exception("Illegal capacity:"+capacity);
            }
            data = new Object[capacity];
        }
    
        public Object push(Object o) throws Exception {
            if(top == data.length - 1){
                throw new Exception("Stack is full!");
            }
            return data[++top] = o;
        }
    
        public Object pop() throws Exception {
            if(top == -1){
                throw new Exception("Stack is empty!");
            }
            return data[top--];
        }
    
        public void print(){
            System.out.print("bottom -> top: | ");
            for(int i = 0 ; i <= top ; i++){
                System.out.print(data[i]+" | ");
            }
            System.out.print("\n");
        }
    }
    

    一个简单的基于链表的实现

    public class LinkedStack {
    
        private LinkedList linkedList = new LinkedList();
    
        public void push(Object o){
            linkedList.addFirst(o);
        }
    
        public Object pop(){
            return linkedList.removeFirst();
        }
    
    }
    

    栈和队列的面试题Java实现【重要】

    三、队列

    特点:先进先出 即最先放入的元素最先被取出。元素存放时类似栈,按顺序排列。元素存储时从队尾放入,取出时则先从队头出队。

    Queue示意图(Copy)
    队列的创建:

    一般情况下,队列创建的方式有两种:

    • 基于数组创建(顺序队列)
    • 基于链表创建(链式队列)

    一个简单的队列的实现:

    public class Queue {
    
        public Node head;
        public Node current;
    
        /**
         * 与链表添加数据方式类似
         * @param data 数据
         */
        public void addNode(int data){
            if(head == null){
                head = new Node(data);
                current = head;
            } else {
                current = new Node(data);
                current = current.next;
            }
        }
    
        /**
         * 出队列永远都是队头的数据
         * @return head 的数据
         * @throws Exception
         */
        public int pop() throws Exception {
            if(head == null){
                throw new Exception("队列为空");
            }
            Node node = head; //要出队的Node
            head = node.next; //指针后移,因为头已经出队
            return node.data;
        }
    
        class Node{
            Node next;
            int data;
            public Node(int data){
                this.data = data;
            }
        }
    }
    

    四、树(多数资料来自百度百科)

      树状图是一种数据结构,它是由n(n>=1)个有限节点组成一个具有层次关系的集合。把它叫做“树”是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。它具有以下的特点:
      每个节点有零个或多个子节点;没有父节点的节点称为根节点;每一个非根节点有且只有一个父节点;除了根节点外,每个子节点可以分为多个不相交的子树;

    定义:

    树(tree)是包含n(n>0)个结点的有穷集,其中:
    (1) 每个元素称为结点(node);
    (2) 有一个特定的结点被称为根结点或树根(root)。
    (3) 除根结点之外的其余数据元素被分为m(m≥0)个互不相交的集合T1,T2,……Tm-1,其中每一个集合Ti(1<=i<=m)本身也是一棵树,被称作原树的子树(subtree)。


    相关概念:
    • 树的高度/深度,结点的层次(Level):从根开始定义起,根为第一层,根的子结点为第二层。依次类推,如上图的树深度/高度为 4。
    • 森林:m(m>=0)棵互不相交的树的集合。
    种类:
    • 无序树:树中任意节点的子结点之间没有顺序关系,可以互换,这种树称为无序树,也称为自由树;
    • 有序树:树中任意节点的子结点之间有顺序关系,这种树称为有序树;
    • 二叉树:每个节点最多含有两个子树的树称为二叉树;
    • 完全二叉树:若设二叉树的深度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层所有的结点都连续集中在最左边,这就是完全二叉树。
    完全二叉树
    • 满二叉树:除最后一层无任何子节点外,每一层上的所有结点都有两个子结点二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是(2^k) -1 ,则它就是满二叉树。
    各种树
    • 哈夫曼树:给定n个权值作为n个叶子结点,构造一棵二叉树,若带权路径长度达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree)。哈夫曼树是带权路径长度最短的树,权值较大的结点离根较近。

    二叉树

    二叉树的结点:
    class TreeNode<E>{
        E data;
        TreeNode left;
        TreeNode right;
    
        public TreeNode(E data){
            this.data = data;
        }
    }
    
    二叉树的创建:
    private List<TreeNode> datas;//存储所有的节点
    public TreeNode root;//根节点
    
    public BinaryTree(){
    }
    
    public void createTree(Object[] objs){
        datas = new ArrayList<>();
        for (Object object : objs) {
            datas.add(new TreeNode(object));
        }
        root=datas.get(0);//将第一个元素作为根节点
        for(int i = 0; i < objs.length/2; i++){
            datas.get(i).left = datas.get(i*2+1);
            if(i*2+2 < datas.size()){// 避免下标越界
                datas.get(i).right = datas.get(i*2+2);
            }
        }
    }
    
    二叉树的遍历:

    二叉树的遍历方式有很多,如果限制了从左到右的方式,主要可以分为四种:

    1. 前序/先序遍历
      先访问根结点,然后前序遍历左子树,再前序遍历右子树。
    //前序/先序遍历
    public void preorder(TreeNode<E> root){
        if(root == null){
            return;
        }
        System.out.print(root.data+"");
        preorder(root.left);
        preorder(root.right);
    }
    
    1. 中序遍历
    //中序遍历
    public void inorder(TreeNode<E> root){
        if(root == null){
            return;
        }
        inorder(root.left);
        System.out.print(root.data+"");
        inorder(root.right);
    }
    
    1. 后序遍历
    //后序遍历
    public void postorder(TreeNode<E> root) {
        if (root == null){
            return;
        }
        postorder(root.left);
        postorder(root.right);
        System.out.println(root.data + " ");
    }
    

    三种遍历方式都用到了递归,从代码上看区别就是打印数据的位置。

    二叉树的高度:

    同样是利用递归以及左右子结点判断逐渐增加高度。

    public int height(TreeNode root) {
        if (root == null)
            return 0;// 递归结束:空树高度为0
        else {
            int i = height(root.left);
            int j = height(root.right);
            return (i < j) ? (j + 1) : (i + 1);
        }
    }
    
    二叉树的结点数量:
    public int size(TreeNode root) {
        if (root == null) {
            return 0;
        } else {
            return 1 + size(root.left) + size(root.right);
        }
    }
    
    二叉树的结点插入:
    public String insert(int value) {
        String error = null;
    
        TreeNode node = new TreeNode(value);
        if (root == null) {
            root = node;
            root.left  = null;
            root.right = null;
        } else {
            TreeNode current = root;
            TreeNode parent = null;
            while (true) {
                if (value < (int)current.data) {
                    parent = current;
                    current = current.left;
                    if (current == null) {
                        parent.left = node;
                        break;
                    }
                } else if (value > (int)current.data) {
                    parent = current;
                    current = current.right;
                    if (current == null) {
                        parent.right = node;
                        break;
                    }
                } else {
                    error = "having same value in binary tree";
                    System.out.print(error);
                }
            } // end of while
        }
        return error;
    }
    
    二叉树的结点删除:
    /**
     * 删除结点
     * @param value
     * @return
     */
    public boolean delete(int value) {
        TreeNode current = root;    //需要删除的节点
        TreeNode parent = null;     //需要删除的节点的父节点
        boolean isLeftChild = true;   //需要删除的节点是否父节点的左子树
    
        while (true) {
            if (value == (int)current.data) {
                break;
            } else if (value < (int)current.data) {
                isLeftChild = true;
                parent = current;
                current = current.left;
            } else {
                isLeftChild = false;
                parent = current;
                current = current.right;
            }
    
            //找不到需要删除的节点,直接返回
            if (current == null)
                return false;
        }
    
        //分情况考虑
        //1、需要删除的节点为叶子节点
        if (current.left == null && current.right == null) {
            //如果该叶节点为根节点,将根节点置为null
            if (current == root) {
                root = null;
            } else {
                //如果该叶节点是父节点的左子节点,将父节点的左子节点置为null
                if (isLeftChild) {
                    parent.left  = null;
                } else { //如果该叶节点是父节点的右子节点,将父节点的右子节点置为null
                    parent.right = null;
                }
            }
        }
        //2、需要删除的节点有一个子节点,且该子节点为左子节点
        else if (current.right == null) {
            //如果该节点为根节点,将根节点的左子节点变为根节点
            if (current == root) {
                root = current.left;
            } else {
                //如果该节点是父节点的左子节点,将该节点的左子节点变为父节点的左子节点
                if (isLeftChild) {
                    parent.left = current.left;
                } else {  //如果该节点是父节点的右子节点,将该节点的左子节点变为父节点的右子节点
                    parent.right = current.left;
                }
            }
        }
        //2、需要删除的节点有一个子节点,且该子节点为右子节点
        else if (current.left == null) {
            //如果该节点为根节点,将根节点的右子节点变为根节点
            if (current == root) {
                root = current.right;
            } else {
                //如果该节点是父节点的左子节点,将该节点的右子节点变为父节点的左子节点
                if (isLeftChild) {
                    parent.left = current.right;
                } else {  //如果该节点是父节点的右子节点,将该节点的右子节点变为父节点的右子节点
                    parent.right = current.right;
                }
            }
        }
        //3、需要删除的节点有两个子节点,需要寻找该节点的后续节点替代删除节点
        else {
            TreeNode successor = getSuccessor(current);
            //如果该节点为根节点,将后继节点变为根节点,并将根节点的左子节点变为后继节点的左子节点
            if (current == root) {
                root = successor;
            } else {
                //如果该节点是父节点的左子节点,将该节点的后继节点变为父节点的左子节点
                if (isLeftChild) {
                    parent.left = successor;
                } else {  //如果该节点是父节点的右子节点,将该节点的后继节点变为父节点的右子节点
                    parent.right = successor;
                }
            }
        }
        current = null;
        return true;
    }
    
    /**
     *
     * 得到后继节点,即删除节点的左后代
     */
    private TreeNode getSuccessor(TreeNode delNode) {
        TreeNode successor = delNode;
        TreeNode successorParent = null;
        TreeNode current = delNode.right;
    
        while (current != null) {
            successorParent = successor;
            successor = current;
            current = current.left;
        }
    
        //如果后继节点不是删除节点的右子节点时,
        if (successor != delNode.right) {
            //要将后继节点的右子节点指向后继结点父节点的左子节点,
            successorParent.left = successor.right;
            //并将删除节点的右子节点指向后继结点的右子节点
            successor.right = delNode.right;
        }
        //任何情况下,都需要将删除节点的左子节点指向后继节点的左子节点
        successor.left = delNode.left;
    
        return successor;
    }
    
    测试:
    public static void main(String[] args){
    
        BinaryTree binTree=new BinaryTree();
        Object[] objs={0,1,2,3,4,5,6,7,8,9};
        binTree.createTree(objs);
        binTree.insert(10);
        binTree.preorder(binTree.root);
    //        binTree.inorder(binTree.root);
    //        binTree.postorder(binTree.root);
    //        System.out.print(binTree.height(binTree.root)+" ");
    }
    

    关于二叉树更多资料:

    二叉树的java实现
    二叉树之Java实现二叉树基本操作

    五、图

    暂时还没有用到图,等到用到的时候再来复习。

    六、排序

    有空记得自己实现。

    必须知道的八大种排序算法【java实现】(一) 冒泡排序、快速排序

    其它比较好的文章:

    Android程序员面试会遇到的算法(part 1 关于二叉树的那点事) 附Offer情况
    那些年,我们被问到的数据结构

    相关文章

      网友评论

        本文标题:Java 数据结构了解一下

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