链表
链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。 相比于线性表顺序结构,操作复杂。由于不必须按顺序存储,链表在插入的时候可以达到O(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而线性表和顺序表相应的时间复杂度分别是O(logn)和O(1)。
使用链表结构可以克服数组链表需要预先知道数据大小的缺点,链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。但是链表失去了数组随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大。链表最明显的好处就是,常规数组排列关联项目的方式可能不同于这些数据项目在记忆体或磁盘上顺序,数据的存取往往要在不同的排列顺序中转换。链表允许插入和移除表上任意位置上的节点,但是不允许随机存取。链表有很多种不同的类型:单向链表,双向链表以及循环链表。链表可以在多种编程语言中实现。像Lisp和Scheme这样的语言的内建数据类型中就包含了链表的存取和操作。程序语言或面向对象语言,如C,C++和Java依靠易变工具来生成链表。
带头结点的单向链表实现
- 创建一个Node类,类中存放一些所需要变量和下一个Node结点的引用~
- 创建操作类,首先需要初始化一个头结点,头结点不要动,也不存放任何数据,仅仅作为一个标识位
链表插入结点(不考虑编号)
- 在链表末尾插入元素,因为头结点不可动,所以我们需要设置一个辅助结点pNode来作为指针,指向头结点
- 当链表不为空时,指针后移(pNode=pNode.next),直到pNode.next=null时,代表着pNode此时指向了链表中最后一个结点
- 输出链表时,需要避免将没有数据的头结点和有数据的结点一起返回,于是辅助结点一开始会指向pNode=head.next,当pNode=null时,退出循环。
public class SingleLinkedList {
public static void main(String[] args) {
SingleLinkedList list = new SingleLinkedList();
list.add(new Node(1, "Apple", 2.00));
list.add(new Node(2, "Pear", 2.00));
list.add(new Node(3, "Strawberry", 2.00));
list.display();
}
// 设置一个变量存储结点的个数
private int size;
Node head = new Node(0, "", 0.00);
public void add(Node node) {
Node pNode = head;
while (true) {
if (pNode.next == null) {
break;
}
pNode = pNode.next;
}
pNode.next = node;
size++;
}
public void display() {
if (size == 0) {
System.out.println("链表为空!");
return;
}
Node pNode = head.next;
while(true) {
if (pNode == null) {
break;
}
System.out.println(pNode);
pNode = pNode.next;
}
}
}
链表插入结点(在指定位置插入)
插入的元素需要按照编号的顺序来插入链表之中:
- 首先通过辅助结点,找到新添加结点的位置
- 将新的结点newNode.next指向辅助结点pNode.next
- 再讲pNode.next指向newNode
- 结合以上三点,我们需要通过遍历的方式来查询新结点添加的位置,并且,辅助结点需要指向新添加的位置的前一个结点,这时联想到,我们的Node结点有id参数,当pNode.next.id > newNode.id时,辅助结点就指向了前一个位置的结点。
链表删除结点
在链表中删除指定的结点(删除指定id的结点):
- 首先进行判断,当链表为空时,或者是链表没有该结点时,返回错误信息
- 通过辅助结点pNode=head,不断后移,找到id参数相同的结点,即当pNode.next.id==id时,将pNode.next指向下下个结点,即pNode.next=pNode.next.next,然后将结点个数减一(size–)
// 删除结点
public void delete(int id) {
if (size == 0) {
System.out.println("链表为空");
return;
}
Node pNode = head;
while(true) {
if (pNode.next == null) {
System.out.println("未找到该结点~");
break;
}
if (pNode.next.id == id) {
pNode.next = pNode.next.next;
size--;
break;
}
pNode = pNode.next;
}
}
链表修改结点
在链表中修改指定结点的属性:
- 通过辅助结点pNode,找到与pNode的id参数相等的结点,并将pNode当前属性替换成newNode结点的属性即可~
// 修改结点
public void updata(Node newNode) {
if (size == 0) {
System.out.println("链表为空");
return;
}
Node pNode = head.next;
while(true) {
if (pNode == null) {
System.out.println("未找到该结点~");
break;
}
if (newNode.id == pNode.id) {
pNode.name = newNode.name;
pNode.price = newNode.price;
break;
}
pNode = pNode.next;
}
}
扩展:获取单链表倒数第n个结点
- 设置一个变量index,来存储倒数第n个结点,当index小于等于0时或大于链表的长度时,则返回下标越界错误!
- 排除头结点,设置辅助结点pNode指向head.next,通过for循环,找到倒数第n个结点,这时需要联系单链表的结点个数size来进行计算,假设当前结点为4个,我们需要找倒数第2个结点,此时pNode指向第一个结点,通过for (int i = 0; i < size-index; i++) 得到只需要将pNode下移2次即可找到倒数第2个结点,也就是第3个结点~
// 获取单链表倒数第n个结点
public Node findLastIndexNode(int index) {
if (index > size || index <= 0) {
System.out.println("下标不存在!");
return null;
}
if (head.next == null) {
System.out.println("链表为空");
return null;
}
Node pNode = head.next;
for (int i = 0; i < size-index; i++) {
pNode = pNode.next;
}
return pNode;
}
扩展:链表的反转
- 先定义一个新的头结点reverseHead,然后从头到尾遍历原来的链表,每遍历一个结点,就将其取出,并放在新的链表reverseHead的最前端!
- 设置一个辅助结点pNode指向链表中除头结点外的第一个结点head.next
- 设置一个临时结点next来保存辅助结点pNode所指向当前结点的下一个结点,即next=pNode.next
- 将pNode指向的结点放在新链表的最前端,即pNode.next=reverseHead.next,并将新头结点指向pNode,即reverseHead.next=pNode,使链表连起来~
- 将pNode下移,即pNode=next
- 最后只需要将head头结点覆盖新头结点reverseHead即可(或者将head.next指向reverseHead.next)
// 链表的反转
public void reverse() {
Node reverseHead = new Node(0, null, 0.00);
Node pNode = head.next;
// 设置一个结点来存储辅助结点pNode的所指向的结点的下一个结点
Node next;
if (pNode == null || pNode.next == null) {
return;
}
while(pNode != null) {
next = pNode.next;
pNode.next = reverseHead.next;
reverseHead.next = pNode;
pNode = next;
}
head.next = reverseHead.next;
}
扩展:链表的逆向输出
从尾到头打印单链表一共有两种方法,第一种是将单链表进行反转操作,然后再遍历输出,但这种方法会破坏原来单链表的结构,如果遇上非常长的单链表,反转操作会非常占空间;第二种就是将单链表的结点依次压入栈中,利用栈后进先出的特点来实现逆序打印的效果!
// 单链表的逆向打印
public void reversePrint() {
if (head.next == null) {
System.out.println("链表为空!");
}
Stack<Node> stack = new Stack<>();
Node pNode = head.next;
while(pNode != null) {
stack.push(pNode); // 将结点入栈
pNode = pNode.next;
}
while(stack.size() > 0) {
System.out.println(stack.pop());
}
}
扩展:按编号顺序合并两个单链表
将两个链表按编号顺序合并为一个链表,并打印输出:
- 在此之前,需要一个获取头结点的方法getHead(),因为在上述代码实例中,我们设置了一个不存数据的头结点,所以getHead()方法返回的应该是head.next。并创建方法,参数为两个链表的头结点head1、head2。
- 在合并之前进行判断,若其中一个单链表为空,则返回另一个单链表。
- 设置两个辅助结点headNode和tailNode,一开始指向head,然后将head1和head2的id进行比较,如果head1.id<head.id,就将辅助结点headNode.next和tailNode指向head1结点,然后将head1结点后移,即head1 = head1.next。
- 建立循环,循环结束条件为两个链表其中一个链表为空,循环体内,对head1和head2的id进行比较,如果head1.id<head.id,将tailNode.next指向head1,并让tailNode指向head1,随后将head1后移,反之亦然。
- 当其中一个链表为空时,例如此时head1.next=null时,说明另一个链表还有数据没有加入,于是将tailNode.next指向该链表,即tailNode.next=head2。
// 按编号顺序组合两个链表
public void combine(Node head1, Node head2) {
// 如果其中一个链表为空,则返回另一个链表
if (head1 == null) {
head.next = head2;
return;
}
if (head2 == null) {
head.next = head1;
return;
}
Node headNode = head;
Node tailNode = head;
// 将辅助头结点和辅助尾结点指向id最小的头结点
if (head1.id < head2.id) {
headNode.next = head1;
tailNode = head1;
head1 = head1.next;
} else {
headNode.next = head2;
tailNode = head2;
head2 = head2.next;
}
// 将id比较小的结点添加至尾结点
while(head1.next != null || head2.next != null) {
if (head1.id < head2.id) {
tailNode.next = head1;
tailNode = head1;
head1 = head1.next;
} else {
tailNode.next = head2;
tailNode = head2;
head2 = head2.next;
}
if (head1 == null) {
tailNode.next = head2;
}
if(head2 == null) {
tailNode.next = head1;
}
}
}
带头结点的双向链表实现
双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。一般我们都构造双向循环链表。
单向链表与双向链表的差异:
-
单向链表,只能在一个方向里查找,而双向链表可以向前或者向后查找。
-
单项链表不能自我删除(利用辅助结点,通过遍历找到待删除结点的前一个结点),但双向链表可以。
双向链表的修改结点和遍历
因为修改结点和遍历都不需要进行指针域的修改操作,所以双向链表的修改结点和遍历操作和单向链表的操作是一模一样的!
// 修改结点
public void updata(Node newNode) {
if (size == 0) {
System.out.println("链表为空");
return;
}
Node pNode = head.next;
while(true) {
if (pNode == null) {
System.out.println("未找到该结点~");
break;
}
if (newNode.id == pNode.id) {
pNode.name = newNode.name;
pNode.price = newNode.price;
break;
}
pNode = pNode.next;
}
}
// 遍历双向链表
public void display() {
if (size == 0) {
System.out.println("链表为空!");
return;
}
Node pNode = head.next;
while(true) {
if (pNode == null) {
break;
}
System.out.println(pNode);
pNode = pNode.next;
}
}
}
双向链表的插入结点(在尾部插入和在指定位置插入)
由于双向链表对于单向链表而言,多了一个指针域pre,指向前一个结点,所以在进行插入操作的时候,需要执行指针域pre的修改操作:
-
在尾部插入结点,一样需要一个辅助结点pNode,通过遍历,找到双向链表的最后一个结点,将最后一个结点的next指向新结点,即pNode.next=newNode,不要忘了还有pre指针域,我们需要将新结点的pre指向最后一个结点,即newNode.pre=pNode。
-
在指定位置插入结点,和单向链表插入结点的操作比较相似,我们需要通过辅助结点找到插入结点位置的前一个结点,即pNode.next.id > newNode.id,然后先将新结点的next域和pre域分别指向后一个结点和结点,即newNode.next=pNode.next;
newNode.pre=pNode;然后再将前一个结点的next域和后一个结点的pre域指向新结点,即pNode.next.pre=newNode;pNode.next=newNode;不过需要注意的是,当插入的结点正好是最后一个结点时,可以直接执行尾部插入结点的方法,然后循环即可~// 添加结点 public void add(Node node) { Node pNode = head; while (true) { if (pNode.next == null) { break; } pNode = pNode.next; } pNode.next = node; node.pre = pNode; size++; } // 在指定位置添加结点 public void addOrderBy(Node newNode) { Node pNode = head.next; while(true) { if (pNode == null) { break; } // 当插入的新结点正好是链表的最后一个结点时 if (pNode.next.id < newNode.id) { add(newNode); break; } if (pNode.next.id > newNode.id) { newNode.next = pNode.next; newNode.pre = pNode; pNode.next.pre = newNode; pNode.next = newNode; size++; break; } if (pNode.next.id == newNode.id) { System.out.println("编号已被占用!"); break; } pNode = pNode.next; } }
双向链表的删除操作
因为双向链表可以向前向后两个方向进行查找,所以它的删除操作并不需要辅助结点找到前一个结点进行删除,而是调用待删除结点的指针域进行删除操作即可,也就是所说的自我删除!
- 利用辅助结点,通过遍历找到待删除的结点,将待删除结点的前一个结点的next域指向后一个结点,即pNode.pre.next=pNode.next;然后再将后一个结点的pre域指向前一个结点,即pNode.next.pre = pNode.pre。
- 需要注意的是,当删除的结点正好是最后一个结点时,它的后一个结点为null,执行以上所述的删除操作时,会报空指针异常,所以将后一个结点的pre域指向前一个结点之前,需要加上一个判断,即pNode.next!=null时,而删除的结点正好是第一个结点时,也需要加上一个判断,不过我们这里采用的是带头结点的双向链表,头结点必定不为空,所以免去了这层判断!
// 删除结点
public void delete(int id) {
if (size == 0) {
System.out.println("链表为空");
return;
}
// 这里的删除操作和单链表的删除操作不尽相同
// 单链表的删除使用的辅助结点指向头结点,因为我们希望辅助结点可以找到待删除结点的前一个结点
// 双向链表的删除并不需要找到前一个结点,所以我们可以直接在第一个结点开始查找
Node pNode = head.next;
while(true) {
if (pNode == null) {
System.out.println("未找到该结点~");
break;
}
if (pNode.id == id) {
pNode.pre.next = pNode.next;
if (pNode.next != null) {
pNode.next.pre = pNode.pre;
}
size--;
break;
}
pNode = pNode.next;
}
}