数据结构与算法-线性表
基本概念
线性表,顾名思义就是从逻辑上讲是连续的,一系列元素组成的数据元素,可以连起来就像一条线一样。
逻辑表述就是:
a1是a2的前驱,ai+1 是ai的后继,a1没有前驱,an没有后继
n为线性表的长度 ,若n==0时,线性表为空表
常见线性表
顺序存储线性表
说一种最常见的顺序存储线性表例子就是,排队买火车票的时候。 长长的一条队列。
删除:突然一哥们打电话有急事从你前边走了, 这时候所有人上前补下缺口。每个人占用了刚才前一个人的位置。
插入: 终于要排到了,这时候一美女过来对你说有急事,求插队。 碍于面子让他站到你前边的位置,也就是你原来站的位置。 这时候所有人都向后移动一个位置。
链式存储线性表
特点是物理存储结构用一组任意的存储单元存储线性表的数据元素。
注意,物理地址可以连续也可以不连续。
单向链表:顾名思义就是单向链接查询,只能正序单向查询遍历。其结构如图:
这里用的图是C++ 的结构图,理解意思就好。 P就是单个数据节点。 该节点包含了数据地址部分, 和下一个数据的地址。
循环链表 将单链表中终端结点的指针端由空指针改为指向头结点,就使整个单链表形成一个环,这种头尾相连的单链表称为单循环链表。 其结构如图:
这样的结构和单向链表的区别很明显,就是最后一个元素的next地址指向了第一元素。 这样的优点就是在遍历的时候可以进行循环遍历。
双向循环链表 双向循环链表是单向循环链表的每个结点中,再设置一个指向其前驱结点的指针域。其结构如图:
这样的结构就是,单个元素包含了 前一个元素的地址和数据部分地址还有后一个元素的地址。 当然看一下就明白 这种结构可以双向循环遍历,比起单向链表循环更加方便自由。
一个空的双向循环链表结构:
可以看到,头部和尾部地址同时指向了自身。
基本操作
- 插入
单向链表:
插入操作:
p->next = e
e->next = ai+1
双向循环链表
插入操作:
s->previous = p
s->next = p->next
p->previous = s
p->next = s
- 删除
单向链表
删除操作:
ai-1->next = ai+1
移除ai
双向循环链表
删除操作:
ai-1->next = ai+1
ai+1->prior = ai-1
移除ai
常见数据结构类分析
这里列举出Java语言中常见的线性表类: ArrayList & LinkedList
ArrayList
基本结构
/* 使用了泛型 继承自AbstractList 实现了List接口 。 */
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
/**
* 默认初始化容器大小
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* 默认空元素数组
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
/**
* 默认空元素的空数据状态
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
* 可以看到整个List使用的是最基础的顺序型存储结构的数组
*/
transient Object[] elementData; // non-private to simplify nested class access
/**
* The size of the ArrayList (the number of elements it contains).
*
* @serial
*/
private int size;
基本操作
- 遍历(查找)
/**
* 查询是否包含具体某个对象,返回true or
false
*/
public boolean contains(Object o) {
return indexOf(o) >= 0;
}
/**
* 遍历查找数组中与要查的元素相同的第一个位置,没查到则返回-1
*/
public int indexOf(Object o) {
if (o == null) {
for (int i = 0; i < size; i++)
if (elementData[i]==null)
return i;
} else {
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
/**
* 遍历查找数组中与要查的元素相同的最后一个位置,没查到则返回-1
*/
public int lastIndexOf(Object o) {
if (o == null) {
for (int i = size-1; i >= 0; i--)
if (elementData[i]==null)
return i;
} else {
for (int i = size-1; i >= 0; i--)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
indexOf 和 lastIndexOf 这两个方法都是通过for 进行遍历操作, 后者采用了倒序方式。
- 插入
/**
* 把元素插入到指定的位置
*
*/
public void add(int index, E element) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
/// 扩容 +1
ensureCapacityInternal(size + 1); // Increments modCount!!
/// 复制elementData 到index 位置,和elementData , index +1 , 中间空出来一个index
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
/// 设置index位置的元素为element
elementData[index] = element;
size++;
}
注意上边ensureCapacityInternal方法在指定位置插入元素需要扩容。 下边我们看一下扩容方法
/**
* 如果需要则扩容,来确保能够装下最后一个需要扩容大小的元素
*/
public void ensureCapacity(int minCapacity) {
int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
// any size if not default element table
? 0
// larger than default for default empty table. It's already
// supposed to be at default size.
: DEFAULT_CAPACITY;
if (minCapacity > minExpand) {
ensureExplicitCapacity(minCapacity);
}
}
/**
* 如果是空数组,直接添加,则取默认值10(Android 源码中是12) 和请求值中大的那个,并进行扩容操作。
*/
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
/**
* modCount ++ 修改数++
* 如果请求的大小超过现在数组的大小则进行扩容
*/
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
/**
* 数组的最大容量
* 有些虚拟机需要保留一个header area ,所以并不是Integer.Max_value
* OutOfMemoryError: 当请求的大小超过指定的最大大小
*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
/**
* 扩容操作
* 先声明一个新容量(原容量+ 原容量/2)如果新容量没有要扩展需求的大则采用需求的容量
如果新容量大于最大数组size 则进入最大扩容方法。
*/
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
/**
* 如果当前的容量大于数组最大容量则返回integer最大值, 否则返回最大数组值。
最大数组值是max_value-8
*/
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
grow方法 先声明一个新容量(原容量+ 原容量/2)如果新容量没有要扩展需求的大则采用需求的容量如果新容量大于最大数组size 则进入最大扩容方法。 后边则是copyarray至最大容量
hugeCapacity方法 看一下数组最大长度,是 Integer.MAX_VALUE 在Java中最大能表示的值为 2的31次方-1 的常量。
overflow 是发生在max_value +1 之后
01111111 111111111111111111111111 + 1 = 10000000 00000000 00000000 00000000
刚好最高位符号位变成了负 ,负数的原码是补码取反+1,刚好等于Integer.min_value 所以这里就判断了 minCapacity<0 操作
。
- 修改
/**
* 修改列表中的具体位置的某个元素,返回旧元素。
*/
public E set(int index, E element) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
E oldValue = (E) elementData[index];
elementData[index] = element;
return oldValue;
}
set 简单的基本数组操作 不再赘述。
- 删除
/**
* 删除指定位置元素
*/
public E remove(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
modCount++;
E oldValue = (E) elementData[index];
/// 计算要移动的元素个数
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
/**
* 先比较值后删除
*/
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
/*
* 快速删除 ,没有边界检测,没有返回值
*/
private void fastRemove(int index) {
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
}
/**
* 遍历设置为null, size 设置为0
*/
public void clear() {
modCount++;
// clear to let GC do its work
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}
这里说一下 System.arraycopy()方法
public static void (Object src,
int srcPos,
Object dest,
int destPos,
int length)
- src:源数组;
- srcPos:源数组要复制的起始位置;
- dest:目的数组;
- destPos:目的数组放置的起始位置;
- length:复制的长度。
/// 需要移动的长度
int numMoved = size - index - 1;
/// 判断是否是最后一位元素
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
将原来数组从index位置后一位起到最后的所有需要移动的元素个数,往前copy了一位。
由上可以看出,线性顺序性存储结构的缺点就是删除太费劲, 当然除非你删除的一直是最后一个元素。当然优点就是查询速度快,毕竟地址都连着的嘛。
LinkedList
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable{
transient int size = 0;
/**
* 第一个元素.
* 初始化条件
* Invariant: (first == null && last == null) ||
* (first.prev == null && first.item != null)
*/
transient Node<E> first;
/**
* 最后一个元素
* Invariant: (first == null && last == null) ||
* (last.next == null && last.item != null)
*/
transient Node<E> last;
/**
* Constructs an empty list.
*/
public LinkedList() {
}
基本结构
/**
* 单个元素的基本机构
*/
private static class Node<E> {
/// 数据item
E item;
///连接下一个元素
Node<E> next;
/// 连接前一个元素
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
基本操作
- 遍历
/**
* 查找具体某个位置的某个元素
*/
Node<E> node(int index) {
// assert isElementIndex(index);
/// 如果index 小于总长度的一半 则正序遍历。
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
/// 倒序遍历, 从最后一个元素一直倒序,获取每个元素的previous , 查到之后返回。
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
从条件判断和遍历方式上看 这种方法很注重效率。
- 插入
/**
* 添加元素到第一位
*/
private void linkFirst(E e) {
final Node<E> f = first;
final Node<E> newNode = new Node<>(null, e, f);
first = newNode;
if (f == null)
last = newNode;
else
f.prev = newNode;
size++;
modCount++;
}
/**
* 添加最后一个元素到末尾
*/
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
/**
* 把具体某个元素 插入到succ 前边
* 操作顺序:
e.prev = succ.prev
e.next = succ
succ.prev.next = e
succ.prev = e
*/
void linkBefore(E e, Node<E> succ) {
// assert succ != null;
final Node<E> pred = succ.prev;
final Node<E> newNode = new Node<>(pred, e, succ);
succ.prev = newNode;
if (pred == null)
first = newNode;
else
pred.next = newNode;
size++;
modCount++;
}
- 修改
/**
* Replaces the element at the specified position in this list with the
* specified element.
*
*/
public E set(int index, E element) {
checkElementIndex(index);
/// 找到index 位置的节点x , 替换item ,返回oldval
Node<E> x = node(index);
E oldVal = x.item;
x.item = element;
return oldVal;
}
- 删除
/**
* 删除某个索引位置的元素
*/
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}
/**
* Tells if the argument is the index of an existing element.
*/
private boolean isElementIndex(int index) {
return index >= 0 && index < size;
}
/**
* Tells if the argument is the index of a valid position for an
* iterator or an add operation.
*/
private boolean isPositionIndex(int index) {
return index >= 0 && index <= size;
}
/**
检查元素索引 从0 开始 最大值 size-1
*/
private void checkElementIndex(int index) {
if (!isElementIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
/**
检查位置索引, 从0 开始到 size
*/
private void checkPositionIndex(int index) {
if (!isPositionIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
/**
* 删除节点 x.
*/
E unlink(Node<E> x) {
// assert x != null;
final E element = x.item;
final Node<E> next = x.next;
final Node<E> prev = x.prev;
/// 如果该元素为第一个元素
if (prev == null) {
/// 设置全局first为next
first = next;
} else {
/// 该元素的前边一个元素的next 设置为x.next 也就是 next
prev.next = next;
/// x.prev 置空
x.prev = null;
}
/// 如果该元素为最后一个元素
if (next == null) {
///设置全局last为previous
last = prev;
} else {
/// x.next.prev = prev;
next.prev = prev;
/// 置空
x.next = null;
}
x.item = null;
size--;
modCount++;
return element;
}
/**
* 删除第一个元素
*/
private E unlinkFirst(Node<E> f) {
// assert f == first && f != null;
final E element = f.item;
final Node<E> next = f.next;
f.item = null;
f.next = null; // help GC
first = next;
/// 如果fist.next ==null , 说明没元素了。
if (next == null)
last = null; /// 最后一个元素也置空
else
next.prev = null; 这里设定的fist.prev 也是空
size--;
modCount++;
return element;
}
/**
* 删除最后一个元素
*/
private E unlinkLast(Node<E> l) {
// assert l == last && l != null;
final E element = l.item;
final Node<E> prev = l.prev;
l.item = null;
l.prev = null; // help GC
last = prev;
// 删完了, 尴尬
if (prev == null)
first = null;
else
prev.next = null;
size--;
modCount++;
return element;
}
总结
看数据结构,看源码一定要有自己的思路,然后又有自己的问题, 带着问题看源码,然后去验证思路,寻找答案,然后再理顺一下结构即可。
至于源码部分 基本上重要的方法基本操作全部分析完成。
其他的比如Iterator , ListIterator 内部的next , hasNext , add ,move 等操作基本上不离以上分析的几种类型, 在此不再赘述。