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);
    }
原文地址:https://www.cnblogs.com/njuptzheng/p/13212281.html