单链表和双链表是链表的两种分类,Day2来实现这两种链表。
首先说一说单链表。
单链表是线性表的升级版,至于线性表的内容,戳一戳这里就懂了(书接上回线性表)。单链表中数据存储的基本单元叫做节点,一个节点又包括数据data和指针next。节点里的数据存储的是该节点的数据,而指针存储的是下一个节点的地址。由于节点中本身就存储了下一个节点的地址,因此单链表的存储不同于线性表的数组结构,单链表的各节点不必通过数组下标相连。不必相连既表现出单链表通过指定元素增改数据、不浪费空间的优点,但又因为不使用下标,也暴露了不容易通过指定第几号元素获取数据的缺点。
实现单链表首先要定义一个节点:
public class ListNode {
Object data;
ListNode next;
public ListNode(Object data) {
this.data = data;
}
}
由于单链表没有数组来控制链表的长度,因此需要定义头元素和尾元素实现上述功能。
在实现链表的增加、删除(按指定元素或下标删除)、更新、判断指定元素是否存在、获取指定位数数据、获取指定数据在第几位等功能时,就可以用头元素作为指针,利用尾元素控制单链表的结束点。(方法实现要使用MyList接口,接口在这里书接上回线性表)
实现删除方法或许不容易想到,迷惑点在于:指针挪到删除的目标元素时,我们无法得知目标元素的上一个地址。这就是单链表的缺点:只能单向扫描,为了改进这一缺点,接下来会引出双链表。但是,在这里,我们仍需要想办法在单链表中解决这个问题。解法就是:在重新定义一个指针,让这个指针指向原指针的上一位,具体方法见下面的代码。这样,我们就可以在找到目标值后,用新指针指向就指针的next,实现了目标元素的上一个节点与下一个相连。实现后,立即退出循环。
可能有些初学者会迷惑:指针定义时是局部变量,改变指向后,用删除方法外的其他方法打印单链表元素,居然不会报错?!这与Java的垃圾回收机制有关,在新指针越过目标指向下一个元素是,目标 元素就不在被使用,也就会被回收。在此之后调用其他方法,这一元素也就不再能访问,也就是实现了删除。
单链表类定义:
public class SingleLinkedList implements MyList {
private ListNode head;//头节点
private ListNode tail;//尾节点
@Override
public void add(Object element) {
if(head==null){//初始化:处理原始为空节点的情况
head=new ListNode(element);//头节点获取data和地址
tail=head;//此时只有一个节点,所以头尾节点同,同时初始化了尾节点
}else {
tail.next=new ListNode(element);//尾元素的指针指向下一个元素
tail=tail.next;//尾元素更新为新增元素
}
}
@Override
public void delete(Object element) {
ListNode p=head;
ListNode pre=p;
while(p!=null){
if(p.data.equals(element)){
if(head.data.equals(element)) head=head.next;
else pre.next=p.next;
break;
}
pre=p;
p=p.next;
}
}
@Override
public void delete(int index) {
ListNode p=head;
for(int i=0;i<index-1;i++){
p=p.next;
}
p.next=p.next.next;
}
@Override
public void update(int index, Object element) {
ListNode p=head;
if(index==0) head.data=element;
else {
for(int i=0;i<index-1;i++){
p=p.next;
}
p.next.data=element;
}
}
@Override
public boolean contains(Object element) {
ListNode p=head;
while (p!=null){
if(p.data.equals(element)) return true;
p=p.next;
}
return false;
}
@Override
public int indexOf(Object element) {
int i=0;
ListNode p=head;
while (p!=null){
if(p.data.equals(element)) return i;
i++;
p=p.next;
}
return -1;
}
@Override
public Object at(int index) {
ListNode p=head;
int size=0;
while(p!=null){
size++;
p=p.next;
}
if(index>=size) return "so sad, not here";
p=head;
for(int i=0;i<index;i++){
p=p.next;
}
return p.data;
}
//定义一个输出方法,便于检测
public void outPut(){
System.out.print("[");
ListNode p=head;
while(p!=null){
if(p.next!=null) System.out.print(p.data+" ");
else System.out.print(p.data);
p=p.next;
}
System.out.println("]");
}
}
下面进行测试:
class SingleLinkedListTest {
SingleLinkedList list=new SingleLinkedList();
@org.junit.jupiter.api.Test
void add() {
list.add("a");
list.add("b");
list.add("c");
list.add("d");
list.outPut();
}
@org.junit.jupiter.api.Test
void delete() {
list.add("a");
list.add("b");
list.add("c");
list.add("d");
list.outPut();
list.delete("c");
list.outPut();
}
@org.junit.jupiter.api.Test
void testDelete() {
list.add("a");
list.add("b");
list.add("c");
list.add("d");
list.delete(2);
list.outPut();
}
@org.junit.jupiter.api.Test
void update() {
list.add("a");
list.add("b");
list.add("c");
list.add("d");
list.update(1,"n");
list.outPut();
}
@org.junit.jupiter.api.Test
void contains() {
list.add("a");
list.add("b");
list.add("c");
list.add("d");
System.out.println(list.contains("n"));
System.out.println(list.contains("c"));
}
@org.junit.jupiter.api.Test
void indexOf() {
list.add("a");
list.add("b");
list.add("c");
list.add("d");
System.out.println(list.indexOf("b"));
System.out.println(list.indexOf("n"));
}
@org.junit.jupiter.api.Test
void at() {
list.add("a");
list.add("b");
list.add("c");
list.add("d");
System.out.println(list.at(1));
System.out.println(list.at(10));
}
}
运行结果:
[a b c d]
[a b d]
[a n c d]
false
true
b
so sad, not here
[a b c d]
[a b d]
1
-1
Process finished with exit code 0
单链表到这里就结束啦。
还记得单链表只能单向扫描的问题吗?双链表就可以解决这一问题:相比单链表,双链表的节点中,多封装了pre指针,这个指针的作业就是指向前一个节点的地址。pre的存在,双链表也就实现了双向扫描的功能。
双链表节点的封装只需要多加一行:
ListNode pre;
双链表的实现也需要定义头节点和尾节点,原因与单链表大致相同。得益于双向扫描,双链表的头尾节点并不需要存储数据,我们只需要将数据塞进头尾之间即可。由于不存储数据,头尾节点又叫做“哑元”。
如法炮制定义一个双链表类:
public class DoubleLinkedList implements MyList {
//设置头尾哑元,操作哑元中间的元素
ListNode head=new ListNode(null);
ListNode tail=new ListNode(null);
public DoubleLinkedList() {
tail.pre=head;
head.next=tail;
}
@Override
public void add(Object element) {
ListNode newNode=new ListNode(element);
tail.pre.next=newNode;
newNode.next=tail;
newNode.pre=tail.pre;
tail.pre=newNode;
}
@Override
public void delete(Object element) {
ListNode p=head.next;
while (p!=tail){
if(p.data.equals(element)) {
p.pre.next=p.next;
p.next.pre=p.pre;
break;
}
p=p.next;
}
}
@Override
public void delete(int index) {
ListNode p=head.next;
for(int i=0;i<index;i++){
p=p.next;
}
p.pre.next=p.next;
p.next.pre=p.pre;
}
@Override
public void update(int index, Object element) {
ListNode p=head.next;
for (int i=0;i<index;i++){
p=p.next;
}
p.data=element;
}
@Override
public boolean contains(Object element) {
ListNode p=head.next;
while (p!=tail){
if(p.data.equals(element)) return true;
p=p.next;
}
return false;
}
@Override
public int indexOf(Object element) {
int i=0;
ListNode p=head.next;
while(p!=tail){
if(p.data.equals(element)) return i;
i++;
p=p.next;
}
return -1;
}
@Override
public Object at(int index) {
ListNode p=head.next;
int n=0;
while (p!=tail){
n++;
p=p.next;
}
if(index>=n) return "so sad, not here";
p=head.next;
for(int i=0;i<index;i++){
p=p.next;
}
return p.data;
}
public void outPut(){
System.out.print("[");
ListNode p=head.next;
while(p!=tail){
if(p.next.data!=null) System.out.print(p.data+" ");
else System.out.print(p.data);
p=p.next;
}
System.out.println("]");
}
}
为了使头尾节点产生联系,要加入一个双链表构造器,让头的next指向尾,尾的pre指向头。
增删方法的定义也与单链表有所不同。先说一说增加:大致思路就是要创建一个新节点,把增加的元素传进新节点,然后把新节点塞进尾节点和上一个节点之间。至于怎么塞进去,就需要考虑一下顺序了:先把尾节点的pre的next指向新节点(搞断尾与上个节点的联系),然后让新节点的next指向尾(建立新节点和尾的联系),最后让新节点的pre指向尾的pre+尾的pre指向新节点。这样做很妙,有点擒贼先擒王的感觉了,切断最重要的,建立目标的,然后再慢慢维护其他的。这里其实也能看出双链表的缺点的,指针多了,维护起来就有点复杂。
终于,来到了测试环节:
class DoubleLinkedListTest {
DoubleLinkedList list=new DoubleLinkedList();
@Test
void add() {
list.add("a");
list.add("b");
list.add("c");
list.add("d");
list.outPut();
}
@Test
void delete() {
list.add("a");
list.add("b");
list.add("c");
list.add("d");
list.outPut();
list.delete("b");
list.outPut();
list.delete("a");
list.outPut();
}
@Test
void testDelete() {
list.add("a");
list.add("b");
list.add("c");
list.add("d");
list.outPut();
list.delete(0);
list.outPut();
list.delete(2);
list.outPut();
}
@Test
void update() {
list.add("a");
list.add("b");
list.add("c");
list.add("d");
list.outPut();
list.update(0,"n");
list.outPut();
list.update(2,"k");
list.outPut();
}
@Test
void contains() {
list.add("a");
list.add("b");
list.add("c");
list.add("d");
list.outPut();
System.out.println(list.contains("a"));
System.out.println(list.contains("k"));
}
@Test
void indexOf() {
list.add("a");
list.add("b");
list.add("c");
list.add("d");
list.outPut();
System.out.println(list.indexOf("a"));
System.out.println(list.indexOf("c"));
System.out.println(list.indexOf("k"));
}
@Test
void at() {
list.add("a");
list.add("b");
list.add("c");
list.add("d");
list.outPut();
System.out.println(list.at(0));
System.out.println(list.at(2));
System.out.println(list.at(9));
}
}
测试结果:
[a b c d]
[a c d]
[c d]
[a b c d]
[n b c d]
[n b k d]
[a b c d]
true
false
[a b c d]
a
c
so sad, not here
[a b c d]
[a b c d]
[b c d]
[b c]
[a b c d]
0
2
-1
Process finished with exit code 0