数据结构与算法的学习笔记目录:《恋上数据结构与算法》的学习笔记 目录索引
链表原理实现
- 一、链表
- 二、链表的设计
- 三、链表的接口设计
- 四、链表接口的实现
- 1. 索引越界的判断
- 2. 根据索引查找指定节点
- 3. 添加数据
- 4. 插入元素
- 5. 删除元素
- 6. 清空元素
- 7. 修改元素
- 8. 查找元素
- 9. 查找元素索引
- 10. 获取链表存储元素的个数
- 11. 链表是否为空
- 12. 判断元素是否存在
- 13. 打印链表中存储的数据
- 五、链表的复杂度
- 六、巩固练习 --- LeetCode算法题
- 1. LeetCode - [237.删除表中的节点](https://leetcode-cn.com/problems/delete-node-in-a-linked-list/)
- 2. LeetCode - [206.反转链表](https://leetcode-cn.com/problems/reverse-linked-list/)
- 3. LeetCode - [141.环形链表](https://leetcode-cn.com/problems/linked-list-cycle/)
动态数组有个明显的缺点,可能会造成内存空间的大量浪费;
链表可以做到用多少就申请多少内存!
一、链表
链表是一种 链式存储 的线性表,所有元素的内存地址不一定是连续的。
二、链表的设计
- 创建类 LinkedList ,用来管理链表数据,其中的 size 属性记录存储数据的数量,first 属性引用链表的第0个元素
- 创建私有类Node ,其中的 element 属性用于存储元素, next 属性用于指向链表中的下一个节点。
public class LinkedList<E> {
private int size;
private Node<E> first;
// 私有类,链表中的节点
private class Node<E>{
E element;
Node<E> next;
// 构造方法
public Node(E element, Node<E> next){
this.element = element;
this.next = next;
}
}
}
三、链表的接口设计
- 与动态数组一样,链表也是需要提供增删改查
// 元素的数量
int size();
// 是否为空
boolean isEmpty();
// 是否包含某个元素
boolean contains(E element);
// 添加元素到最后面
void add(E element);
// 返回index位置对应的元素
E get(int index);
// 设置index位置的元素
E set(int index, E element);
// 往index位置添加元素
void add(int index, E element);
// 删除index位置对应的元素
E remove(int index);
// 查看元素的位置
int indexOf(E element);
// 清除所有元素
void clear();
四、链表接口的实现
1. 索引越界的判断
- 在操作链表数据时,需要防止索引越界,防止出现程序异常的问题
- 当查找和删除数据时,需要检查索引的范围是 0 到 size-1 之间,所以方法如下:
// 索引越界判断
private void rangCheck(int index){
if(index < 0 || index >= size){
throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
}
}
- 当插入数据时,因为可以将数据加到所有数据的最后,所以需要检查索引的范围是 0 到 size 之间
private void rangCheckForAdd(int index){
if(index < 0 || index > size){
throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
}
}
2. 根据索引查找指定节点
- 因为链表中的数据都存在节点中,所以操作数据时 需要找到对应的节点
- 我们可以实现一个根据索引,来查找指定节点的方法
private Node<E> node(int index){
rangCheck(index);
// 头节点,就是first指向的那个节点
Node<E> node = first;
// 根据索引遍历,查找对应的节点
for(int i = 0; i < index; i++){
node = node.next;
}
return node;
}
3. 添加数据
- 添加数据时,需要创建一个节点存储数据,并将该节点拼接到最后节点的后面,然后 size 加 1;
- 两种情况:
- 第一种:当前链表没有数据,新节点拼接到first
- 第二种:当前链表有数据,新节点拼接到最后的节点
public void add(E element){
// 当first等于null时,说明此时没有节点,所以first引用新节点
if(first == null){
first = new Node<>(element, null);
}else{
// 当first不等于null时,说明链表中有节点,此时获取最后一个节点,并将该节点的next指向新节点
Node<E> node = node(size-1);
node.next = new Node<E>(element, null);
}
}
4. 插入元素
- 插入链表时,只需要创建新节点保存元素,然后插入指定位置即可
- 两种情况:
-
第一种:插入到0的位置,需要使用first指向新节点
-
第二种:插入到非0的位置,直接找到前一个节点进行处理
public void add(int index, E element){
// 检查索引是否越界
rangCheckForAdd(index);
// 当插入到0的位置
if(index == 0){
// 将first指向新节点,新节点的next指向first之前指向的节点
first = new Node<E>(element, first.next);
}else {
// 找到指定位置前面的节点
Node<E> prev = node(index);
// 将前面节点的next指向新节点,新节点的next指向prev之前指向的节点
prev.next = new Node<>(element, prev.next);
}
size++;
}
- 添加元素的方法可以简写
public void add(E element){
/*// 当first等于null时,说明此时没有节点,所以first引用新节点
if(first == null){
first = new Node<>(element, null);
}else{
// 当first不等于null时,说明链表中有节点,此时获取最后一个节点,并将该节点的next指向新节点
Node<E> node = node(size-1);
node.next = new Node<E>(element, null);
}*/
// 元素添加到size位置,即添加到最后面
add(size, element);
}
5. 删除元素
- 删除元素,找到指定节点前面的节点,然后直接指向需要删除节点的下一个节点即可,然后 size 减 1 ;
- 两种情况:
- 第一种:删除第0个元素,需要使用first指向第1个节点
- 第二种:删除非0索引的元素,找到前一个节点,然后直接指向下一个节点即可
public E remove(int index){
// 检查索引是否越界
rangCheck(index);
// 记录需要删除的节点
Node<E> old = first;
// 当删除第0个元素时,将first的next指向索引为“1”的节点即可
if(index == 0){
first = first.next;
}else{
// 找到前一个元素
Node<E> prev = node(index - 1);
// 记录需要删除的节点
old = prev.next;
// 将prev的next指向需要删除节点的后一个节点
prev.next = old.next;
}
size--;
// 返回删除的元素
return old.element;
}
6. 清空元素
- 清空元素,直接将 first 指向 null 即可,同时 size 置为 0。
public void clear(){
first = null;
size = 0;
}
7. 修改元素
- 修改元素,找到对应节点,直接修改元素即可
public E set(int index, E element){
// 找到对应节点,node方法中已经判断了索引是否越界
Node<E> node = node(index);
// 记录旧元素
E old = node.element;
// 覆盖元素
node.element = element;
// 返回旧元素
return old;
}
8. 查找元素
- 查找元素。直接找到对应的节点,取出元素即可
public E get(int index){
// node方法中已经判断了索引是否越界
return node(index).element;
}
9. 查找元素索引
- 查找指定元素的索引,需要遍历所有节点,找到节点对应的元素与执行元素相等即可
- 因为元素可以是 znull ,所以需要分两种情况处理
private static final int ElEMENT_NOT_FOUND = -1;
public int indexOf(E element){
// 取出头结点
Node<E> node = first;
// 当element为null的处理
if(element == null){
// 遍历节点,找到存储为null的节点,返回索引
for(int i = 0; i < size; i++){
if(node.element == null){
return i;
}
node = node.next;
}
}else{
for(int i = 0; i < size; i++){
// 遍历节点,找到存储的元素与指定元素相等的节点,返回索引
if(element.equals(node.element)){
return i;
}
node = node.next;
}
}
// 没有找到元素对应的节点,返回-1;
return ElEMENT_NOT_FOUND;
}
10. 获取链表存储元素的个数
- 获取链表存储元素的个数,就是 size 的值
public int size(){
return size;
}
11. 链表是否为空
- 链表是否为空,只需要判断 size 是否等于 0 即可
public boolean isEmpty(){
return size == 0;
}
12. 判断元素是否存在
- 链表元素是否存在,只需要判断元素的索引是否为 ElEMENT_NOT_FOUND
public boolean contains(E element){
return indexOf(element) != ElEMENT_NOT_FOUND;
}
13. 打印链表中存储的数据
- 只需要重写 toString 方法即可
@Override
public String toString() {
StringBuilder string = new StringBuilder();
string.append("size = ").append(size).append(", [");
Node<E> node = first;
for (int i = 0; i < size; i++) {
if (i != 0) {
string.append(",");
}
string.append(node.element);
node = node.next;
}
string.append("]");
return string.toString();
}
到此为止,我们成功的实现了链表。