zoukankan      html  css  js  c++  java
  • Java数据结构与算法之链表

    二、链表

    1、介绍

    链表是一个有序的列表,上一个数据连接下一个数据,通过链表指针连接,顺序不可改变。

    看一下链表在内存中的存储结构:

    1. 链表是以节点的形式存储在内存空间中,是链式存储;
    2. 每个节点包括data域(存储数据)和next域(存储指向下一节点的地址值);
    3. 虽然节点是有顺序的,但是在内存中去不是连续的,通过各个节点的连接实现有序存储;
    4. 链表分为有头节点和无头节点,头节点内只存储next域,指向链表的第一个节点。
    

    1.1 链表的初始化

    // 定义节点,每个LinkedNode对象就是一个节点
    class LinkedNode {
        int val;
        String name;
        LinkedNode next; // 指向下一个节点
    
    	// 构造器
        public LinkedNode(int val, String name) {
            this.val = val;
            this.name = name;
        }
    
    	// 重写toString
        @Override
        public String toString() {
            return "LinkedNode{" +
                    "val=" + val +
                    ", name=" + name +
                    '}';
        }
    }
    

    1.2 链表的添加

    class SingleLinkedList {
    	// 定义一个头节点
        LinkedNode headNode = new LinkedNode(0,"");
    
        public LinkedNode getHead() {
            return head;
        }
        // 添加节点到单向链表
        // 1.找到当前列表的最后节点
        // 2.将这个节点的next域指向要添加的节点
        public void add(LinkedNode linkedNode) {
        	// 因为head节点永远指向第一个节点处,一旦移动的话就会找不到第一个节点
        	// 因此我们需要一个辅助节点
        	// 添加链表所以存在链表为空的情况,所以将temp设为头节点
            LinkedNode temp = headNode;
    
            while (true) {
            	
            	// 因为temp此时是头节点,所以要判断.next是否为空
                if (temp.next == null) {
                    break;
                }
                temp = temp.next;
            }
            temp.next = linkedNode;
        }
    
        // 遍历链表
        public void showList() {
            if (headNode.next == null) {
                System.out.println("链表为空");
                return;
            }
            // 同样的原因
            // 因为上面已经判断了链表为空,所以这里可以直接将temp设置为第一个节点
            LinkedNode temp = headNode.next;
            while (true) {
            	// 这里的边界条件就可以直接判断temp是否为空了
                if (temp == null) {
                    break;
                }
                System.out.println(temp.toString());
                temp = temp.next;
            }
        }
    }
    

    这里我想谈一下本人之前对于辅助节点temp的一个错误认识:我之前的理解在于既然新建了一个辅助节点temp,那么之后的每一个temp = temp.next就是在重构了一个链表。
    但现在有了新的理解,原先链表保持不变,改变的仅仅是temp,仅仅从temp的next域获得下一节点的地址,每一次temp = temp.next,就是把temp节点移动到正确的位置上,temp仅仅是原链表内部节点的映射。temp内部包含所映射节点的所有信息,包括data域和next域。

    1.3链表的有序添加

    首先我们需要遍历链表,找到待添加节点linkedNode的正确添加位置。存在三种情况:位置在链表末尾;在链表中间;链表中已存在待添加节点。在末尾和已存在的情况较为好处理,我们来看下在链表中间情况。
    假设我们已遍历找到节点temp的next节点比待添加节点linkedNode大,那么意味着待添加节点linkedNode应该位于temp和temp.next之间。所以我们让linkedNode指向temp.next,temp指向linkedNode即可。

    	public void addOrder(LinkedNode linkedNode) {
        	// 因为head节点永远指向第一个节点处,一旦移动的话就会找不到第一个节点
        	// 因此我们需要一个辅助节点
        	// 添加链表所以存在链表为空的情况,所以将temp设为头节点
            LinkedNode temp = headNode;
    		boolean flag = false; // flag定义为链表内是否存在即将添加的节点
    		
            while (true) {
            	if(temp.next == null) { // 表示遍历结束,节点可直接添加在链表尾部
            		break;
            	}
            	if(temp.next.val == linkedNode.val) { // 表示已存在待添加节点
            		flag = true;
            		break;
            	}else if(temp.next.val > linkedNode.val) { // 表示已经找到位置,具体分析看图示
            		break;
            	}
            	temp = temp.next;
            }
            if(flag) {
            	System.out.println("要添加的节点已经存在了");
            }else {
            	linkedNode.next = temp.next;
            	temp.next = linkedNode;
            }
        }
    

    1.4 单链表节点的修改

    思路就是遍历链表,找到对应节点,然后将新节点的内容重新赋值给旧的节点。

    	public void update(LinkedNode linkedNode) {
    	
    		// 先判断链表是否为空
        	if (headNode.next == null) {
                System.out.println("链表为空");
                return;
            }
            
            LinkedNode temp = headNode.next;
            boolean flag = false;
    
            while (true) {
            	if(temp == null) {
            		break;
            	}
            	if(temp.val == linkedNode.val) {
            		flag =true;
            		break;
            	}
            	temp = temp.next;
            }
            if(flag) {
            	temp.name = linkedNode.name;
            }esle {
            	System.out.println("未找到对应节点");
            }
        }
    

    1.5 单链表节点的删除

    思路依旧是遍历链表,找到待删除的节点temp.next。既然要删除temp.next,意思就是temp指向的是temp.next.next。

    	public void deletData(LinkedNode linkedNode) {
    	
    		// 先判断链表是否为空
        	if (headNode.next == null) {
                System.out.println("链表为空");
                return;
            }
            
            LinkedNode temp = headNode;
    		boolean flag = false;
    		
            while (true) {
            	if(temp.next == null) {
            		break;
            	}
            	if(temp.next.val == linkedNode.val) {
            		flag =true;
            		break;
            	}
            	temp = temp.next;
            }
            if(flag) {
            	temp.next = temp.next.next;
            }esle {
            	System.out.println("未找到对应节点");
            }
        }
    

    2、双向链表的简单叙述

    2.1 双向链表的初始化

    // 定义节点,每个LinkedNode对象就是一个节点
    class LinkedNode2 {
        int val;
        String name;
        LinkedNode2 next; // 指向下一个节点
        LinkedNode2 pre; // 指向前一个节点
    
    	// 构造器
        public LinkedNode2(int val, String name) {
            this.val = val;
            this.name = name;
        }
    
    	// 重写toString
        @Override
        public String toString() {
            return "LinkedNode2{" +
                    "val=" + val +
                    ", name=" + name +
                    '}';
        }
    }
    

    2.2 双向链表的添加

    class DoubleLinkedList {
    	// 定义一个头节点
        LinkedNode2 headNode = new LinkedNode2(0,"");
    
        // 添加节点到单向链表
        // 1.找到当前列表的最后节点
        // 2.将这个节点的next域指向要添加的节点
        public void add(LinkedNode2 linkedNode) {
        	// 因为head节点永远指向第一个节点处,一旦移动的话就会找不到第一个节点
        	// 因此我们需要一个辅助节点
        	// 添加链表所以存在链表为空的情况,所以将temp设为头节点
            LinkedNode2 temp = headNode;
    
            while (true) {
            	
            	// 因为temp此时是头节点,所以要判断.next是否为空
                if (temp.next == null) {
                    break;
                }
                temp = temp.next;
            }
            temp.next = linkedNode;
            linkedNode.pre = temp;
        }
    
        // 遍历链表
        public void showList() {
            if (headNode.next == null) {
                System.out.println("链表为空");
                return;
            }
            // 同样的原因
            // 因为上面已经判断了链表为空,所以这里可以直接将temp设置为第一个节点
            LinkedNode2 temp = headNode.next;
            while (true) {
            	// 这里的边界条件就可以直接判断temp是否为空了
                if (temp == null) {
                    break;
                }
                System.out.println(temp.toString());
                temp = temp.next;
            }
        }
    }
    

    2.3 双向链表的修改

    与单向链表的遍历一致

    public void updata(LinkedNode2 linkedNode) {
        if(headNode.next == null) {
            System.out.println("链表为空");
            return;
        }
        
        LinkedNode2 temp = headNode.next;
        boolean flag = false;
        
        while(true) {
            if(temp == null) {
                return;
            }
           if(temp.val == linkedNode.val) {
               flag =true;
               break;
           }
           temp = temp.next;
        }
        if(flag) {
            temp.name = linkedNode.name;
        }else {
            System.out.println("为找到对应节点");
        }
    }
    

    2.4 双向链表的删除

    	public void deletData(LinkedNode2 linkedNode) {
    	
    		// 先判断链表是否为空
        	if (headNode.next == null) {
                System.out.println("链表为空");
                return;
            }
            
            LinkedNode2 temp = headNode.next;
    		boolean flag = false; // 标志是否找到待删除节点
    		
            while (true) {
            	if(temp == null) {
            		break;
            	}
            	if(temp.val == linkedNode.val) {
            		flag =true;
            		break;
            	}
            	temp = temp.next;
            }
            
            if(flag) { // 找到节点
            	// 将temp前一节点指向temp后一节点
            	temp.pre.next = temp.next;
            	// 如果是最后一个节点,则无法将后一个节点指向前一个节点,需要判断
            	if(temp.next != null) {
            		temp.next.pre = temp.pre;
            	}
            }esle {
            	System.out.println("未找到对应节点");
            }
        }
    

    3、Joseph问题

    约瑟夫环问题(Joseph)又称丢手绢问题:已知 m 个人围坐成一圈,由某人起头,下一个人开始从 1 递增报数,报到数字 n 的那个人出列,他的下一个人又从 1 开始报数,数到 n 的那个人又出列;依此规律重复下去,直到 m 个人全部出列约瑟夫环结束。如果从 0 ~ (m-1) 给这 m 个人编号,请输出这 m 个人的出列顺序。

    3.1 环形链表的初始化

    class BoyLinkedNode {
        int id;
        BoyLinkedNode next;
    
    
        public BoyLinkedNode(int id) {
            this.id = id;
        }
    
        @Override
        public String toString() {
            return "BoyLinkedNode{" +
                    "id=" + id +
                    '}';
        }
    }
    

    3.2 环形链表的添加

    class BoyLinkedList {
        private BoyLinkedNode first = null;
    
        /**
         * 创建约瑟夫环
         * @param num 表示有num个人
         */
        public void addNode(int num) {
        	// 判断输入是否合理
            if (num < 1) {
                System.out.println("人数太少");
                return;
            }
    
    		// 同样first节点不可移动,需要一个辅助节点
            BoyLinkedNode curBoy = null;
    		// for循环往链表中增加节点
            for (int i = 1; i <= num; i++) {
                BoyLinkedNode boy = new BoyLinkedNode(i);
                // 第一个节点需要单独添加
                // 便于将头结点映射给first节点
                if (i == 1) {
                    first = boy;
                    first.next = first; // 一个节点也需要成环形链表
                    curBoy = first; // 映射辅助节点
                }
    
                curBoy.next = boy;
                boy.next = first;
                curBoy = boy;
            }
        }
    }
    

    3.3 约瑟夫环的游戏部分

    /**
         *
         * @param startId 表示从哪个节点开始数
         * @param countNum 表示数几下
         * @param num 表示一共有几个节点
         */
        public void countBoy(int startId, int countNum, int num) {
            // 先判断输入的合理性
            if (startId < 1 || startId > num || num < 1) {
                System.out.println("重新输入");
                return;
            }
    
    		// 需要一个辅助节点helperBoy与first配合,first寻找出局节点,helperBoy负责将出局节点的前后节点连接
    		// 不再需要原链表,所以first可以移动
            BoyLinkedNode helperBoy = first;
    		// helperBoy节点到达指定节点
            while (true) {
                if (helperBoy.next == first) {
                    break;
                }
                helperBoy = helperBoy.next;
            }
    		// first节点到达开始读数节点位置,helperBoy跟随
            for (int i = 0; i < startId-1; i++) {
                first = first.next;
                helperBoy = helperBoy.next;
            }
    
            while (true) {
            	// 跳出循环条件,helperBoy==first
                if (helperBoy == first) {
                    break;
                }
                // for循环寻找出局节点,
                for (int i = 0; i < countNum-1; i++) {
                    first = first.next;
                    helperBoy = helperBoy.next;
                }
                System.out.printf("%d出队
    ",first.id);
                helperBoy.next = first.next; // 节点出局
                first = first.next;
            }
            System.out.printf("%d出队
    ",first.id);
        }
    
  • 相关阅读:
    Springboot配置多数据源Rabbitmq
    SpringBoot 搭建 Rabbitmq
    SpringBoot 成Rabbitmq的疑惑记录
    Docker安装Redis关于Mounts denied解决
    使用Preferences写入注册表
    RSA解密报错 javax.crypto.BadPaddingException: Decryption error
    星座和生肖转化
    bio与nio
    跳表
    springboot+dubbo+zookeeper
  • 原文地址:https://www.cnblogs.com/njuptzheng/p/13212281.html
Copyright © 2011-2022 走看看