【数据结构和算法5】链表的相关定义和操作(单项链表)

目录

1、基本概念和访问性能

2、单项链表的基本操作(没有哨兵)

2.1、单向链表的定义

2.2、头部添加数据

2.3、单链表遍历

2.4、获取链表长度和获取链表所有数据

2.5、尾部添加数据

2.6、根据索引(index)获取相应的节点数据

2.7、向单链表固定的索引位置插入数据(重点☆☆☆☆☆)

2.8、指定位置删除节点(重点☆☆☆☆☆)

3、有哨兵节点的单项链表


1、基本概念和访问性能

链表和数组相似,都是线性存储的。链表中每一个元素都是指向下一个元素的,在内存中,地址空间是不连续的。

链表有单向链表、双向链表和循环链表三种

单向链表:每一个元素只知道下一个元素是谁,不知道上一个元素

双向链表:每一个节点元素都知道自己的上一个元素和下一个元素

循环链表:通常链表的尾结点的 next 指针都是 null,但是对于循环链表,尾结点的 next 指针指向头结点

随机访问性能:O(n)

插入或者删除性能:找到插入点O(n) + 插入操作O(1) = O(n)

2、单项链表的基本操作(没有哨兵)

2.1、单向链表的定义

根据单向链表的相关定义,在单项链表由每一个独立的节点数据组成,可以定义单项链表的类如下,定义一个存储当前节点数据的 Node 节点和链表的头结点。

没有哨兵表示链表的头结点是真实有有效的数据。

/**
 * 单向链表
 *
 * @author zjj_admin
 */
public class SinglyLinkedList {
​
    /**
     * 链表头指针,这里头结点是真实有效的
     */
    private Node head = null;
​
    /**
     * 链表中的节点,链表和节点是组合的关系,所以做成内部内比较合适。
     * <p>
     * static 关键字为什么要加?什么时候应该使用 static ?
     */
    private static class Node {
        /**
         * 节点数据
         */
        int value;
​
        /**
         * 当前节点的下一个指针
         */
        Node next;
​
        public Node(int value, Node next) {
            this.value = value;
            this.next = next;
        }
    }
​
​
}
​

思考:

1:为什么在定义 Node 要是用内部类,而不是用外部类?

因为我们操作的对象时链表不是节点,所以说一般不要让使用者知道我们链表的内部实现效果。

2:为什么要是用 static 修饰成静态内部类?这个后面解答,答案在单链表遍历部分里面

2.2、头部添加数据

添加数据思路:

a:当空链表时,直接将头结点指向插入数据即可

b:当不是空链表时

  1. 使用新添加数据创建一个新的节点

  2. 新节点的 next 指针指向 head

  3. 让 head 节点指向新节点即可

/**
 * 单向链表
 *
 * @author zjj_admin
 */
public class SinglyLinkedList {
      /**
     * 在链表的头部插入一个数据
     *
     * @param value 待插入数据
     */
    public void addFirstInitial(int value) {
        //当head == null 时【链表为空】,可以被优化
        if (head == null) {
            head = new Node(value, null);
        } else {
            //当链表不为空时,让当前节点的 next 指针指向 head 节点即可,然后再将 head 指向当前节点,具体实现代码如下
            Node curr = new Node(value, null);
            curr.next = head;
            head = curr;
        }
    }
}
​

可以将上面的添加代码进行简化

  /**
     * 在链表的头部插入一个数据
     *
     * @param value 待插入数据
     */
    public void addFirst(int value) {
        //可以将上面的代码进行简化
        head = new Node(value, head);
    }

2.3、单链表遍历

使用 while 循环进行遍历,使用 Consumer 函数式接口

   
 /**
     * 遍历链表,使用函数式接口 Consumer
     */
    public void loopWhile(Consumer<Integer> consumer) {
        Node p = head;
        //当前节点不为 null 就一直循环
        while (p != null) {
            // accept 里面传入相关的参数
            consumer.accept(p.value);
            p = p.next;
        }
    }

测试代码

      
  SinglyLinkedList linkedList = new SinglyLinkedList();
​
        linkedList.addFirst(1);
        linkedList.addFirst(2);
        linkedList.addFirst(3);
        linkedList.addFirst(4);
        //遍历数据
        linkedList.loopWhile((value) -> {
            System.out.println(value);
        });
​

使用 for 循环进行遍历

    /**
     * 遍历链表,使用函数式接口 Consumer
     */
    public void loopFor(Consumer<Integer> consumer) {
        //使用基于 for 循环遍历
        for (Node p = head; p != null; p = p.next) {
            consumer.accept(p.value);
        }
    }

测试方式和 while 循环遍历相同

使用迭代器 Iterable 遍历

实现代码,需要实现 Iterable 接口并且重写 iterator 方法

/**
 * 单向链表
 *
 * @author zjj_admin
 */
public class SinglyLinkedList implements Iterable<Integer> {
    
    private Node head = null;
​
    /**
     * 基于迭代器进行循环
     *
     * @return
     */
    @Override
    public Iterator<Integer> iterator() {
​
        /**
         * 匿名内部类
         */
        return new Iterator<Integer>() {
            Node p = head;
            /**
             * 是否存在下一个元素
             * @return
             */
            @Override
            public boolean hasNext() {
                return p != null;
            }
​
            /**
             * 返回当前数据,并指向下一个元素
             * @return
             */
            @Override
            public Integer next() {
                int v = p.value;
                p = p.next;
                return v;
            }
        };
    }
​
}

这里回答一下为什么 Node 节点要声明成静态内部类。

观察上面的 iterator() 方法,实际上就是一个匿名内部类,转化成内部类的方式代码如下:

   
 /**
     * 基于迭代器进行循环
     *
     * @return
     */
    @Override
    public Iterator<Integer> iterator() {
​
        /**
         * 匿名内部类
         */
        return new NodeIterator();
    }
​
    /**
     * 匿名内部类的完整写法
     * 这里不能添加 static ,因为外 NodeIterator 里面引用了外部类的 head 成员变量
     */
    private class NodeIterator implements Iterator<Integer> {
        Node p = head;
​
        /**
         * 是否存在下一个元素
         * @return
         */
        @Override
        public boolean hasNext() {
            return p != null;
        }
​
        /**
         * 返回当前数据,并指向下一个元素
         * @return
         */
        @Override
        public Integer next() {
            int v = p.value;
            p = p.next;
            return v;
        }
    }

因为在 NodeIterator 类里面用到了外部类的成员变量 head,所以说 NodeIterator 类一定不能使用 static关键字。

static 关键字为什么要加?什么时候应该使用 static ?

  • 当内部内中没有引用外部类的成员变量时,就推荐加 static。

  • 当内部内中有引用外部类的成员变量时,就一定不能加 static。

2.4、获取链表长度和获取链表所有数据

在获取链表数据时需要将数据存放在一个数组中,所以需要先求出链表的长度大小,实现代码如下。

  
  /**
     * 获取链表的长度
     *
     * @return
     */
    public int size() {
        int size = 0;
        if (head == null) {
            return size;
        }
        Node p = head;
        while (p != null) {
            size++;
            p = p.next;
        }
        return size;
    }
​
    /**
     * 获取链表中的所有数据
     *
     * @return 数据数组
     */
    public int[] values() {
        //获取链表长度
        int size = size();
        int[] nums = new int[size];
        //索引从 0 开始
        int i = 0;
        Node p = head;
        while (i < size) {
            nums[i] = p.value;
            p = p.next;
            i++;
        }
        return nums;
    }
​

2.5、尾部添加数据

退步添加数据实现思路

1:当链表为空时,按照头部插入的方式添加即可

2:当链表不为空时,需要插在最后一个节点的后面,所以需要先找到尾部节点。

    /**
     * 添加数据到尾结点
     *
     * @param value
     */
    public void addLast(int value) {
        Node last = findLast();
        if (last == null) {
            //没有数据,添加到头部即可
            addFirst(value);
            return;
        }
        //将当前数据节点存放到 last 的 next 指针
        last.next = new Node(value, null);
    }
​
    /**
     * 找到当前链表的最后一个节点
     *
     * @return
     */
    private Node findLast() {
        //当头结点为空时就直接返回 null
        if (head == null) {
            return null;
        }
        Node p = head;
        //依次遍历,当当前节点 的 next 指针为 null 时,就表示是最后一个节点了
        while (p.next != null) {
            p = p.next;
        }
        return p;
    }

2.6、根据索引(index)获取相应的节点数据
    /**
     * 获取索引为 index 对应节点的数据
     *
     * @param index
     * @return
     */
    public int get(int index) {
        Node node = getNode(index);
        if (null == node) {
            throw new IndexOutOfBoundsException("index:" + index + " 越界了");
        }
        return node.value;
    }
​
​
    /**
     * 获取索引为 index 对应节点
     *
     * @param index
     * @return
     */
    private Node getNode(int index) {
        int i = 0;
        Node p = head;
        while (p != null) {
            //只要当 i 和索引相等时就可以反悔了
            if (i == index) {
                return p;
            }
            p = p.next;
            i++;
        }
        return null;
    }

2.9、带哨兵的单向链表的相关操作

2.7、向单链表固定的索引位置插入数据(重点☆☆☆☆☆)

在固定的位置插入数据的步骤:

1:找到插入位置的上一个节点

2:在上一个节点后面插入新数据即可

实现代码如下:

  
  /**
     * 在链表的指定位置插入数据,详细代码
     *
     * @param index 位置索引
     * @param value 插入数据
     */
    public void addInitial(int index, int value) {
        if (index == 0) {
            //添加在头部
            addFirst(value);
            return;
        }
        //获取上一个节点
        Node prev = getNode(index - 1);
        if (prev == null) {
            throw new IndexOutOfBoundsException("index:" + index + " 越界了");
        }
        //当前需要插入的节点
        Node curr = new Node(value, null);
        //让当前节点的 next 指针指向 prev 的 next
        curr.next = prev.next;
        //再让 上一个节点的 next 指针指向 curr
        prev.next = curr;
    }

简化代码:

   
 /**
     * 在链表的指定位置插入数据,精简代码
     *
     * @param index 位置索引
     * @param value 插入数据
     */
    public void add(int index, int value) {
        if (index == 0) {
            //添加在头部
            addFirst(value);
            return;
        }
        //获取上一个节点
        Node prev = getNode(index - 1);
        if (prev == null) {
            throw new IndexOutOfBoundsException("index:" + index + " 越界了");
        }
        //当前需要插入的节点
        prev.next = new Node(value, prev.next);
    }
2.8、指定位置删除节点(重点☆☆☆☆☆)

  /**
     * 删除第一个节点
     *
     * @return 被删除的索引节点数据
     */
    public int removeFirst() {
        if (head == null) {
            throw new IndexOutOfBoundsException("没有数据了...");
        }
        int v = head.value;
        //让头结点的 next 节点成为新的头节点即可
        head = head.next;
        return v;
    }
​
    /**
     * 根据索引数据
     *
     * @param index 删除索引
     * @return 被删除的索引节点数据
     */
    public int remove(int index) {
        if (index == 0) {
            //深处第一个元素
            return removeFirst();
        }
        //获取被删除节点的上一个节点
        Node prev = getNode(index - 1);
        if (prev == null) {
            throw new IndexOutOfBoundsException("index " + index + " 越界了...");
        }
        Node removed = prev.next;
        if (removed == null) {
            throw new IndexOutOfBoundsException("index " + index + " 越界了...");
        }
        int v = removed.value;
        //只需要将被删除节点的上一个节点的 next 指针指向被删除节点的 next 指针就可以了
        prev.next = removed.next;
        return v;
    }

3、有哨兵节点的单项链表

带哨兵的单项链表可以将链表的 head 节点设置为哨兵节点,哨兵节点一直存在,不存储真实的数据。

查看下列代码,是哨兵模式下的单向链表定义。

和没有哨兵模式相比,带哨兵的模式代码更加简化,很多时候不需要考虑头结点为空的情况了。

/**
 * 单向链表
 * 有哨兵节点,哨兵节点是 head 节点,head 节点后面的数据才是真正的数据
 *
 * @author zjj_admin
 */
public class SinglyLinkedListSentinel implements Iterable<Integer> {
​
    /**
     * 链表头指针,这里头结点是哨兵,没有含义
     */
    private Node head = new Node(Integer.MIN_VALUE, null);
​
    /**
     * 在链表的头部插入一个数据,不需要考虑头结点为 null 的情况了
     *
     * @param value 待插入数据
     */
    public void addFirstInitial(int value) {
​
        //定义当前节点
        Node curr = new Node(value, null);
        //head 节点是哨兵节点
        curr.next = head.next;
        head.next = curr;
    }
​
​
    /**
     * 遍历链表,使用函数式接口 Consumer
     */
    public void loopWhile(Consumer<Integer> consumer) {
        Node p = head.next;
        //当前节点不为 null 就一直循环
        while (p != null) {
            // accept 里面传入相关的参数
            consumer.accept(p.value);
            p = p.next;
        }
    }
​
    /**
     * 遍历链表,使用函数式接口 Consumer
     */
    public void loopFor(Consumer<Integer> consumer) {
        //使用基于 for 循环遍历
        for (Node p = head.next; p != null; p = p.next) {
            consumer.accept(p.value);
        }
    }
​
​
    /**
     * 在链表的头部插入一个数据
     *
     * @param value 待插入数据
     */
    public void addFirst(int value) {
        //可以将上面的代码进行简化
        head.next = new Node(value, head.next);
    }
​
​
    /**
     * 添加数据到尾结点
     *
     * @param value
     */
    public void addLast(int value) {
        Node last = findLast();
        //将当前数据节点存放到 last 的 next 指针
        last.next = new Node(value, null);
    }
​
    /**
     * 找到当前链表的最后一个节点
     *
     * @return
     */
    private Node findLast() {
        Node p = head;
        //依次遍历,当当前节点 的 next 指针为 null 时,就表示是最后一个节点了
        while (p.next != null) {
            p = p.next;
        }
        return p;
    }
​
    /**
     * 获取链表的长度
     *
     * @return
     */
    public int size() {
        int size = 0;
        Node p = head.next;
        while (p != null) {
            size++;
            p = p.next;
        }
        return size;
    }
​
    /**
     * 获取链表中的所有数据
     *
     * @return 数据数组
     */
    public int[] values() {
        //获取链表长度
        int size = size();
        int[] nums = new int[size];
        //索引从 0 开始
        int i = 0;
        Node p = head.next;
        while (i < size) {
            nums[i] = p.value;
            p = p.next;
            i++;
        }
        return nums;
    }
​
​
    /**
     * 获取索引为 index 对应节点的数据
     *
     * @param index
     * @return
     */
    public int get(int index) {
        Node node = getNode(index);
        if (null == node) {
            throw new IndexOutOfBoundsException("index:" + index + " 越界了");
        }
        return node.value;
    }
​
​
    /**
     * 获取索引为 index 对应节点
     * 哨兵节点的索引为 -1,真实节点的索引从 0 开始
     *
     * @param index
     * @return
     */
    private Node getNode(int index) {
        int i = -1;
        Node p = head;
        while (p != null) {
            //只要当 i 和索引相等时就可以反悔了
            if (i == index) {
                return p;
            }
            p = p.next;
            i++;
        }
        return null;
    }
​
    /**
     * 在链表的指定位置插入数据,详细代码
     *
     * @param index 位置索引
     * @param value 插入数据
     */
    public void addInitial(int index, int value) {
        //获取上一个节点
        Node prev = getNode(index - 1);
        if (prev == null) {
            throw new IndexOutOfBoundsException("index:" + index + " 越界了");
        }
        //当前需要插入的节点
        Node curr = new Node(value, null);
        //让当前节点的 next 指针指向 prev 的 next
        curr.next = prev.next;
        prev.next = curr;
    }
​
    /**
     * 在链表的指定位置插入数据,精简代码
     *
     * @param index 位置索引
     * @param value 插入数据
     */
    public void add(int index, int value) {
        //获取上一个节点
        Node prev = getNode(index - 1);
        if (prev == null) {
            throw new IndexOutOfBoundsException("index:" + index + " 越界了");
        }
        //当前需要插入的节点
        prev.next = new Node(value, prev.next);
    }
​
    /**
     * 删除第一个节点
     *
     * @return 被删除的索引节点数据
     */
    public int removeFirst() {
        if (head.next == null) {
            throw new IndexOutOfBoundsException("没有数据了...");
        }
        int v = head.next.value;
        head.next = head.next.next;
        return v;
    }
​
    /**
     * 根据索引数据
     *
     * @param index 删除索引
     * @return 被删除的索引节点数据
     */
    public int remove(int index) {
        //获取被删除节点的上一个节点
        Node prev = getNode(index - 1);
        if (prev == null) {
            throw new IndexOutOfBoundsException("index " + index + " 越界了...");
        }
        Node removed = prev.next;
        if (removed == null) {
            throw new IndexOutOfBoundsException("index " + index + " 越界了...");
        }
        int v = removed.value;
        //只需要将被删除节点的上一个节点的 next 指针指向被删除节点的 next 指针就可以了
        prev.next = removed.next;
        return v;
    }
​
    /**
     * 基于迭代器进行循环
     *
     * @return
     */
    @Override
    public Iterator<Integer> iterator() {
​
        /**
         * 匿名内部类
         */
​
        return new Iterator<Integer>() {
            Node p = head.next;
​
            /**
             * 是否存在下一个元素
             * @return
             */
            @Override
            public boolean hasNext() {
                return p != null;
            }
​
            /**
             * 返回当前数据,并指向下一个元素
             * @return
             */
            @Override
            public Integer next() {
                int v = p.value;
                p = p.next;
                return v;
            }
        };
    }
​
    /**
     * 链表中的节点,链表和节点是组合的关系,所以做成内部内比较合适。
     */
    private static class Node {
        /**
         * 节点数据
         */
        int value;
​
        /**
         * 当前节点的下一个指针
         */
        Node next;
​
        public Node(int value, Node next) {
            this.value = value;
            this.next = next;
        }
    }
​
}
​

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
链表是一种常见的数据结构,它由一系列节点组成,每个节点包含一个数据元素和一个指向下一个节点的指针。链表的实现可以使用C++语言来完成。以下是一个简链表的代码示例: ```c++ #include <iostream> using namespace std; // 定义链表节点结构体 struct ListNode { int val; ListNode* next; ListNode(int x) : val(x), next(NULL) {} }; // 定义链表类 class LinkedList { public: LinkedList() { head = NULL; } // 在链表头部插入节点 void insertAtHead(int val) { ListNode* newNode = new ListNode(val); newNode->next = head; head = newNode; } // 在链表尾部插入节点 void insertAtTail(int val) { ListNode* newNode = new ListNode(val); if (head == NULL) { head = newNode; } else { ListNode* cur = head; while (cur->next != NULL) { cur = cur->next; } cur->next = newNode; } } // 删除链表中第一个值为val的节点 void deleteNode(int val) { if (head == NULL) { return; } if (head->val == val) { ListNode* temp = head; head = head->next; delete temp; return; } ListNode* cur = head; while (cur->next != NULL && cur->next->val != val) { cur = cur->next; } if (cur->next != NULL) { ListNode* temp = cur->next; cur->next = cur->next->next; delete temp; } } // 遍历链表并输出每个节点的值 void printList() { ListNode* cur = head; while (cur != NULL) { cout << cur->val << " "; cur = cur->next; } cout << endl; } private: ListNode* head; }; // 测试代码 int main() { LinkedList list; list.insertAtHead(1); list.insertAtHead(2); list.insertAtTail(3); list.insertAtTail(4); list.printList(); list.deleteNode(2); list.printList(); return 0; } ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值