数据结构与算法-3基础数据结构-链表

数据结构与算法之链表

课程时间为两个半月,共二十几节课,具有连贯性,希望兄弟们尽量不要缺席。

1 回顾上节课作业

二维数组的内存地址是怎么样的?写出寻址公式?

  • 一维数组内存地址计算公式:loc = init_loc + index * size
  • 二维数组可转化为一维数组,如元素 4 在二维数组中位置是(1,0),转化为一维后是第 3 个元素。其在一维中的下标计算方式为:i * n + j(其中i为行下标,n为一维长度,j为列下标)。

2 链表 面试经典

  1. 如何设计一个LRU缓存淘汰算法?
    • 说明:最近使用淘汰算法,只需要维护一个有序的单链表就可以了。有序的指的就是加入的时间排序
  2. 约瑟夫问题
    • 约瑟夫问题是个有名的问题:N个人围成一圈,从第一个开始报数,第M个将被杀掉,最后剩下一个,其余人都将被杀掉。
    • 比如N=6,M=5 留下的就是1
    • 1 2 3 4 5 6 => 6 1 2 3 4 => 6 1 2 3 =>1 2 3 => 1 3 => 1

3 链表的基本概念

  • 定义:将一组零散的内存块串联在一起,通过指针来访问数据。
  • 特点:不需要连续的内存空间,有指针引用,常见的有单链表、双向链表和循环链表
  • 操作:包括查找、插入和删除等,时间复杂度均为O(n)。

4 单链表

结构:由节点组成,每个节点包含数据和指向下一个节点的指针。

操作:插入操作分为头插法、尾插法和中间插入法,删除操作则是通过修改指针来实现。

链表集合示例代码
/**
 * 单向链表
 * 实现思路:
 * 1. 节点类 MyOneWayNode
 * 2. 链表类 MyOneWayLinkList
 * 3. 新增节点(头插法) addToHead
 * 3. 新增节点(尾插法) addToTail
 * 4. 删除节点(头部) deleteToHead
 * 4. 删除节点(尾部) deleteToTail
 * 5. 遍历节点遍历 print
 * 6. 搜索节点 search
 */
public class MyOneWayLinkList {
    private MyOneWayNode head;

    public MyOneWayLinkList() {
        this.head = null;
    }

    public void print() {
        MyOneWayNode cur = head;
        while (cur != null){
            System.out.print(cur.data + " ");
            cur = cur.next;
        }
        System.out.println();
    }

    public void addToHead(int data) {
        MyOneWayNode node = new MyOneWayNode(data);
        node.next = head;
        head = node;
    }

    public void addToTail(int data) {
        MyOneWayNode node = new MyOneWayNode(data);
        if (head == null){
            head = node;
        }else {
            MyOneWayNode cur = head;
            while (cur.next != null){
                cur = cur.next;
            }
            cur.next = node;
        }
    }

    public MyOneWayNode deleteToHead() {
        if (head != null){
            MyOneWayNode node = head;
            head = head.next;
            return node;
        }
        return null;
    }

    public MyOneWayNode deleteToTail() {
        MyOneWayNode node = null;
        if (head != null){
            MyOneWayNode cur = head;
            while (cur.next != null && cur.next.next != null){
                cur = cur.next;
            }
            if (cur.next != null){
                node = cur.next;
                cur.next = null;
            }else {
                node = cur;
                head = null;
            }
        }
        return node;
    }

    public MyOneWayNode search(int data) {
        MyOneWayNode cur = head;
        while (cur != null){
            if (cur.data == data){
                return cur;
            }
            cur = cur.next;
        }
        return null;
    }

    public static void main(String[] args) {
        MyOneWayLinkList myOneWayLinkList = new MyOneWayLinkList();
        myOneWayLinkList.addToHead(1);
        myOneWayLinkList.addToHead(2);
        myOneWayLinkList.addToHead(3);
        System.out.println("addToHead...end...");
        myOneWayLinkList.print(); // 3 2 1
        myOneWayLinkList.addToTail(4);
        System.out.println("addToTail...end...");
        myOneWayLinkList.print(); // 3 2 1 4
        myOneWayLinkList.deleteToHead();
        System.out.println("deleteToHead...end...");
        myOneWayLinkList.print(); // 2 1 4
        myOneWayLinkList.deleteToTail();
        System.out.println("deleteToTail...end...");
        myOneWayLinkList.print(); // 2 1
        System.out.println(myOneWayLinkList.search(2)); // MyOneWayNode{data=2, next=MyOneWayNode{data=1, next=null}}
    }
}

class MyOneWayNode{
    int data;
    MyOneWayNode next;

    public MyOneWayNode(int data) {
        this.data = data;
    }

    @Override
    public String toString() {
        return "MyOneWayNode{" +
                "data=" + data +
                ", next=" + next +
                '}';
    }
}
数组集合示例代码
/**
 * 数组集合
 * 1.属性
 *      数组对象
 *      集合大小
 * 2.新增
 * 3.删除
 * 4.查找
 * 5.遍历
 */
public class MyArrayList<T> {
    private T[] array;
    private int size;
    private static final int DEFAULT_CAPACITY = 10;

    public MyArrayList() {
        this.array = (T[]) new Object[DEFAULT_CAPACITY];
        this.size = 0;
    }

    public MyArrayList(int capacity) {
        this.array = (T[]) new Object[capacity];
        this.size = 0;
    }

    public void add(T t) {
        if (size == array.length) {
            resize();
        }
        array[size++] = t;
    }

    public void add(int index,T t){
        if (index < 0 || index >= array.length){
            throw new RuntimeException("index out of bound");
        }
        if (size == array.length){
            resize();
        }
        for (int i = size; i >=index; i--) {
            array[i] = array[i - 1];
        }
        array[index] = t;
        size++;
    }

    public void remove(int index){
        if (index < 0 || index >= array.length){
            throw new RuntimeException("index out of bound");
        }
        for (int i = index; i < size; i++) {
            array[i] = array[i + 1];
        }
        size--;
    }

    public T get(int index){
        if (index < 0 || index >= array.length){
            throw new RuntimeException("index out of bound");
        }
        return array[index];
    }

    public void print(){
        for (int i = 0; i < size; i++) {
            System.out.print(array[i] + " ");
        }
    }

    private void resize() {
        T[] newArray = (T[]) new Object[array.length * 2];
        for (int i = 0; i < array.length; i++) {
            newArray[i] = array[i];
        }
        array = newArray;
    }

    public static void main(String[] args) {
        MyArrayList<Integer> myArrayList = new MyArrayList<>();
        myArrayList.add(1);
        myArrayList.add(2);
        myArrayList.add(3);
        myArrayList.add(4);
        myArrayList.add(5);
        myArrayList.add(6);
        myArrayList.add(7);
        myArrayList.add(8);
        myArrayList.add(9);
        myArrayList.add(10);
        System.out.println("initial......");
        myArrayList.print(); // 1 2 3 4 5 6 7 8 9 10
        System.out.println();
        myArrayList.add(3, 100);
        System.out.println("add......");
        myArrayList.print(); // 1 2 3 100 4 5 6 7 8 9 10
        System.out.println();
        myArrayList.remove(3);
        System.out.println("remove......");
        myArrayList.print(); // 1 2 3 4 5 6 7 8 9 10
        System.out.println();
        System.out.println(myArrayList.get(3)); // 4
        System.out.println(myArrayList.get(4)); // 5
    }
}

5 循环链表

结构:尾节点的指针指向头节点,形成一个环形链表。

应用:解决约瑟夫问题,即围成一个圈,从第一个人开始报数,报到指定数字的人退出,直到最后一个人。

循环链表示例代码
package cn.zxc.demo.leetcode_demo.linklist;

/**
 * 环形链表
 * 1.环形链表节点类
 * 2.环形链表类
 * 3.新增节点
 * 4.删除节点
 * 5.打印链表
 */
public class MyRoundLinkList {
    private MyRoundNode head;
    private MyRoundNode tail;

    public MyRoundLinkList() {
        this.head = null;
        this.tail = null;
    }

    public void print() {
        MyRoundNode cur = head;
        if (cur != null) System.out.print(cur.data + " -> ");
        while (cur!= null && cur.next != head){
            cur = cur.next;
            System.out.print(cur.data + " -> ");
        }
        if (cur != null && cur.next != null) System.out.print(cur.next.data + " -> ");
        System.out.println();
    }

    public void addToHead(int data) {
        MyRoundNode node = new MyRoundNode(data);
        node.next = head;
        head = node;
        if (tail == null){
            tail = node;
            tail.next = head;
        }else {
            tail.next = node;
        }
    }

    public void addToTail(int data) {
        MyRoundNode node = new MyRoundNode(data);
        if (head == null){
            head = node;
            tail = node;
            head.next = tail;
            tail.next = head;
        }else {
            tail.next = node;
            tail = node;
            tail.next = head;
        }
    }

    public MyRoundNode deleteToHead() {
        MyRoundNode node = null;
        if (head != null){
            node = head;
            if (head == tail){
                tail = null;
                head = null;
            }else{
                head = head.next;
                tail.next = head;
            }
        }
        if (node != null) node.next = null;
        return node;
    }

    public MyRoundNode deleteToTail() {
        MyRoundNode node = null;
        if (head != null){
            node = tail;
            if (head == tail){
                head = null;
                tail = null;
            }else{
                MyRoundNode cur = head;
                while (cur != null && cur.next != tail){
                    cur = cur.next;
                }
                cur.next = head;
                tail.next = null; // 为了方便GC
                tail = cur;
            }
        }
        return node;
    }

    public MyRoundNode deleteToN(int num) {
        MyRoundNode node = null;
        if (head != null){
            if (head == tail){ // 只有一个节点
                node = tail;
                head = null;
                tail = null;
            }else{
                MyRoundNode cur = head;
                for (int i = 0; i < num - 2; i++) { // 找到待删除节点的前一个节点
                    cur = cur.next;
                }
                node = cur.next; // 待删除节点
                if (node == tail){ // 待删除节点 == 尾节点 更新尾节点
                    tail = cur;
                    tail.next = head;
                } else if (node == head) { // 待删除节点 == 头节点 更新头节点,并更新尾部节点的下一个节点
                    head = node.next;
                    tail.next = head;
                }else{
                    cur.next = cur.next.next; // 删除节点
                }
                node.next = null; // 为了方便GC
            }
        }
        return node;
    }

    public MyRoundNode search(int data) {
        MyRoundNode cur = head;
        if (head != null && cur.data == data) return cur;
        while (cur != null && cur.next != null && cur.next != head){
            if (cur.next.data == data){
                return cur;
            }
            cur = cur.next;
        }
        return null;
    }

    public static void main(String[] args) {
        MyRoundLinkList myRoundLinkList = new MyRoundLinkList();
        myRoundLinkList.addToHead(1);
        myRoundLinkList.print(); // 1 -> 1
        myRoundLinkList.addToHead(2);
        myRoundLinkList.addToHead(3);
        System.out.println("addToHead...end...");
        myRoundLinkList.print(); // 3 -> 2 -> 1 -> 3
        myRoundLinkList.addToTail(4);
        System.out.println("addToTail...end...");
        myRoundLinkList.print(); // 3 -> 2 -> 1 -> 4 -> 3
        myRoundLinkList.deleteToHead();
        System.out.println("deleteToHead...end...");
        myRoundLinkList.print(); // 2 -> 1 -> 4 -> 2
        myRoundLinkList.deleteToTail();
        System.out.println("deleteToTail...end...");
        myRoundLinkList.print(); // 2 -> 1 -> 2
        System.out.println(myRoundLinkList.search(2));
    }
}
class MyRoundNode{
    int data;
    MyRoundNode next;

    public MyRoundNode(int data) {
        this.data = data;
    }

    public MyRoundNode(int data, MyRoundNode next) {
        this.data = data;
        this.next = next;
    }
}

解决约瑟夫问题
/**
 * 解决约瑟夫问题:N个人围成一圈,从第一个开始报数,第M个将被杀掉,最后剩下一个,其余人都将被杀掉
 * 思路:使用环形链表处理
 * 
 */
public class YsfDemo {
    public static void main(String[] args) {
        int n = 6;
        int m = 5;
        int[] array = initArray(n);
        int num = ysf(array, m);
        System.out.println("最后剩下的人:" + num);
    }

    private static int ysf(int[] array, int m) {
        // 将数组转换为环形链表
        MyRoundLinkList myRoundLinkList = new MyRoundLinkList();
        for (int i = 0; i < array.length; i++) {
            myRoundLinkList.addToTail(array[i]);
        }
        System.out.print("当前链表 : ");
        myRoundLinkList.print();
        MyRoundNode node = null;
        MyRoundNode temp = null;
        // 遍历环形链表
        while ((temp = myRoundLinkList.deleteToN(m)) != null){
            node = temp;
            System.out.println("删除节点:" + node.data);
            System.out.print("当前链表 : ");
            myRoundLinkList.print();
        }
        return node.data;
    }

    private static int[] initArray(int i) {
        int[] array = new int[i];
        for (int j = 0; j < i; j++) {
            array[j] = j + 1;
        }
        return array;
    }
}

当前链表 : 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 1 ->
删除节点:5
当前链表 : 1 -> 2 -> 3 -> 4 -> 6 -> 1 ->
删除节点:6
当前链表 : 1 -> 2 -> 3 -> 4 -> 1 ->
删除节点:1
当前链表 : 2 -> 3 -> 4 -> 2 ->
删除节点:3
当前链表 : 2 -> 4 -> 2 ->
删除节点:2
当前链表 : 4 -> 4 ->
删除节点:4
当前链表 :
最后剩下的人:4

6 双向链表

结构:每个节点包含数据、指向前一个节点的指针和指向后一个节点的指针。

操作:插入操作需要修改两个指针,删除操作则需要修改三个指针。

示例代码
package cn.zxc.demo.leetcode_demo.linklist;

/**
 * 双向链表
 * 1.节点类
 *      数值
 *      上一个系节点
 *      下一个节点
 * 2.链表类
 *      头插法
 *      尾插法
 *      删除节点
 *      搜索节点
 *      打印节点
 */
public class MyDoubleLinkList {
    public MyDoubleLinkNode head;
    public MyDoubleLinkList()
    {
        head = null;
    }

    public void addToHead(int data)
    {
        MyDoubleLinkNode node = new MyDoubleLinkNode(data);
        if (head != null){
            head.pre = node;
            node.next = head;
        }
        head = node;
    }

    public void addToTail(int data)
    {
        MyDoubleLinkNode node = new MyDoubleLinkNode(data);
        if (head == null){
            head = node;
            return;
        }
        MyDoubleLinkNode cur = head;
        while (cur.next != null){
            cur = cur.next;
        }
        cur.next = node;
        node.pre = cur;
    }

    public void delete(int data)
    {
        MyDoubleLinkNode node = search(data);
        if (node != null){
            if (node.pre != null){ // ① 待删除节点的pre存在:将pre节点的next指向待删除节点的next
                node.pre.next = node.next;
                if (node.next != null) node.next.pre = node.pre;
            }else{ // ② 待删除节点的pre不存在:将head指向待删除节点的next
                head = node.next;
                head.pre = null;
            }
            node.next = null;
        }
    }

    public MyDoubleLinkNode search(int data)
    {
        MyDoubleLinkNode cur = head;
        while (cur != null){
            if (cur.data == data){
                return cur;
            }
            cur = cur.next;
        }
        return null;
    }

    public void print()
    {
        MyDoubleLinkNode cur = head;
        MyDoubleLinkNode tail = null;
        System.out.print("链表(正序):");
        while (cur != null){
            System.out.print(cur.data + " -> ");
            tail = cur;
            cur = cur.next;
        }
        System.out.println();
        System.out.print("链表(倒序):");
        while (tail != null){
            System.out.print(tail.data + " -> ");
            tail = tail.pre;
        }
        System.out.println();
    }

    public static void main(String[] args)
    {
        MyDoubleLinkList list = new MyDoubleLinkList();
        list.addToHead(1);
        list.addToHead(2);
        list.addToHead(3);
        list.addToTail(4);
        System.out.println("初始化链表:");
        list.print();
        list.delete(3);
        System.out.println("删除3:");
        list.print();
        list.delete(4);
        System.out.println("删除4:");
        list.print();
    }
}
class MyDoubleLinkNode{
    int data;
    MyDoubleLinkNode pre;
    MyDoubleLinkNode next;
    public MyDoubleLinkNode(int data)
    {
        this.data = data;
    }
}

初始化链表:
链表(正序):3 -> 2 -> 1 -> 4 ->
链表(倒序):4 -> 1 -> 2 -> 3 ->
删除3:
链表(正序):2 -> 1 -> 4 ->
链表(倒序):4 -> 1 -> 2 ->
删除4:
链表(正序):2 -> 1 ->
链表(倒序):1 -> 2 ->

7 数组VS链表

存储方式

  • 数组:数组在内存中采用连续存储的方式,即数组中的元素在内存中是连续排列的。这种存储方式使得数组在访问元素时非常高效,因为可以通过索引直接计算元素的内存地址。
  • 链表:链表在内存中并不是连续存储的,而是通过指针(或引用)将各个节点连接在一起。每个节点包含两部分信息:一部分是数据域,用于存储数据;另一部分是指针域(或引用域),用于指向下一个节点。由于链表中的节点是分散存储的,因此访问链表中的元素需要通过指针进行遍历。

访问效率

  • 数组:由于数组在内存中是连续存储的,因此可以通过索引直接访问数组中的元素。这种访问方式非常高效,时间复杂度为O(1)。此外,数组还可以借助CPU的缓存机制,预读数组中的数据,进一步提高访问效率。
  • 链表:由于链表中的节点是分散存储的,因此访问链表中的元素需要通过指针进行遍历。这种访问方式相对较慢,时间复杂度为O(n)。此外,链表对CPU缓存不友好,因为每次访问下一个节点时都需要从内存中读取新的数据,无法有效利用缓存机制。

大小限制与动态扩容

  • 数组:数组的大小在声明时是固定的,一经声明就要占用整块连续内存空间。如果声明的数组过大,系统可能没有足够的连续内存空间分配给它,导致“内存不足(out of memory)”。如果声明的数组过小,则可能出现不够用的情况。在数组需要扩容时,通常需要再申请一个更大的内存空间,把原数组拷贝进去,这个过程非常费时。
  • 链表:链表本身没有大小的限制,天然地支持动态扩容。当需要添加新的节点时,只需要分配一个新的节点并将其插入到链表中即可。因此,链表在动态扩容方面比数组更加灵活和高效。

8 总结

  1. 链表是一种重要的数据结构,它可以动态地增加和删除节点,不需要预先分配连续的内存空间。
  2. 单链表是最简单的链表结构,它只有一个指针,只能单向遍历。
  3. 循环链表是一种特殊的单链表,它的尾节点指向头节点,形成一个环形结构。
  4. 双向链表是一种更复杂的链表结构,它有两个指针,可以双向遍历。
  5. 链表的应用非常广泛,例如LRU缓存淘汰算法和约瑟夫问题等。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值