一、数组的链式描述
在链式描述中,数据对象实例的每一个元素都用一个单元或结点来描述。结点不必是数据成员,因此不是用公式来确定元素位置。取而代之的是,每一个结点都包含另一个相关结点的位置信息,这个信息称为链或指针。
设L=(e0,e1,e2,…,en-1)是一个线性表。在这个表的链式描述中,每个元素都在一个单独的结点中描述,每一个结点都有它的链域,它的值是线性表的下一个元素的地址。这样一来,元素ei的结点链接这ei+1的结点。元素en-1的结点没有其他结点来可链接,因此链域值是NULL。
上图是线性表L的链式描述。链域用箭头表示。为了确定元素e2的位置,必须从firstNode开始,从其中的链域找到e1结点的指针,再从结点的链域找到e2结点的指针。一般来说,为了找到索引为theIndex的元素,需要从firstNode开始,跟踪theIndex个指针才能找到。
要删除下图中的元素e2,需要以下步骤(注意,e2是单链表中的第三个结点):
·找到第二个结点
·把第二个结点和第四个结点链接起来。
注意,删除了上图第三个结点,其后续结点的索引自动减一。链表的结点都是从firstNode开始。沿着一系列指针可以找到结点,而应该被删除的结点,从firstNode开始是不可能找到的,因此它不再是链表的结点,也就不用去修改它的指针域。
为了在链表中插入一个索引为index的结点,需要首先找到索引为index-1的结点,然后在它后面插入新结点。下图显示了在两种情况下(index=0和0<index≤listSize)进行删除时的指针变化
二、Java语言实现
package JavaProject.MyList;
//单链表
class Node<T> {//结点结构
T data;
Node<T> next;
public Node() { //默认无参构造方法
this.next = null;
}
public Node(T element) {//给结点赋值的构造方法
this.data = element;
this.next = null;
}
public Node(T element, Node<T> next) {//给结点赋值,给next指向
this.data = element;
this.next = next;
}
}
public class LinkedList<T> {
private int size;
private Node<T> firstNode = new Node<>();
private Node<T> tail = new Node<>();
public LinkedList() { //头尾不赋值
size = 0;
firstNode.next = tail;
}
public LinkedList(T[] a) { //通过数组转化成链表
size = 0;
firstNode.next = tail;
for (int i = 0; i < a.length; i++) {
this.add(a[i]);
}
}
//下面的theIndex都是从0开始
public void insert(T element, int theIndex) {
if (theIndex > size) {
try {
throw new OutOfBoundsError();
} catch (OutOfBoundsError e) {
e.printStackTrace();
}
return;
} else {
Node<T> p = firstNode;
for (int i = 0; i <= theIndex - 1; i++) {//找要插入的前一个结点
p = p.next;
}
p.next = new Node<T>(element, p.next); //插入目标结点
}
size++;
}
public void add(T element) {
insert(element, size);
}
public void delete(int theIndex) { //
if (theIndex > size) {
try {
throw new OutOfBoundsError();
} catch (OutOfBoundsError e) {
e.printStackTrace();
}
return;
} else {
Node<T> p = firstNode;
for (int i = 0; i <= theIndex - 1; i++) {//移到要删除的前一个结点
p = p.next;
}
p.next = p.next.next;
}
size--;
}
public T get(int theIndex) { //index从0开始
T element = null;
Node<T> p = firstNode;
if (theIndex >= size) {
try {
throw new OutOfBoundsError();
} catch (OutOfBoundsError e) {
e.printStackTrace();
}
return null;
}
for (int i = 0; i <= theIndex; i++) {
p = p.next;
}
element = p.data;
return element;
}
public void set(int theIndex, T element) {
if (theIndex >= size) {
try {
throw new OutOfBoundsError();
} catch (OutOfBoundsError e) {
e.printStackTrace();
}
}
Node<T> p = firstNode;
for (int i = 0; i <= theIndex; i++) { //找到对应的索引结点
p = p.next;
}
p.data = element;
}
public int indexOf(T element) {
int theIndex = -1;
if (!isEmpty()) {
Node<T> p = firstNode;
for (int i = 0; i < size; i++) {
p = p.next;
if (p.data.equals(element)) {
theIndex = i;
break;
}
}
}
return theIndex;
}
public void print() {
Node<T> p = firstNode;
for (int i = 0; i < size; i++) {
p = p.next;
System.out.print(p.data + " ");
}
System.out.println();
}
public int size() {
return size;
}
public boolean isEmpty() {
if (size == 0) {
return true;
} else {
return false;
}
}
public void mergList(LinkedList<T> listB) {//合并两个链表
Node<T> p = this.firstNode;
for (int i = 0; i < size; i++) { //找到A表的最后一个元素结点
p = p.next;
}
p.next = listB.firstNode.next;
size = this.size + listB.size;
return ;
}
}
说明:这个类包含了几个方法:
- 两个构造方法:一个是无参但有方法体的,将size初始化为0,并将头结点指向尾结点,但是首尾结点均不赋值,仅作为标记。另一个是把传入的数组参数转化成单链表。
- insert方法,用于在指定索引处插入指定元素,要先找到要插入位置的前一个结点,再进行插入操作
- add方法,直接在列表尾部插入元素
- delete方法:删除指定索引处的元素
- get方法:获取指定索引处的元素
- set方法:将指定索引处的旧值换成新值
- indexOf方法:返回指定元素的索引,如果有多个,则返回第一个出现的索引
- print方法:按顺序输出链表元素
- size方法:返回链表的当前大小
- isEmpty方法:判断链表是否为空
- mergeList方法:将两个链表合并。方法是将第一个表的最后一个存放元素的结点(尾结点的前一个结点)指向第二个表的第一个存放元素的结点(头结点的下一个结点)
三、顺序结构和链式结构的优缺点
1.顺序表存储
存取速度高效,通过下标来直接存储。但是插入和删除比较慢,有时要扩容,插入或者删除一个元素时,整个表需要遍历移动元素。
2.链表存储
优点:插入和删除速度快,保留原有的物理顺序,插入或者删除一个元素时,只需要改变指针指向即可 。但是查找速度慢,因为查找时,需要从头结点开始逐个访问。