【Review】链表基础、链表arithmetic常用思路

本文深入探讨了数据结构中的链表,包括单链表、双链表和循环链表的概念及操作。文章通过Java集合类LinkedList详细阐述了链表的实现,并对比了ArrayList。此外,还介绍了链表操作如插入、删除的原理,并讨论了线程安全问题及解决方案。文章最后通过LeetCode题目展示了链表操作的实战,如反转链表、删除元素等。
摘要由CSDN通过智能技术生成

Review


数据结构【树和二叉、图】 — java集合类巩固, 链表专题arithmetic — 分几篇专题


中间插了一节Linux,Linux还是很重要的,当然博主后期还会继续补充计算机网络、操作系统相关的知识,同时最主要的是SQL,包括Mysql的复习和noSQL的代表Redis

个人经过几天的试水,发现项目还是写后端吧,前端的CSS真的emmm… 反正短时间内本人的项目都只负责后端模块;前端只是能看懂,让封装一个简单的组件还行,复杂的就算了…

另外arithmetic感觉链表的题真的没啥难度,主要就是思想要到位,还没有KMP、滑动的一半难

data structure

前面的review中已经就分享过线性表了,但是一笔带过了,这里结合之后的链表专题来总结一下,同时会结合java的类库来进行实现

链表

首先还是简单介绍一下书本上定义的链表结果,书本当然是基于C++实现的,当时也用C++比较顺手

  • 单链表: 链表是一种通过指针串联再一个的线性结构,每一个数据结点都是两部分组成,一种数据域一个指针域(存放指向下一个结点的指针),最后一个结点的指针域指向null,空指针

  • 双链表: 每一个节点都有两个指针域、一个指向下一个节点,一个指向上一个结点。 双链表既可以向前查询也可以向后查询 【 但是现在arithmetic中一般挺少】

  • 循环链表: 循环链表可以解决约瑟夫环问题 【高频问题】,还有就是普通的单链表的判环

存储方式

链表既然是链式的,所以内存地址不是连续分布的,而是散乱分布在内存中的地址中,散乱分布在内存中的地址上,分配机制取决于操作系统的内存管理【 地址分散,通过指针串联在一起】

链表操作

​ 删除结点: 只要将结点的next指向下一个结点的next就可以了,如果是在C++中,是没有JVM的GC垃圾回收的,所以需要free进行手动的释放内存,所以最好用一个指针指向这块地址,不然找不到了【java中可以内存回收,不用手动释放】

​ 添加结点: 链表适合做增加和删除,复杂度都是O(1),比顺序表好多了; 但是注意如果是按位置进行添加,那么就需要进行遍历,new 一个结点然后将其插入到其中即可【 创建一个链表的方法头插法和尾插法,C++中经常用,这里就不再赘述】, 尾插法是逆序过来的 — 所以反转链表的另外一个方法就是创建一个dummyHead进行头插即可

java中的LinkedList

java的集合后面会单独出一篇博客来安利一下,因为真的很常用,特别是用java来刷题的朋友们,选一个集合很重要,并且一般都是没有提示的,所以需要记住常用的方法(比如Set的add,contains)、Map的containsKey,get,put、getOrDefault、List的add、contains,但是注意其remove方法的参数是index,所以不能使用List来做频度序列的数据结构-----(建议使用频度数组)— 具体的技巧可以上加也可以下埋

  • java的ArrayList和LinkedList的区别?

​ 很easy,首先ArrayList和LinkedList都实现了List接口,都是线性的数据结构,但是ArrayList底层是基于数组的,所以适合进行随机存取,增删的效率低; LinkedList是基于双向链表实现的,更适合做增删的操作; 需要注意的是LinkedList同时实现了Deque ,可以当作双向队列使用,这是ArrayList不具备的

  • LinkedList和ArrayList线程不安全,如何?

​ LinkedList和ArrayList都是线程不安全的,因为多个线程竞争的时候,可能导致数据的添加的不准确,还有就是扩容不正确【CPU是随机分配资源给线程,所以可能线程执行到一般的时候失去了控制权 ---- 这个后面操作系统会讲一下】

提供三种解决办法: concurrent 同时发生地,并存的

  • 使用集合工具类Collections【Colloection是接口 – 集合总】,该类的类方法synchronizedList包裹一下就可以是线程安全的了,包装之后就加上了同步锁就安全了
  • 使用安全的集合类来进行代替,比如CopyOnWriteArrayList【CopyOnWriteArrayList是线程安全的,实例方法中加上了对象锁🔒,两种线程 – write线程和read线程;write线程在操作的时候都会将原来的数组给复制一份副本,写线程操作的都是副本,操作完成之后将复制给原数组,所以不影响读操作, 只是write线程会竞争 ----- Copy为主,但是问题就是Copy的不是最新的数据,并且每次复制会导致空间的占用效率低】ConcurrentLinkedQueue也是线程安全的
  • 使用Vector 【方法加上了synchronized关键字】
public class Vector<E>
    extends AbstractList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{  //实现了List接口,也是一个列表,同时实现了序列化用于网络传输,序列化还可以通过另外方法ObjectInputStream的readObject和对应的writeObject也可以   在Dubbo中,provider的实体bean定义在中间接口工程中,因为要进行IO网络传输,所以必须序列化,实现Serializable
    
     public synchronized void removeElementAt(int index) {

CopyOnWirte有lock锁🔒

并且同时要注意Arrays工具类,这个类还是使用挺频繁的,其排序还是挺快的,sort底层就是简单排序和快排,后面也会解释排序,字符串String的比较是一个字符一个字符比较,也就是字典序排序

class Solution {
    public List<Integer> lexicalOrder(int n) {
        //首先应该使用java原本的api
        String[] test = new String[n];
        for(int i = 0 ; i < n ; i ++) {
            test[i] = String.valueOf(i + 1);
        }
        Arrays.sort(test);
        List<Integer> res = new ArrayList();
        for(int i = 0; i < n ; i ++) {
            res.add(Integer.parseInt(test[i]));
        }
        return res;
    }
}


----优化的算法就是手写DFS,非递归的-----字典序就是取模即可,画一棵树即可----
   
class Solution {
    public List<Integer> lexicalOrder(int n) {
        List<Integer> res = new ArrayList();
        int num = 1; //开始存放
        for(int i = 0; i < n ; i ++) {
            //一共执行n次,每次放入一个,放入的是number
            res.add(num);
            if(num * 10 <= n) {
                //说明num是下一个字典序的
                num *= 10;
            }else{
                while(num % 10 == 9 || num + 1 > n) { //当找到9或者超出的时候,说明该字典序已经找完,返回上一个中 【DFS】
                    num /= 10;
                }
                 num ++;
            }
        }
        return res;
    }
}

arithmetic

java链表设计

这个还是需要练习一下的,因为之前都是使用的C++来手写的链表,突然换到java还有些蒙圈,但是好了,Java是OOP,所以用OOP的思想,结点就是一个类,其结点的属性都是指针域和数据域,链表也是一个类,链表类中的属性包括头结点和数量size

class ListNode{
    int val;
    ListNode next;
    public ListNode(){}
    public ListNode(int val){this.val = val;}
}

public MyLinkedList {
    ListNode head;  //头节点自定义,所以这里为了方便还是使用dummyHead的原则来简化,只是注意最后返回的是dummyHead.next
    int size;  
    
    public MyLinkedList(){}
    
    public MyLinkedList(ListNode head, int size) {
        head = new ListNode(0); //虚拟的头节点,是否存储看个人想法
        size = 0; //初始化
    }
    
    //获取链表的第index结点的值;注意看题目,0---index
    public int get(int index) {
        if(index < 0 || index >= size) {
            return -1;
        }
        ListNode temp = head; //虚拟的头节点
       for(int i = 0; i <= index; i ++) { //考虑第0号结点决定=号
           p = p.next;
       }
        return p.val; //排除了dummyHead
    }
    
    //在index位置插入
    public void addAtIndex(int index,int val) {
        if(index < 0) index = 0;
        if(index > size) return;
        List temp = head;
        size ++;
        for(int i = 0; i < index; i ++) {
           temp = temp.next;
        }
         ListNode s = new ListNode(val);
        s.next = p.next;
        p.next = s;
    }
    
    //在index位置删除
    public void deleteAtIndex(int index) {
        if(index < 0 || index >= size) return;
        ListNode temp = head;
        for(int i = 0; i < index; i++) {
            temp = temp.next; //< 找到前一个结点
        }
        temp.next = temp.next.next;
        size --; //注意变化size
    }
    
    //在头部插入
    public void AddAtHead(int val) {
        this.addAtIndex(0,val);
    }
    
    public void AddAtTail(int val) {
        this.addAtIndex(size, val);
	}
}

上面说过,只要链表上手了之后,所有的链表问题都是非常easy的; 这里介绍几个重要的思想方法

  • 前排特列【空,1个】, 防止后面报错空指针

  • 虚拟头节点 【 建立在题目中的链表没有头节点的概念-- 也就是第一个结点也是存储的数据】dummyHead

  • 三小兵方法 【 同时定义三个结点连续操作prev,cur、next — 注意循环条件的位置,出现.都要判断空指针异常 – 所以需要决定定义几个变量】 — 还有两小兵p,q 作用都是为了记录原来结点的位置防止丢失

203. 移除链表元素 - 力扣(LeetCode) (leetcode-cn.com)

熟悉之后这就是一个非常简单的题目,其实注意的就是上面的设计的思路,想要的是目标结点还是目标结点的前一个结点, 如果是目标就是=,不是就是< ,这里类似,使用虚拟头节点方法

判断的条件就是p.next != null 这样找到的就是前面的结点,并且会包含到第一个结点

class Solution {
    public ListNode removeElements(ListNode head, int val) {
      //虚拟头节点方便
      ListNode dummyHead = new ListNode();
      dummyHead.next = head;
      ListNode p = dummyHead;
      while(p.next != null) {
          if(p.next.val == val) {
              p.next = p.next.next;
          }else{
              p = p.next;
          }
      }
      return dummyHead.next; //虚拟头节点返回的新链表应该是next开始
    }
}

一定要注意dummyHead最后返回的是dummyHead.next,dummy只是虚拟的,这里只是注意一下,逻辑是找到删除,其余情况继续,所以是if —else; 如果不是return,那么就必须分别在块中,return的可以不写else块

206. 反转链表 - 力扣(LeetCode) (leetcode-cn.com)

第一种思路是头插法,这里使用两小兵,使用虚拟结点轻松ak

class Solution {
    public ListNode reverseList(ListNode head) {
        //第一种思路,头插,使用头节点
        ListNode dummyHead = new ListNode(0);
        ListNode p = head;
        //再来一个小兵防止丢失,q记录下一个结点的位置p.next
        //这里需要的是当前结点,所以条件因该是p != null ,不是.next
        while(p != null) {
            ListNode q = p.next;
            //插入
            p.next = dummyHead.next;
            dummyHead.next = p;
            //继续移动,q记录的位置
            p = q;
        }
        return dummyHead.next;
    }
}

返回dummyHead.next

还有一种思路就是就地反转呢; 这里因为就地反转同时操作的是两个结点,所以需要三小兵prev,cur,next; 想这种next = cur.next----- 本来就是循环 就放在循环中就可以了,以cur为主呢

class Solution {
    public ListNode reverseList(ListNode head) {
       //还有一种思路,就是就地反转,就是前面来一个虚拟的结点dummyHead = null,因为原本的尾结点也是null
       ListNode dummyHead = null; //呼应呢
       //这里两个小兵不够,因为同时操作的两个结点,来三个
       ListNode prev = dummyHead;
       ListNode cur = head;
       while(cur != null) {
            ListNode next = cur.next;
            //操作
           cur.next = prev;
           //移动
           prev = cur;
           cur = next; //所以最后prev在最后一个结点
       }
       return prev;
    }
}

只是需要最后的头节点的位置是prev,prev移动了最后

24. 两两交换链表中的节点 - 力扣(LeetCode) (leetcode-cn.com)

还是虚拟结点和三小兵, 这里的三小兵中的next和cur都是prev的小弟,所以都放在循环里面

class Solution {
    public ListNode swapPairs(ListNode head) {
        //上来就是头节点和三小兵,因为交换涉及两个结点,记录位置还需要一个就是3个
        ListNode dummyHead = new ListNode();
        dummyHead.next = head;
        ListNode prev = dummyHead;
        while(prev.next != null && prev.next.next != null) { //懒判断,前面错了,后面就不判断了
            ListNode cur = prev.next;
            ListNode next = cur.next;

            prev.next = next;
            cur.next = next.next;
            next.next = cur;

            //移动到下一个位置
            prev =  cur;  //cur的位置   这里的三小兵也有点假,实际上只是prev在起作用
        }
        return dummyHead.next;
    }
}

返回的是dummyHead.next🎉

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值