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.链表的接口(部分)实现
- 实现代码:
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.抽取公共部分:继承关系设计
- 发现存在公共的部分:私有方法,私有属性,接口实现。那么我们再创建出来一个父类AbstractList,抽取这些公共的部分。
- 此时既有父类,也有接口,那么继承关系怎么设计呢?
public class LinkedList<E> extends AbstractList<E>
public class ArrayList<E> extends AbstractList<E>
public abstract class AbstractList<E> implements List<E>
-
为什么是抽象父类?
1.因为这个父类只抽取公共的部分,实现公共的业务,非公共部分的不实现。 所以如果
既要继承List<E>接口,又想不实现其中的非公共部分的业务,
那么就要用抽象类。2.而且这些非公共的业务方法,由于继承关系,会强制子类去实现。
3.还有一点,如果是普通抽象类,那么是可以被实例化的。我们想要它被实例化吗?不想,因为它AbstractList作为中间的一层它只起到抽取公共部分的作用,不被外界可见。所以我们用抽象类,不让它被实例化,与外界隔绝。
-
为什么不在父类中申明这些公共的方法,再交给子类实现呢?
1.要明白:存在这样的一种情况:两个子类中都有add()方法,但是两者的实现逻辑不一样。那么显然两个子类无法抽取出一个公共的add()方法,此时就需要接口来声明一个add()方法,然后交给子类分别实现。
2.而且在接口申明的公共方法,子类必须实现这些方法,这正是我们想要的:强制子类分别实现自己的业务逻辑。
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;
- 最后得到继承体系
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;
}
}