03-链表(Linked List)

1.动态数组的缺点

1.动态数组有个明显的缺点:在插入时,如果发现要扩容,那么一次会扩容成原来的1.5倍。但是只是插入1个元素,很可能扩容出来的空间很大一部分根本用不上,所以可能会造成内存空间的大量浪费。

2.那么能否用到多少就申请多少内存空间呢?

  • 链表可以做到这一点

2.链表

链表是一种链式存储的线性表,所有元素的内存地址不一定是连续的。

2.1.链表的设计

  • 需要一个size属性,表明链表中有多少节点;
  • 需要一个first属性,指向第一个结点;
  • 结点Node只有LinkedList类用到,所以定义成LinkedList的内部类;
    Node类中有element属性,表示存储的数据部分;
    Node类中有next属性,指向下一个Node节点;
  • LinkedList和Node也应该是泛型的,而且两者泛型应该一致;
    在这里插入图片描述
  • 代码实现
public class LinkedList<E> {
    
    
	private int size;
	private Node<E> first;
	
	private static class Node<E>{
    
    
		E elemnt;
		Node<E> next;
		public Node(E element, Node<E> next){
    
    
			this.elemnt = element;
			this.next = next;
		}	
	}
}

2.2.链表的接口设计

链表和动态数组都属于线性表,链表的接口大部分和动态数组是一样的,但是具体的实现肯定是不一样的。那么我们抽取出来一个接口类:List,申明这些公共的方法,并且放到里面,然后LinkedList和ArrayList实现这个接口。

public interface List<E> {
    
    

	void clear();

	int size();

	boolean isEmpty();

	boolean contains(E element);

	void add(E element);

	E get(int index);

	E set(int index, E element);

	void add(int index, E element);

	E remove(int index);

	int indexOf(E element);
}

在这里插入图片描述

2.3.链表的接口(部分)实现

  1. 实现代码:
public class LinkedList<E> implements List<E>{
    
    
	
	private int size;
	private Node<E> first;
	//-1下标:代表没有这个元素
	private static final int ELEMENT_NOT_FOUND = -1;
	
	/**
	 * 检查索引是否越界
	 * @param index
	 */
	private void rangeCheck(int index) {
    
    
		if(index < 0 || index >= size) {
    
    
			outOfBounds(index);
		}
	} 
	
	/**
	 * 检查索引是否越界
	 * @param index
	 */
	private void rangeCheckForAdd(int index) {
    
    
		if(index < 0 || index > size) {
    
    
			outOfBounds(index);
		}
	} 
	
	/**
	 * 打印索引越界异常信息
	 * @param index
	 */
	private void outOfBounds(int index) {
    
    
		throw new IndexOutOfBoundsException("Index:" + index + ", Size:" + size);
	}
	
	//内部类:Node节点
	private static class Node<E>{
    
    
		E elemnt;
		Node next;
		public Node(E element, Node<E> next){
    
    
			this.elemnt = element;
			this.next = next;
		}	
	}
	
	@Override
	public int size() {
    
    
		return size;
	}

	@Override
	public boolean isEmpty() {
    
    
		return size == 0;
	}

	@Override
	public boolean contains(E element) {
    
    	
		return indexOf(element) != ELEMENT_NOT_FOUND;
	}

	@Override
	public void add(E element) {
    
    
		add(size, element);
	}
}

2.4.抽取公共部分:继承关系设计

  1. 发现存在公共的部分:私有方法,私有属性,接口实现。那么我们再创建出来一个父类AbstractList,抽取这些公共的部分。
  2. 此时既有父类,也有接口,那么继承关系怎么设计呢?
public class LinkedList<E> extends AbstractList<E>
public class ArrayList<E> extends AbstractList<E>
public abstract class AbstractList<E> implements List<E>
  1. 为什么是抽象父类?

    1.因为这个父类只抽取公共的部分,实现公共的业务,非公共部分的不实现。 所以如果既要继承List<E>接口,又想不实现其中的非公共部分的业务,那么就要用抽象类。

    2.而且这些非公共的业务方法,由于继承关系,会强制子类去实现。

    3.还有一点,如果是普通抽象类,那么是可以被实例化的。我们想要它被实例化吗?不想,因为它AbstractList作为中间的一层它只起到抽取公共部分的作用,不被外界可见。所以我们用抽象类,不让它被实例化,与外界隔绝。

  2. 为什么不在父类中申明这些公共的方法,再交给子类实现呢?

    1.要明白:存在这样的一种情况:两个子类中都有add()方法,但是两者的实现逻辑不一样。那么显然两个子类无法抽取出一个公共的add()方法,此时就需要接口来声明一个add()方法,然后交给子类分别实现。

    2.而且在接口申明的公共方法,子类必须实现这些方法,这正是我们想要的:强制子类分别实现自己的业务逻辑。

    3.如果在父类中抽取这些方法,子类可以不实现,那么之后调用方法就会默认调用父类中的方法,出现这种情况显然不是我们想要的。

  3. 供外界使用的属性,更适合放在List接口中:

    1.接口中的属性默认是public的

    2.抽象父类AbstractList是和外界屏蔽的,它只起到抽取公共部分的作用。

    3.ELEMENT_NOT_FOUND 这个属性,其实是被外界需要的。外界会使用到,比如:
    if(list.indexOf(20) == List.ELEMENT_NOT_FOUND )

    4.使用ArrayList和LinkedList时,是不用考虑AbstractList的,所以这个属性不放在AbstractList中。即不想这样调用:
    AbstractList.ELEMENT_NOT_FOUND

    5.真正用的是ArrayList和LinkedList类和List接口,AbstractList作为中间的一层,不被外界可见。这也是为什么使用抽象类,不能被实例化,与外界隔绝。

	//-1下标:代表没有这个元素
	static final int ELEMENT_NOT_FOUND = -1;
  1. 最后得到继承体系在这里插入图片描述

2.5.代码(结构)

List< E >接口:

package com.mj;

public interface List<E> {
    
    
	//-1下标:代表没有这个元素
	static final int ELEMENT_NOT_FOUND = -1;

	void clear();

	int size();

	boolean isEmpty();

	boolean contains(E element);

	void add(E element);

	E get(int index);

	E set(int index, E element);

	void add(int index, E element);

	E remove(int index);

	int indexOf(E element);
}

AbstractList< E > implements List< E > i抽象父类

package com.mj;

public abstract class AbstractList<E> implements List<E>{
    
    
	
	protected int 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);
		}
	} 

	protected void outOfBounds(int index) {
    
    
		throw new IndexOutOfBoundsException("Index:" + index + ", Size:" + size);
	}
	
	public int size() {
    
    
		return size;
	}

	public boolean isEmpty() {
    
    
		return size == 0;
	}

	public boolean contains(E element) {
    
    	
		return indexOf(element) != ELEMENT_NOT_FOUND;
	}

	public void add(E element) {
    
    
		add(size, element);
	}
	
}

ArrayList< E > extends AbstractList< E >:子类1

package com.mj;

@SuppressWarnings("unchecked")
public class ArrayList<E> extends AbstractList<E>{
    
    
	
	/**
	 * 所有的元素
	 */
	private E[] elements;
	
	private static final int DEFAULT_CAPACITY = 10;
	
	private void ensureCapacity(int capacity) {
    
    
		int oldCapacity = elements.length;
		if (oldCapacity >= capacity) return;
		int newCapacity = oldCapacity + (oldCapacity >> 1);
		E[] newElememts = (E[])new Object[newCapacity];
		for (int i = 0; i < size; i++) {
    
    
			newElememts[i] = elements[i];
		}
		elements = newElememts;
		System.out.println("扩容了: "+"旧容量,"+oldCapacity+"新容量,"+newCapacity);
	}
	
	public ArrayList() {
    
    
		//调用有参构造
		this(DEFAULT_CAPACITY);
	}
	
	public ArrayList(int capacity) {
    
    
		capacity = (capacity < DEFAULT_CAPACITY) ? DEFAULT_CAPACITY : capacity;
		elements = (E[])new Object[capacity];
	}
	
	public void clear() {
    
    
		for (int i = 0; i < size; i++) {
    
    
			elements[i] = null;
		}
		size = 0;
	}

	public E get(int index) {
    
    
		rangeCheck(index);
		
		return elements[index];
	}

	public E set(int index, E element) {
    
    
		rangeCheck(index);
		
		E old = elements[index];
		elements[index] = element;
		return old;
	}

	public void add(int index, E element) {
    
    
		rangeCheckForAdd(index);
		//确保容量足够:查看容量是否足够,不够就扩容
		ensureCapacity(size + 1);
		for(int i = size; i > index; i--) {
    
    
			elements[i] = elements[i-1]; 
		}
		elements[index] = element;
		size++;
	}

	public E remove(int index) {
    
    
		rangeCheck(index);
		
		E old = elements[index];
		//index后的元素前移
		while (index < size-1) {
    
    
			elements[index] = elements[index+1];
			index++;
		}
		
		elements[--size] = null;
		return old;
	}
	
	public int remove(E element) {
    
    
		int index = indexOf(element);
		remove(indexOf(element));
		return index;
	}

	public int indexOf(E element) {
    
    
		if(element == null) {
    
    
			for (int i=0; i < size; i++) {
    
    
				if (elements[i] == null) return i;
			}
		}else {
    
    
			//那么element一定不为null,放在前面调用equals绝对没问题
			for (int i=0; i < size; i++) {
    
    
				if (element.equals(elements[i])) return i;
			}
		}	
		return ELEMENT_NOT_FOUND;
	}
	
	@Override
	public String toString() {
    
    
		StringBuilder string = new StringBuilder();
		string.append("[");
		for (int i = 0; i < size; i++) {
    
    
			if(i != 0)
				string.append(", ");
			string.append(elements[i]);

		}
		string.append("]");
		return string.toString();
	}

}

LinkedList< E > extends AbstractList< E >:子类2

package com.mj;


public class LinkedList<E> extends AbstractList<E>{
    
    
	private Node<E> first;
	
	//内部类:Node节点
	private static class Node<E>{
    
    
		E elemnt;
		Node next;
		public Node(E element, Node<E> next){
    
    
			this.elemnt = element;
			this.next = next;
		}	
	}

	@Override
	public void clear() {
    
    
		
	}

	@Override
	public E get(int index) {
    
    
		return null;
	}

	@Override
	public E set(int index, E element) {
    
    
		return null;
	}

	@Override
	public void add(int index, E element) {
    
    
		
	}

	@Override
	public E remove(int index) {
    
    
		return null;
	}

	@Override
	public int indexOf(E element) {
    
    
		return 0;
	}
}

3.链表LinkedList的方法实现

链表LinkedList的体系结构,继承关系已经设计好了,下面我们来实现其中的具体方法

3.1.clear()

在这里插入图片描述

	@Override
	public void clear() {
    
    
		size = 0;
		first = null;
	}

3.2.add(int index, E element)和node(int index)

1.分析插入操作:发现需要先找到插入位置的前一个节点。所以我们写一个私有方法node(int index):传入索引,找到索引位置的节点。

	/**
	 * 返回索引位置处的节点
	 * @param index
	 * @return
	 */
	private Node<E> node(int index) {
    
    
		rangeCheck(index);
		Node<E> node = first;
		for (int i = 0; i < index; i++) {
    
    
			node = node.next;
		}
		return node;
	}

2.插入操作add(int index, E element)

注意:first是一个Node引用,指向第一个节点,索引为0的节点

在这里插入图片描述

	@Override
	public void add(int index, E element) {
    
    
		rangeCheckForAdd(index);
		//插到头部
		if(index == 0) {
    
    
			first.next = new Node<E>(element, first.next);
		}else {
    
    
			Node<E> perNode = node(index-1);
			perNode.next = new Node<E>(element, perNode.next);
			//perNode.next = newNode;
		}
		size++;
	}

3.3.get(int index)和set(int index, E element)

	@Override
	public E get(int index) {
    
    
		return node(index).elemnt;
	}
	
	@Override
	public E set(int index, E element) {
    
    
		Node<E> oldNode = node(index);
		E oldElement = oldNode.elemnt;
		oldNode.elemnt = element;
		return oldElement;
	}

3.4.remove(int index)

	@Override
	public E remove(int index) {
    
    
		rangeCheck(index);
		Node<E> oldNode = null;
		if(index == 0) {
    
    
			oldNode = first;
			first = first.next;
		}else {
    
    
			Node<E> preNode = node(index-1);
			oldNode = preNode.next;
			preNode.next = preNode.next.next;
		}
		size--;
		return oldNode.element;
	}

3.5.indexOf(E element)和toString()

	@Override
	public int indexOf(E element) {
    
    
		Node<E> tmpNode = first;
		if(element == null) {
    
    
			for (int i=0; i < size; i++) {
    
    
				if (tmpNode.element == null) return i;
				tmpNode = tmpNode.next;
			}
		}else {
    
    
			//那么element一定不为null,放在前面调用equals绝对没问题
			for (int i=0; i < size; i++) {
    
    
				if (element.equals(tmpNode.element)) return i;
				tmpNode = tmpNode.next;
			}
		}	
		return ELEMENT_NOT_FOUND;
	}
	
	@Override
	public String toString() {
    
    
		StringBuilder string = new StringBuilder();
		Node<E> tmpNode = first;
		string.append("[");
		for (int i = 0; i < size; i++) {
    
    
			if(i != 0) string.append(", ");
			string.append(tmpNode.element);
			tmpNode = tmpNode.next;
		}
		string.append("]");
		return string.toString();
	}

4.练习

4.1.翻转链表

206.翻转链表
输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL

4.1.1递归算法

1.递归边界:

   	if(head == null) return head;
   	if(head.next == null) return head;

2.逻辑:
调用reverseList2(head.next)时,得到如下结果
在这里插入图片描述
3. 代码实现

	    /**
	     * 递归解法
	     * @param head
	     * @return
	     */
	    public ListNode reverseList2(ListNode head) {
    
    
	    	if(head == null) return head;
	    	if(head.next == null) return head;
	    	ListNode newHead = reverseList2(head.next);
	    	head.next.next = head;
	    	head.next = null;
	    	return newHead;
	    }

4.1.2. 迭代实现

注意1:

  • ListNode first = null;新链表的头结点:初始值肯定是null。会动态变化,一直保持着是新链表的第一个结点,方便下一个要翻转的节点指向。
  • first = cur;first又变成新链表的第一个结点:且方便下一个要翻转的节点指向

注意2: 一定要注意翻转第一个结点的情况。

  • 翻转第一个结点head,让其指向新链表的“第一个结点first”。你应该能明白我这里的引号。
  • 新链表的“第一个结点first”要继续成为新链表的第一个结点:first指向刚才被翻转的节点head。被翻转的节点后移。
	    public ListNode reverseList(ListNode head) {
    
    
	    	//指向当前要翻转的节点
	    	ListNode cur = head;
	    	//新链表的头结点:会动态变化,一直保持着是新链表的第一个结点,方便下一个要翻转的节点指向。初始值肯定是null。
	    	ListNode first = null;
	    	ListNode tmp = null;
	    	while(cur != null){
    
    
	    		tmp = cur.next;//先保存下一个节点
	    		cur.next = first;
	    		first = cur;//first又变成新链表的第一个结点:且方便下一个要翻转的节点指向
	    		cur = tmp;
	    	}
	    	return first;
	    }

4.2.判断一个链表是否有环

141.环形链表

给定一个链表,判断链表中是否有环。

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

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

在这里插入图片描述

4.2.1.解法思路

1.思路:快慢指针

设置一个快指针和一个慢指针,快指针每次走两步,满指针每次走一步。如果有环,那么快指针一定会追上慢指针。(如果快指针每次走三步,有环时,那么可能在第一圈的时候错过慢指针,要走好几圈才能相遇)

快指针每次走两步,满指针每次走一步:这样是最保险的。让快指针走了一圈后,每一次外循环,快慢指针距离都会接近一步,这样保证两者bui错过。

如果快指针达到空,说明没有环。

满指针一开始指向头结点,快指针一开始指向第二个节点:如果都指向头结点,那么一开始就相遇了,后面不好再进行条件判断。

在这里插入图片描述
2.代码

	    public boolean hasCycle(ListNode head) {
    
    
	    	if(head==null || head.next==null) return false;
	    	ListNode slow = head;
	    	ListNode fast = head.next;
	    	//fast == null && fast.next == null:到达边界条件,说明没环
 	    	while(fast != null && fast.next != null) {
    
    
 	    		//比较放到下面:因为第一次比较肯定不相等。
 	    		slow = slow.next;
 	    		fast = fast.next.next;
 	    		if(slow == fast) {
    
    
 	    			return true;
 	    		}
	    	}
	        return false;
	    }
	}

5.作业

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/tttxxl/article/details/115178773
今日推荐