目录
2.8修改索引为index位置的元素为新值,返回修改前的元素值
数组这种结构适用于频繁查询低频插入和修改的场景,若频繁插入和删除时,由于需要进行元素的搬移以及扩容等操作,浪费空间,性能开销较大,因此引入了链表。
链表是逻辑连续的,不是物理连续的。
物理连续指的是前一个元素一定是位于后一个元素之前,典型的就是数组。
逻辑连续是指每一个结点之间通过一个“钩子”连接,没有这个钩子,每个节点之间是彼此独立的毫无关系。优点就是可以在任意结点之前或者之后插入新的结点,对其他的节点并不造成影响。不需要考虑空间是否够用以及扩容问题,不会造成空间的浪费。
由于链表可以根据是否带头结点、是否循环、是否双向,排列组合有8中结构,只需要考虑三种结构:带向不带头的单链表(单向链表),单项带头链表,双向不带头链表。
定义线性表接口Seqlist。定义接口带来的好处:就是可以以非常低的成本来更换具体的子类。
- public interface SeqList {
- void add(int val);
- // 在索引为index的位置插入新元素
- void add(int index,int val);
- // 查询线性表中是否包含指定元素val
- boolean contains(int val);
- // 返回索引为index的元素值
- int get(int index);
- // 修改索引为index位置的元素为新值,返回修改前的元素值
- int set(int index,int newVal);
- // 删除线性表中索引为index的元素,返回删除前的元素值
- int removeByIndex(int index);
- // 删除第一个值为val的元素
- void removeByValueOnce(int val);
- // 在线性表中删除所有值为val的元素
- void removeAllValue(int val);
- }
有两个特殊的结点:头结点:只有头结点没有前驱。尾结点:只有尾结点没有后继。
单链表只能从前向后遍历,无论是插入还是删除方法在链表中都要找到操作位置的前驱结点。
val表示该结点中存储的数值,next属性保存下一个结点的地址,若没有下一个结点则为null。
size表示该链表中保存的有效元素个数,head 保存了第一个结点的地址。
- public class MyLinkedList implements SeqList {
- private int size;
- private Node head;
- private class Node {
- private int val;
- private Node next;
- public Node(int val) {
- this.val = val;
- }
- }
- }
必须先让新结点先挂在原先头结点的前面,然后再让head指向新的结点。
- public void addFirst(int val) {
- Node node = new Node(val);
- node.next = head;
- head = node;
- size ++ ;
- }
首先要排除不合法的情况,然后根据index的特殊索引选择头插法,然后剩下的情况再继续分析,首先需要遍历该列表停在index位置的前驱节点,然后将该位置的next指向新的node,将node的next修改为原位置的next,最后size++。
- public void add(int index, int val) {
- if (index < 0 || index > size) {
- throw new IllegalArgumentException("add index illegal!");
- }
- if (index == 0) {
- addFirst(val);
- return;
- }
- Node prev = head;
- for (int i = 1; i
- prev = prev.next;
- }
- Node node = new Node(val);
- node.next = prev.next;
- prev.next = node;
- size++;
- }
2.4尾插法
尾插法不必全部写出,在按照索引插入中,index == size时就是尾插, 并且同样是有前驱节点,尾结点本来指向的就是空,所以新的尾结点也应该指向空。所以尾插法中写入 add(size,val)即可。
- public void add(int val) {
- add(size,val);
- }
2.5遍历链表
不能直接使用head进行遍历,遍历一次之后,头结点的地址就找不到了。所以创建一个n使他等于head,用n进行遍历。
- public String toString() {
- StringBuilder sb = new StringBuilder();
- for(Node n = head;n!=null;n = n.next){
- sb.append(n.val);
- sb.append("->");
- if(n.next == null){
- sb.append("NULL");
- }
- }
- return sb.toString();
- }
- public static void main(String[] args) {
- MyLinkedList myLinkedList = new MyLinkedList();
- myLinkedList.addFirst(1);
- myLinkedList.add(3);
- myLinkedList.add(5);
- myLinkedList.addFirst(7);
- myLinkedList.add(2,10);
- System.out.println();
- }
调用三种添加方法,尾插3,头插1,尾插5,头插7,在索引为3的位置插入4。最后的结果应为1,3,5,4,7。
2.6查询线性表中是否包含指定元素
创建一个Node类型的n使他等于head,然后遍历整个链表,当第一次遇见n的val等于指定元素,即返回true。如果遍历到最后还是没有,则返回false。遍历到最后意味着n==null。
- public boolean contains(int val) {
- for(Node n = head;n!= null;n = n.next){
- if(n.val == val){
- return true;
- }
- }
- return false;
- }
- public static void main(String[] args) {
- MyLinkedList myLinkedList = new MyLinkedList();
- myLinkedList.add(3);
- myLinkedList.addFirst(1);
- myLinkedList.add(5);
- myLinkedList.add(7);
- myLinkedList.add(3,4);
- System.out.println(myLinkedList.toString());
- System.out.println(myLinkedList.contains(40));
- System.out.println(myLinkedList.contains(4));
- }
2.7返回索引为index的元素值
首先要创建一个rangeCheck来判断index是否合法。然后遍历链表,找到索引为index的结点,返回该结点的值。
- public int get(int index) {
- if(!rangCheck(index)){
- throw new IllegalArgumentException("get index illegal!");
- }
- Node node = head;
- for(int i = 0;i
- node = node.next;
- }
- return node.val;
- }
-
- private boolean rangCheck(int index) {
- return index >= 0 && index < size;
- }
- public static void main(String[] args) {
- MyLinkedList myLinkedList = new MyLinkedList();
- myLinkedList.add(3);
- myLinkedList.addFirst(1);
- myLinkedList.add(5);
- myLinkedList.add(7);
- myLinkedList.add(3,4);
- System.out.println(myLinkedList.toString());
- System.out.println(myLinkedList.get(2));
- }
2.8修改索引为index位置的元素为新值,返回修改前的元素值
- public int set(int index, int newVal) {
- if(!rangCheck(index)){
- throw new IllegalArgumentException("get index illegal!");
- }
- Node node = head;
- for(int i = 0;i
- node = node.next;
- }
- int temp = node.val;
- node.val = newVal;
- return temp;
- }
- public static void main(String[] args) {
- MyLinkedList myLinkedList = new MyLinkedList();
- myLinkedList.add(3);
- myLinkedList.addFirst(1);
- myLinkedList.add(5);
- myLinkedList.add(7);
- myLinkedList.add(3,4);
- System.out.println(myLinkedList.toString());
- System.out.println(myLinkedList.set(4,9));
- System.out.println(myLinkedList.toString());
- System.out.println(myLinkedList.set(0,10));
- System.out.println(myLinkedList.toString());
- System.out.println(myLinkedList.set(2,20));
- System.out.println(myLinkedList.toString());
- }
2.9删除链表中索引为index的元素,返回删除前的元素值
这个方法可以引申出链表的另外两个方法,删除头结点和尾结点。只需要调用该函数并且把index设置为0或者size-1。
想要删除在索引位置上的元素,就需要找到该位置的前驱结点,使前驱节点的与索引位置的next结点连接,这时候就有一个特殊情况,头结点是没有前驱结点的,所以需要把头结点单独分析。
排除不合法的情况后,现针对index == 0来分析,直接将head = head.next即可,同时要让长度减一。剩下的情况只需要找到index位置的前驱结点,使它的next = index位置的next。
- public int removeFirst(){
- return removeByIndex(0);
- }
- public int removeLast(){
- return removeByIndex(size-1);
- }
- public int removeByIndex(int index) {
- if(!rangCheck(index)){
- throw new IllegalArgumentException("get index illegal!");
- }
- if(index ==0){
- int tmp = head.val;
- head = head.next;
- size--;
- return tmp;
- }
- Node node = head;
- for(int i = 1;i
- node = node.next;
- }
- int tmp = node.next.val;
- node.next = node.next.next;
- size--;
- return tmp;
- }
- public static void main(String[] args) {
- MyLinkedList myLinkedList = new MyLinkedList();
- myLinkedList.add(1);
- myLinkedList.add(3);
- myLinkedList.add(5);
- myLinkedList.add(7);
- myLinkedList.add(3,4);
- System.out.println(myLinkedList.toString());
- System.out.println(myLinkedList.removeFirst());
- System.out.println(myLinkedList.removeLast());
- System.out.println(myLinkedList.removeByIndex(1));
- System.out.println(myLinkedList.toString());
- }
先删除头结点,然后删除尾结点,再删除下标为1的数。
2.10删除第一个值为val的元素
想要删除值为的元素,就需要找到它的前驱结点,使前驱节点的与查找元素的后继结点连接,这种时候也需要将头结点单独来看,因为头结点没有前驱结点。
并且如果链表为null这种情况也要分开处理,因为链表为空代表链表中没有值处理不了。
逻辑上的终止条件(假设要删除位置的前驱结点为prev):此处删除的是prev的后继节点,因此保证prev的后继节点必须存在,所以终止条件是prev.next != null。
- public void removeByValueOnce(int val) {
- if(head == null){
- return ;
- }
- if(head.val==val){
- int tmp = head.val;
- head = head.next;
- size--;
- return ;
- }
- for(Node prev = head;prev.next!= null;prev = prev.next){
- if(prev.next.val== val){
- prev.next = prev.next.next;
- size--;
- return;
- }
- }
- }
- public static void main(String[] args) {
- MyLinkedList myLinkedList = new MyLinkedList();
- myLinkedList.add(7);
- myLinkedList.add(1);
- myLinkedList.add(3);
- myLinkedList.add(7);
- myLinkedList.add(5);
- myLinkedList.add(3,4);
- myLinkedList.add(7);
- System.out.println(myLinkedList.toString());
- myLinkedList.removeByValueOnce(7);
- System.out.println(myLinkedList.toString());
- myLinkedList.removeByValueOnce(7);
- System.out.println(myLinkedList.toString());
- myLinkedList.removeByValueOnce(7);
- System.out.println(myLinkedList.toString());
- }
2.11在表中删除所有值为val的元素
若链表为空则直接返回,若链表的头结点就是待删除的结点,需要考虑删除后补上的结点的值也是val,所以需要进行while循环,直到head的值不是val,在这种情况中需要让head!=null ,否则无法取得head的val值。
进行完这一步,head可能为空,也可能head的值不是val。同样创建prev进行遍历使 prev= head。所以需要先保证prev不为空,才能取得prev.next的val值。
删除一个结点之后不能直接使prev引用向后走,prev引用直到prev.next.val != val时prev才能向后走,否则如果补上的结点值也为val则会被忽略。
- public void removeAllValue(int val) {
- if(head == null){
- return ;
- }
- while (head!=null&&head.val==val){
- int tmp = head.val;
- head = head.next;
- size--;
- }
- if(head == null){
- return ;
- }
- Node prev = head;
- while(prev.next!=null){
- if(prev.next.val== val){
- prev.next = prev.next.next;
- size--;
- }else {
- prev = prev.next;
- }
- }
- }
- public static void main(String[] args) {
- MyLinkedList myLinkedList = new MyLinkedList();
- myLinkedList.add(7);
- myLinkedList.add(7);
- myLinkedList.add(3);
- myLinkedList.add(7);
- myLinkedList.add(7);
- myLinkedList.add(5);
- myLinkedList.add(3,4);
- myLinkedList.add(7);
- System.out.println(myLinkedList.toString());
- myLinkedList.removeAllValue(7);
- System.out.println(myLinkedList.toString());
- }
三、带头单链表dummyHead
在单链表的操作过程中,每次插入和删除都需要考虑头结点的情况,因为只有头结点特殊,没有前驱,所以需要想一个办法,让链表中所有的有效结点都一视同仁,就不需要区额外关注头结点的情况。
因此引入一种新的结构,虚拟头结点dummyHead,不存储具体的元素,只是作为链表的头来使用,所有存储元素的结点都是该头结点的后继结点。
3.1结构定义
与单链表不同的地方就是要在构造方法中先定义虚拟头结点。后来每一个结点都在虚拟头结点的后面。
- public class LinkedListWithHead {
- private int size;
- private Node dummyHead;
- private static class Node {
- private int val;
- private Node next;
- public Node(int val) {
- this.val = val;
- }
- }
- public LinkedListWithHead(){
- this.dummyHead = new Node(-1);
- }
- }
3.2中间位置的插入
首先排除不合法情况后不需要将第一个有效结点单独拿出来调用头插法,因为它也有前驱结点,所以可以一视同仁。
- public void add(int index, int val){
- if (index < 0 || index > size) {
- throw new IllegalArgumentException("add index illegal!");
- }
- Node prev = dummyHead;
- for (int i = 0; i < index; i++) {
- prev = prev.next;
- }
- Node node = new Node(val);
- node.next = prev.next;
- prev.next = node;
- size++;
- }
3.3头插法和尾插法
与单链表没有什么差别,就是将单链表的头结点换成了带头链表的虚拟头结点的后继节点。
- public void addFirst(int val){
- Node node = new Node(val);
- node.next = dummyHead.next;
- dummyHead.next = node;
- size++;
- }
但是在带头链表中,第一个有效结点也有前驱节点,所以也可以像尾插法一样运用add方法。
- public void addFirst(int val){
- add(0,val);
- }
- public void addLast(int val) {
- add(size,val);
- }
3.4遍历带头链表
经过链表之后,带头链表的遍历就很好写了,摘除头结点之后遍历即可。
- public String toString() {
- StringBuilder sb = new StringBuilder();
- Node cur = dummyHead.next;
- while (cur != null) {
- sb.append(cur.val);
- sb.append("->");
- if (cur.next == null) {
- sb.append("NULL");
- }
- cur = cur.next;
- }
- return sb.toString();
- }
- public static void main(String[] args) {
- LinkedListWithHead linkedListWithHead = new LinkedListWithHead();
- linkedListWithHead.addLast(3);
- linkedListWithHead.addLast(5);
- linkedListWithHead.addLast(9);
- linkedListWithHead.addFirst(1);
- linkedListWithHead.add(3,7);
- System.out.println(linkedListWithHead.toString());
- }
先尾插3,5,9。然后头插1,在指定位置3插入7。
3.5 在表中删除所有值为val的元素
具体逻辑也是和单链表一样的,只是不需要特别考虑头结点。用cur指向虚拟头结点,来判断cur.next的val是否和需要判断的值相同,如果相同就将cur.next指向 cur.next.next,直到不相同的时候,就让cur向右移。
- public void removeAllValue(int val){
- Node cur = dummyHead;
- while (cur.next!=null){
- Node sec = cur.next.next;
- if(cur.next.val == val){
- cur.next = sec;
- size--;
- }else{
- cur = cur.next;
- }
- }
- }
- public static void main(String[] args) {
- LinkedListWithHead linkedListWithHead = new LinkedListWithHead();
- linkedListWithHead.addLast(1);
- linkedListWithHead.addLast(1);
- linkedListWithHead.addLast(3);
- linkedListWithHead.addLast(1);
- linkedListWithHead.addLast(5);
- linkedListWithHead.addLast(1);
- linkedListWithHead.addFirst(1);
- System.out.println(linkedListWithHead.toString());
- linkedListWithHead.removeAllValue(1);
- System.out.println(linkedListWithHead.toString());
- }
四、用递归写链表的方法
具体构造还是和之前一样。
- public class LinkedListR implements SeqList {
- private int size;
- private Node head;
- private class Node {
- private int val;
- private Node next;
- public Node(int val) {
- this.val = val;
- }
- }
- }
4.1链表指定位置的插入
首先就是不合法情况的排除,然后先判断如果index==0就是头插法,如果index!=0,那么直接让head.next = add(head.next,index - 1,val),调用递归一直到找到index位置然后插入即可。
然后头插和尾插都调用这个方法输入对应的位置即可。
- public void addFirst(int val) {
- add(0,val);
- }
-
- @Override
- public void add(int val) {
- add(size,val);
- }
-
- @Override
- public void add(int index, int val) {
- if (index < 0 || index > size) {
- throw new IllegalArgumentException("add index illegal!");
- }
- head = add(head,index,val);
- }
- private Node add(Node head, int index, int val) {
- if (index == 0) {
- Node node = new Node(val);
- node.next = head;
- head = node;
- size ++;
- return head;
- }
- head.next = add(head.next,index - 1,val);
- return head;
- }
4.2链表的正向输出
链表的正向输出用递归写也很简单,首先head==null时,输出NULL然后返回即可,然后先输出当前的值在调用递归,一个接一个输出。
- private void print(Node head) {
- if (head == null) {
- System.out.print("NULL\n");
- return;
- }
- System.out.print(head.val + "->");
- print(head.next);
- }
- public void print() {
- print(head);
- }
4.3链表的反向输出
用递归的方法反向输出会更加的容易,能倒叙从尾结点开始输出内容,一直到头结点。
首先当head==null时,直接返回就好,然后需要把先把子链表全部输出完,站在head位置,只要一直输出就好。但是这个时候最后输出没有办法加上null,正常正向输出最后会有null,所以创建一个变量count计数,在每次调用递归前count++。然后每次调用完递归返回后再count--,当count==0时,已经走到了链表的头结点,在最后要继续输出null。
- public void printReverse() {
- printReverse(head);
- }
- int count = 0;
- private void printReverse(Node head) {
- if (head == null) {
- return;
- }
- count ++;
- printReverse(head.next);
- count --;
- System.out.print(head.val + "->");
- if (count == 0) {
- System.out.print("NULL\n");
- }
- }
- public static void main(String[] args) {
- LinkedListR list = new LinkedListR();
- list.addFirst(1);
- list.addFirst(3);
- list.add(5);
- list.add(7);
- list.add(1,10);
- list.print();
- list.printReverse();
- }
五、双向链表
工程中常用的链表就是双向链表。
每个节点即保存下一个结点的地址,也保存第一个结点的地址,因此我们在双向链表的任意一个节点,就可以即向后遍历,又可以向前遍历。
5.1结构定义
双向链表的Node重要定义三个变量,prev是前驱,next是后继,val是存储的值。
主要的变量也要定义三个,size长度,head头结点,tail尾结点。
- public class DoubleLinkedList implements SeqList {
- private int size;
- private Node head;
- private Node tail;
- private static class Node {
- private Node prev;
- private int val;
- private Node next;
- public Node(int val) {
- this.val = val;
- }
- }
- }
-
5.2头插法和尾插法
双向链表的头插和尾插就很简单,只要排除了链表为空的情况即可,若链表为空那么直接让head = tail = node。有了前驱和后继两个指向,剩下就方便解决了,尾插就直接让原先的尾结点的后继指向node,然后让node的前驱指向tail,再将tail改成node即可,头插同理。
- public void add(int val) {
- Node node = new Node(val);
- size ++;
- if(tail == null){
- head = tail = node;
- return;
- }
- tail.next = node;
- node.prev = tail;
- tail = node;
- }
- public void addFirst(int val){
- Node node = new Node(val);
- size ++;
- if(head == null){
- head = tail = node;
- return;
- }
- head.prev = node;
- node.next = head;
- head = node;
- }
5.3在指定位置插入结点
首先排除不合法情况,然后排除头插和尾插法。
然后创建一个node方法来找到index位置的前驱结点,为了让node方法进入循环的次数少一些要判断一下index-1和size一半的大小关系,如果小就从头遍历,如果大就从尾部遍历。然后用prev存储前驱结点,用sec存储原链表的index位置的结点,将node插入即可。
- public void add(int index, int val) {
- if (index < 0 || index > size) {
- throw new IllegalArgumentException("add index illegal!");
- }
- if(index == 0){
- addFirst(val);
- return;
- }
- if(index == size){
- add(val);
- return;
- }
- Node prev = node(index - 1);
- Node sec = prev.next;
- Node node = new Node(val);
- size ++;
- prev.next = node;
- node.prev = prev;
- node.next = sec;
- sec.prev = node;
- }
- private Node node(int index){
- Node result = null;
- if(index<(size>>1)){
- result = head;
- for(int i = 0;i
- result = result.next;
- }
- }else{
- result = tail;
- for(int i = size-1;i > index;i--){
- result = result.prev;
- }
- }
- return result;
- }
- public static void main(String[] args) {
- DoubleLinkedList list = new DoubleLinkedList();
- list.add(3);
- list.add(7);
- list.add(9);
- list.addFirst(1);
- list.add(2,5);
- System.out.println(list.toString());
- }
5.4包含、查找指定索引值、修改指定索引值。
包含就很容易了只要遍历数组查找有没有结点的值和指定值相同。
get也很简单只要调用node方法,找到index位置的前驱元素,返回index位置的值即可。
set直接找到index位置的元素,然后将这个位置元素换成新的元素。
- public boolean contains(int val) {
- Node node = head;
- for(int i = 0;i
- if(node.val==val){
- return true;
- }
- node = node.next;
- }
- return false;
- }
- @Override
- public int get(int index) {
- Node node = node(index-1);
- return node.next.val;
- }
- @Override
- public int set(int index, int newVal) {
- Node node = node(index);
- int x = node.val;
- Node sec = new Node(newVal);
- sec.prev = node.prev;
- sec.next = node.next;
- node.prev.next = sec;
- node.next.prev = sec;
- return x;
- }
- public static void main(String[] args) {
- DoubleLinkedList list = new DoubleLinkedList();
- list.add(3);
- list.add(7);
- list.add(9);
- list.addFirst(1);
- list.add(2,5);
- System.out.println(list.toString());
- System.out.println(list.contains(1));
- System.out.println(list.get(2));
- System.out.println(list.set(3,1));
- System.out.println(list.toString());
- }
5.5删除操作
删除指定位置结点有node方法就很好做了,找到该节点然后通过node.next.prev = node.prev; node.prev.next = node.next;删除即可。
删除第一次出现val值首先要排除头结点和尾结点,先对这两个点进行处理,然后处理中间结点,找到和val值一样的结点的前驱结点,然后进行删除操作。
删除全部val值也要排除头结点和尾结点,先对这两个点进行处理,并且要使用循环操作,以防补上来的结点也满足条件,然后处理中间结点,找到和val值一样的结点的前驱结点,然后进行删除操作,如果不是就对下一个结点进行操作。
- public int removeByIndex(int index) {
- Node node = node(index);
- int x = node.val;
- node.next.prev = node.prev;
- node.prev.next = node.next;
- size--;
- return x;
- }
- @Override
- public void removeByValueOnce(int val) {
- if(head.val == val){
- head.next.prev = null;
- head = head.next;
- size--;
- return;
- }
- if(tail.val == val){
- tail.prev.next = null;
- tail = tail.prev;
- size--;
- return;
- }
- Node node = head;
- for(int i = 0;i
- if(node.val==val){
- node.next.prev = node.prev;
- node.prev.next = node.next;
- size--;
- }
- node = node.next;
- }
- }
- @Override
- public void removeAllValue(int val) {
- while (head.val == val){
- head.next.prev = null;
- head = head.next;
- size--;
- }
- while (tail.val == val){
- tail.prev.next = null;
- tail = tail.prev;
- size--;
- }
- Node node = head;
- while (node.next!=null){
- if(node.next.val==val){
- node.next= node.next.next;
- node.next.prev = node;
- size--;
- }else {
- node = node.next;
- }
- }
- }
- public static void main(String[] args) {
- DoubleLinkedList list = new DoubleLinkedList();
- list.add(3);
- list.add(7);
- list.add(9);
- list.add(3);
- list.add(3);
- list.addFirst(1);
- list.addFirst(3);
- list.addFirst(3);
- list.add(2,5);
- list.add(4,3);
- System.out.println(list.toString());
- System.out.println(list.removeByIndex(3));
- System.out.println(list.toString());
- list.removeByValueOnce(3);
- System.out.println(list.toString());
- list.removeAllValue(3);
- System.out.println(list.toString());
- }
六、ArrayList和LinkedList的区别
LinkedList还是队列Queue的子类,也是双端队列Deque的子类。
LinkedList不仅有正向遍历的迭代器,还有一个反向遍历的迭代器,从链表的尾部开始遍历。
不同点 ArrayList LinkedList 存储空间上 物理上一定连续 逻辑上连续,但物理上不一定连续 随机访问 支持O(1) 不支持:O(N)
头插 需要搬移元素,效率低O(N) 只需修改引用的指向,O(1) 插入
空间不够时需要扩容 没有扩容的概念 应用场景 元素高效存储+频繁访问 任意位置插入和删除频繁
-
相关阅读:
【类和对象之构造方法】
Bus 消息总线
【分布式】: 幂等性和实现方式
工作记录:vue-grid-layout 修改 margin 导致 item 高度剧烈变化
Google codelab WebGPU入门教程源码<3> - 绘制网格(源码)
1.django部署(案例)
python+Vue在线投稿网站系统pycharm源码
Stream流中的 max()和 sorted()方法
猿创征文|设计模式之享元模式的理解
个人项目中使用Flume的拦截器以及ChannelSelector和SinkProcessor的介绍
-
原文地址:https://blog.csdn.net/m0_50987960/article/details/127867041