三、链表

85 阅读5分钟

一、链表

  • 动态数组有个明显的缺点

    • 可能会造成内存空间的大量浪费
  • 能否用到多少就申请多少内存?

    • 链表可以办到这一点
  • 链表是一种链式存储的线性表,所有元素的内存地址不一定是连续的 链表

二、链表的设计

链表的设计

public class LinkedList<E> {

	private int size;
	private Node<E> first;
	
	private static 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); // 添加元素到最后面

E get(int index); // 返回index位置对应的元素

E set(int index, E element); // 设置index位置的元素

void add(int index, E element); // 往index位置添加元素

E remove(int index); // 删除index位置对应的元素

int indexOf(E element); // 查看元素的位置

void clear(); // 清除所有元素

那么链表和动态数组的很多接口是一样的,就可以设计一个接口List。

但是有一部分接口的实现是相同的(size、isEmpty、contains、add),有些接口的实现是不相同的,于是可以在抽象一个父类AbstractList来实现链表和动态数组相同接口实现。

List接口:

public interface List<E> {
	
	static final int ELEMENT_NOT_FOUND = -1;
	
	int size(); // 元素的数量

	boolean isEmpty(); // 是否为空

	boolean contains(E element); // 是否包含某个元素

	void add(E element); // 添加元素到最后面

	E get(int index); // 返回index位置对应的元素

	E set(int index, E element); // 设置index位置的元素

	void add(int index, E element); // 往index位置添加元素

	E remove(int index); // 删除index位置对应的元素

	int indexOf(E element); // 查看元素的位置

	void clear(); // 清除所有元素
	
}

AbstractList类实现:

public abstract class AbstractList<E> implements List<E>{

	protected int size;
	
	/**
	 * 元素的数量
	 * @return
	 */
	@Override
	public int size() {
		return size;
	}

	/**
	 * 是否为空
	 * @return
	 */
	@Override
	public boolean isEmpty() {
		return size == 0;
	}

	/**
	 * 是否包含某个元素
	 * @param element
	 * @return
	 */
	@Override
	public boolean contains(E element) {
		return indexOf(element) != ELEMENT_NOT_FOUND;
	}

	/**
	 * 添加元素到尾部
	 * @param element
	 */
	@Override
	public void add(E element) {
		add(size,element);
	}
	
	protected void outOfBounds(int index) {
		throw new IndexOutOfBoundsException("Index:" + index + ", Size:" + size);
	}
	
	protected void rangeCheck(int index) {
		if (index < 0 || index >= size) {
			outOfBounds(index);
		}
	}
	
	protected void rangeCheckForAdd(int index) {
		if (index < 0 || index > size) {
			outOfBounds(index);
		}
	}
}

四、清空元素 - clear()

清空元素

  • 清空元素只需要将first置为null就可以了,然后size置为0。
  • next是不需要置为null的,因为first = null后,first置为null之前的元素是没有GCRoot引用的,会被GC自动回收。
public void clear() {
    first = null;
    size = 0;
}

五、添加元素 - add(int index,E element)

findNode(int index)

如果想添加一个元素,得先找到要添加元素位置的前一个元素才可以完成,所以这里先实现一个findNode方法用于根据索引查找对应的元素

/**
* 获取index位置对应的节点对象
* @param index
* @return
*/
private Node<E> findNode(int index){
    rangeCheck(index);
    
    Node<E> node = first;
    for(int i=0;i < index;i++) {
    	node = node.next;
    }
    return node;
}

add(int index,E element)

public void add(int index, E element) {
    rangeCheckForAdd(index);
    if(index == 0) {
    	first = new Node<E>(element, first);
    }else {
    	Node<E> prev = findNode(index - 1);
    	prev.next = new Node<E>(element, prev.next);
    }
    size++;
}

在编写链表过程中,要注意边界测试,比如 index 为 0 、size – 0 、size 时

六、获取和设值 - get(ind index)、set(int index,E element)

上面findNode方法实现了,那么对应的get和set方法也可以实现了。

public E get(int index) {
    return findNode(index).element;
}

public E set(int index, E element) {
    Node<E> node = findNode(index);
    E old = node.element;
    node.element = element;
    return old;
}

七、删除元素 - remove(int index)

public E remove(int index) {
    rangeCheck(index);//如果链表中没有数据
    Node<E> node = first;
    if(index == 0) {
    	first = first.next;
    }else {
    	Node<E> prev = findNode(index - 1);
    	node = prev.next;
    	prev.next = node.next;
    }
    	size--;
    return node.element;
}

八、查看元素的索引 - indeOf(E element)

/**
 * 查看元素的索引
 * @param element
 * @return
 */
@Override
public int indexOf(E element) {
	if (element == null) {  // 1
		for (int i = 0; i < size; i++) {
			Node<E> node = first;
			if (node.element == null) return I; 
			node = node.next;
		}
	} else {
		for (int i = 0; i < size; i++) {
			Node<E> node = first;
			if (element.equals(node.element)) return I;
			node = node.next;
		}
	}
	return ELEMENT_NOT_FOUND;
}

九、推荐一个神奇的网站

数据结构和算法动态可视化 数据结构和算法动态可视化

十、Leetcode题

1、237. 删除链表中的节点

请编写一个函数,用于删除单链表中某个特定节点。在设计函数时需要注意,你无法访问链表的头节点head,只能直接访问要被删除的节点

题目数据保证需要删除的节点不是末尾节点

public void deleteNode(ListNode node) {
	node.val = node.next.val;
	node.next = node.next.next;
}

2、 206. 反转链表

给你单链表的头节点head,请你反转链表,并返回反转后的链表。

输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]

递归

  • 使用递归函数,一直递归到链表的最后一个结点,该结点就是反转后的头结点,记作newHead .
  • 此后,每次函数在返回的过程中,让当前结点的下一个结点的next指针指向当前节点。
  • 同时让当前结点的next指针指向LNULL ,从而实现从链表尾部开始的局部反转
  • 当递归函数全部出栈后,链表反转完成。
public static ListNode reverseList(ListNode head) {
	if(head == null || head.next == null) return head;
	ListNode newHead = reverseList(head.next);
	head.next.next = head;
	head.next = null;
	return newHead;
}

迭代

public static ListNode reverseList1(ListNode head) {
	ListNode newHead = null;
	while(head != null) {
		ListNode temp = head.next;
		head.next = newHead;
		newHead = head;
		head = temp;
	}
	return newHead;
}

3、141. 环形链表

给你一个链表的头节点head,判断链表中是否有环。

如果链表中有某个节点,可以通过连续跟踪next指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数pos来表示链表尾连接到链表中的位置(索引从 0 开始)。如果pos-1,则在该链表中没有环。注意:pos不作为参数进行传递,仅仅是为了标识链表的实际情况。

如果链表中存在环,则返回true。 否则,返回false

输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。
public boolean hasCycle(ListNode head) {
	 if(head == null || head.next == null) return false;
	 ListNode slow = head;
	 ListNode fast = head.next;
	 while(fast != null && fast.next != null) {
		slow = slow.next;
		fast = fast.next.next;
		if(slow == fast) {
			return true;
		}
	 }
	 return false;
 }

代码链接