玩转数据结构:第5章 链表和递归
Java面试笔试宝典 - 第 8 章 数据结构与算法
8.1 链表
8.1.1 如何实现单链表的增删操作
链表作为最基本的(线性的、动态的)数据结构,在程序设计中有着非常重要的作用,其存储特点如下:可以用任意一组存储单元来存储单链表中的数据元素(存储单元可以是不连续的),而且,除了存储每个数据元素 ai的值以外,还必须存储指示其直接后继元素的信息。这两部分信息组成的数据元素 ai的存储映像称为结点。N 个结点链在一块被称为链表,当结点只包含其后继结点的信息的链表就被称为单链表,在内存中存储的方式如图 8-1 所示。
在 Java 语言中,可以定义如下的数据类来存储结点信息。
public class Node<E> {
Node next = null;
E data;
public Node(E data) {
this.data = data;
}
}
链表最重要的操作就是向链表中插入元素和从链表中删除元素。
单链表的插入操作是将值为 x 的新结点插入到单链表的第 i 个结点的位置上,即插入到数据元素 ai-1 与 ai 之间。其具体步骤如下:
1)找到 ai-1 的引用(存储位置)p。
2)生成一个数据域为 x 的新结点 s。
3)设置 p.next=s。
4)设置 s.next=ai。
图 8-2 为单链表插入结点示意图。
单链表的删除操作是将单链表的第 i 个结点删去。其具体步骤如下:
1)找到 ai-1的存储位置 p。
2)令 p.next 指向 ai的直接后继结点(即把 ai从链上摘下)ai+1。
图 8-3 为单链表删除结点示意图。
下面链表操作的示例给出了链表的基本操作。
package cn.bjut.content8.test1;
public class MyLinkedList {
Node head = null; //链表头的引用
/**
* 向链表中插入数据
* @param d:插入数据的内容
*/
public void addNode(int d){
Node newNode = new Node(d);
if(head == null){
head = newNode;
return;
}
Node tmp = head;
while (tmp.next!=null){
tmp = tmp.next;//
}
//add node to end
tmp.next = newNode;
}
public Boolean deleteNode(int index){
if(index<1 || index>length()){ //删除的位置不合理
return false;
}
//删除链表第一个元素
if(index == 1){
head = head.next;
return true;
}
int i = 2;
Node preNode = head;
Node curNode = preNode.next;
while (curNode!=null){
if(i==index){
preNode.next = curNode.next;
return true;
}
preNode = curNode;
curNode = curNode.next;
i++;
}
return true;
}
/**
* 返回结点的长度
* @return
*/
public int length(){
int length=0;
Node tmp = head;
while (tmp!=null){
length ++;
tmp=tmp.next;
}
return length;
}
/**
* 对链表进行排序
* 返回排序后的头结点
*/
public Node orderList(){
Node nextNode = null;
int temp =0;
Node curNode=head;
while(curNode.next!=null){
nextNode =curNode.next;
while (nextNode!=null){
if((int)curNode.data > (int)nextNode.data){
temp = (int)curNode.data;
curNode.data=nextNode.data;
nextNode.data=temp;
}
nextNode=nextNode.next;
}
curNode=curNode.next;
}
return head;
}
public void printList(){
Node tmp = head;
while (tmp!=null){
System.out.println(tmp.data);
tmp=tmp.next;
}
}
public static void main(String[] args) {
MyLinkedList list = new MyLinkedList();
list.addNode(5);
list.addNode(3);
list.addNode(1);
list.addNode(3);
System.out.println("listLen"+list.length());
System.out.println("before order");
list.printList();
list.orderList();
System.out.println("after order");
list.printList();
}
}
上述程序运行结果为:
以上这个例子主要实现了链表的最基本的操作,这些操作包括给链表增加结点(每次都把新增加的结点加到链表尾部)和删除链表中的结点和计算链表的长度。此外还通过插入排序算法实现了对链表的排序。
8.1.2 如何从链表中删除重复数据
如何从链表中删除重复数据,最容易想到的方法就是遍历链表,把遍历到的值存储到一个 Hashtable 中,在遍历过程中,若当前访问的值在 Hashtable 中已经存在,则说明这个数据是重复的,因此就可以删除。具体实现代码如下:
/**
* 删除链表中重复结点
* @param head
*/
public void deleteDuplecate(Node head){
Hashtable<Object,Integer> table=new Hashtable<>();
Node tmp = head;
Node pre = null;
while (tmp!=null){
if(table.containsKey(tmp.data))
pre.next=tmp.next;
else {
table.put(tmp.data,1);
pre = tmp;
}
tmp =tmp.next;
}
}
以上这种方法的优点是时间复杂度较低,但也有一个很明显的缺点,就是在遍历过程中需要额外的存储空间来保存已遍历过的值。是否还有更加高效的算法呢?下面介绍另外一种不需要额外存储空间的算法。
这种方法的主要思路为对链表进行双重循环遍历,外循环正常遍历链表,假设外循环当前遍历的结点为 cur,内循环从 cur 开始遍历,若碰到与 cur 所指向结点值相同,则删除这个重复结点。算法的实现方法如下:
以上方法的优点是不需要额外的存储空间,缺点也很明显,时间复杂度比上面介绍的算法的时间复杂度高。假设外循环当前遍历的结点为 cur,内循环在遍历的过程中会删除掉与 cur 结点值相同的所有结点。在实现时还可以采用另外一种实现方法:外循环当前遍历的结点为 cur,内循环从链表头开始遍历至 cur,只要碰到与 cur 值相同的结点就删除该结点,同时内循环结束,因为与 cur 相同的结点只可能存在一个(如果存在多个,在前面的遍历过程中已经被删除了)。采用这种方法在特定的数据发布的情况下会提高算法的时间复杂度。
8.1.3 如何找出单链表中的倒数第 k 个元素
为了找出单链表中的倒数第 k 个元素,最容易想到的方法是首先遍历一遍单链表,求出整个单链表的长度 n,然后将倒数第 k 个,转换为正数第 n-k 个,接下去遍历一次就可以得到结果。但是该方法存在一个问题,即需要对链表进行两次遍历,第一次遍历用于求解单链表的长度,第二次遍历用于查找正数第 n-k 个元素。
显然,以上这种方法还可以进行优化。于是想到了第二种方法,如果沿从头至尾的方向从链表中的某个元素开始,遍历 k 个元素后刚好达到链表尾,那么该元素就是要找的倒数第 k 个元素,根据这一性质,可以设计如下算法:从头结点开始,依次对链表的每一个结点元素进行这样的测试,遍历 k 个元素,查看是否到达链表尾,直到找到那个倒数第 k 个元素。此种方法将对同一批元素进行反复多次的遍历,对于链表中的大部分元素而言,都要遍历 k 个元素,如果链表长度为 n,该算法时间复杂度为 O(kn)级,效率太低。
存在另外一种更高效的方式,只需要一次遍历即可查找到倒数第 k 个元素。由于单链表只能从头到尾依次访问链表的各个结点,因此,如果要找出链表的倒数第 k 个元素的话,也只能从头到尾进行遍历查找,在查找过程中,设置两个指针,让其中一个指针比另一个指针(虽然 Java 语言没有指针的概念,但是引用与指针有着非常相似的性质。为了便于理解,在后续的介绍中都采用指针的概念来介绍)先前移 k-1 步,然后两个指针同时往前移动。循环直到先行的指针值为 NULL 时,另一个指针所指的位置就是所要找的位置。程序代码如下:
public Node findElemDescK(Node head,int k){
if(k<1)
return null;
Node p1 = head;
Node p2 = head;
for(int i =0;i<k-1 && p1!=null;i++)//前移k-1步
p1 = p1.next;
if(p1 == null){
System.out.println("k不合法");
return null;
}
while (p1.next !=null){
p1 = p1.next;
p2 = p2.next;
}
return p2; //找出单链表中的倒数第 k 个元素
}
8.1.4 如何实现链表的反转
为了正确地反转一个链表,需要调整指针的指向,而与指针操作相关代码总是非常容易出错的。先举个例子看一下具体的反转过程,例如,i,m,n 是 3 个相邻的结点,假设经过若干步操作,已经把结点 i 之前的指针调整完毕,这些结点的 next 指针都指向前面一个结点。现在遍历到结点 m,当然,需要调整结点的 next 指针,让它指向结点 i,但需要注意的是,一旦调整了指针的指向,链表就断开了,因为已经没有指针指向结点 n,没有办法再遍历到结点 n 了,所以为了避免链表断开,需要在调整 m 的 next 之前要把 n 保存下来。接下来试着找到反转后链表的头结点,不难分析出反转后链表的头结点是原始链表的尾结点,即 next 为空指针的结点。下面给出非递归方法实现链表的反转的实现代码。
//8.1.4 如何实现链表的反转
public void ReverseIteratively(Node head){
Node pReversedHead = head;
Node pNode = head;
Node pPrev = null;
while(pNode !=null){
Node pNext=pNode.next;
if(pNext==null)
pReversedHead=pNode;
pNode.next=pPrev;
pPrev=pNode;
pNode=pNext;
}
this.head=pReversedHead;
}