2.线性表—单链表

来源:互联网 发布:网络拓扑图的网络设备 编辑:程序博客网 时间:2024/06/05 06:02

1.链式存储结构实现

单链表和双链表(这边讲单链表)。

 

2.基础概念

a.结点:结点由数据域和地址域(链)两部分组成。而结点整体在效果上可以看作是该结点的地址(指针)。
这个地址域一般是后继元素的地址(即下一个结点的总体)。所以最后一个元素的地址域为^,其表示空,即没有后续元素。
b.单链表:每个结点只有一个地址域的线性链表称为单链表。
c.双链表:每个结点有两个地址域的线性表链称为双链表,两个地址域分别指向
前驱元素和后继元素。

 

3.单链表的实现


线性表接口LList:

package com.clarck.datastructure.linked;

/**
* 线性表接口LList,描述线性表抽象数据类型,泛型参数T表示数据元素的数据类型

* @author clarck
*
*/
public interface LList<T> {
/**
* 判断线性表是否空
* @return
*/
boolean isEmpty();

/**
* 返回线性表长度
* @return
*/
int length();

/**
* 返回第i(i≥0)个元素
* @param i
* @return
*/
T get(int i);

/**
* 设置第i个元素值为x
* @param i
* @param x
*/
void set(int i, T x);

/**
* 插入x作为第i个元素
* @param i
* @param x
*/
void insert(int i, T x);

/**
* 在线性表最后插入x元素
* @param x
*/
void append(T x);

/**
* 删除第i个元素并返回被删除对象
* @param i
* @return
*/
T remove(int i);

/**
* 删除线性表所有元素
*/
void removeAll();

/**
* 查找,返回首次出现的关键字为key元素
* @param key
* @return
*/
T search(T key);
}


单链表结点类:

package com.clarck.datastructure.linked;

/**
* 单链表结点类,T指定结点的元素类型

* @author clarck
*
* @param <T>
*/
public class Node<T> {
/**
* 数据域,保存数据元素
*/
public T data;

/**
* 地址域,引用后继结点
*/
public Node<T> next;

/**
* 构造结点,data指定数据元素,next指定后继结点

* @param data
* @param next
*/
public Node(T data, Node<T> next) {
this.data = data;
this.next = next;
}

/**
* 构造节点
*/
public Node() {
this(null, null);
}

/**
* 返回结点元素值对应的字符串
*/
@Override
public String toString() {
return this.data.toString();
}

/**
* 比较两个结点值是否相等,覆盖Object类的equals(obj)方法
*/
@SuppressWarnings("unchecked")
@Override
public boolean equals(Object obj) {
return obj == this || obj instanceof Node && this.data.equals(((Node<T>)obj).data);
}

}

 

线性表的链式表示和实现:

package com.clarck.datastructure.linked;

/**
* 线性表的链式表示和实现 带头结点的单链表类,实现线性表接口

* @author clarck

* @param <T>
*/
public class SinglyLinkedList<T> implements LList<T> {
/**
* 头指针,指向单链表的头结点
*/
public Node<T> head;

/**
* 默认构造方法,构造空单链表
*/
public SinglyLinkedList() {
// 创建头结点,data和next值均为null
this.head = new Node<T>();
}

/**
* 由指定数组中的多个对象构造单链表。采用尾插入构造单链表
* 若element==null,Java将抛出空对象异常;若element.length==0,构造空链表

* @param element
*/
public SinglyLinkedList(T[] element) {
// 创建空单链表,只有头结点
this();
// rear指向单链表最后一个结点
Node<T> rear = this.head;
for (int i = 0; i < element.length; i++) {
rear.next = new Node<T>(element[i], null);
rear = rear.next;
}
}

/**
* 判断单链表是否空,O(1)
*/
@Override
public boolean isEmpty() {
return this.head.next == null;
}

/**
* 返回单链表长度,O(n), 基于单链表遍历算法
*/
@Override
public int length() {
int i = 0;
// p从单链表第一个结点开始
Node<T> p = this.head.next;
// 若单链表未结束
while (p != null) {
i++;
// p到达后继结点
p = p.next;
}
return i;
}

/**
* 返回第i(≥0)个元素,若i<0或大于表长则返回null,O(n)
*/
@Override
public T get(int i) {
if (i >= 0) {
Node<T> p = this.head.next;
for (int j = 0; p != null && j < i; j++) {
p = p.next;
}

// p指向第i个结点
if (p != null) {
return p.data;
}
}
return null;
}

/**
* 设置第i(≥0)个元素值为x。若i<0或大于表长则抛出序号越界异常;若x==null,不操作。O(n)
*/
@Override
public void set(int i, T x) {
if (x == null)
return;

Node<T> p = this.head.next;
for (int j = 0; p != null && j < i; j++) {
p = p.next;
}

if (i >= 0 && p != null) {
p.data = x;
} else {
throw new IndexOutOfBoundsException(i + "");
}
}

/**
* 插入第i(≥0)个元素值为x。若x==null,不插入。 若i<0,插入x作为第0个元素;若i大于表长,插入x作为最后一个元素。O(n)
*/
@Override
public void insert(int i, T x) {
// 不能插入空对象
if (x == null) {
return;
}

// p指向头结点
Node<T> p = this.head;
// 寻找插入位置
for (int j = 0; p.next != null && j < i; j++) {
// 循环停止时,p指向第i-1结点或最后一个结点
p = p.next;
}
// 插入x作为p结点的后继结点,包括头插入(i<=0)、中间/尾插入(i>0)
p.next = new Node<T>(x, p.next);
}

/**
* 在单链表最后添加x对象,O(n)
*/
@Override
public void append(T x) {
insert(Integer.MAX_VALUE, x);
}

/**
* 删除第i(≥0)个元素,返回被删除对象。若i<0或i大于表长,不删除,返回null。O(n)
*/
@Override
public T remove(int i) {
if (i >= 0) {
Node<T> p = this.head;
for (int j = 0; p.next != null && j < i; j++) {
p = p.next;
}

if (p != null) {
// 获得原对象
T old = p.next.data;
// 删除p的后继结点
p.next = p.next.next;
return old;
}
}
return null;
}

/**
* 删除单链表所有元素 Java将自动收回各结点所占用的内存空间
*/
@Override
public void removeAll() {
this.head.next = null;
}

/**
* 顺序查找关键字为key元素,返回首次出现的元素,若查找不成功返回null
* key可以只包含关键字数据项,由T类的equals()方法提供比较对象相等的依据
*/
@Override
public T search(T key) {
if (key == null)
return null;
for (Node<T> p = this.head.next; p != null; p = p.next)
if (p.data.equals(key))
return p.data;
return null;
}

/**
* 返回单链表所有元素的描述字符串,形式为“(,)”,覆盖Object类的toString()方法,O(n)
*/
@Override
public String toString() {
String str = "(";
for (Node<T> p = this.head.next; p != null; p = p.next) {
str += p.data.toString();
if (p.next != null)
str += ","; // 不是最后一个结点时后加分隔符
}
return str + ")"; // 空表返回()
}

/**
* 比较两条单链表是否相等
*/
@SuppressWarnings("unchecked")
@Override
public boolean equals(Object obj) {
if (obj == this)
return true;

if (obj instanceof SinglyLinkedList) {
SinglyLinkedList<T> list = (SinglyLinkedList<T>) obj;
return equals(this.head.next, list.head.next);
}
return false;
}

/**
* 比较两条单链表是否相等,递归方法

* @param p
* @param q
* @return
*/
private boolean equals(Node<T> p, Node<T> q) {
return p == null && q == null || p != null && q != null
&& p.data.equals(q.data) && equals(p.next, q.next);
}
}

 

测试类:

package com.clarck.datastructure.linked;

/**
* 单链表的测试

* @author clarck

*/
public class SinglyLinkedList_test {
public static void main(String args[]) {
SinglyLinkedList<String> lista = new SinglyLinkedList<String>();
for (int i = 0; i <= 5; i++)
lista.insert(i, new String((char) ('A' + i) + ""));
System.out.println("lista: " + lista.toString() + ",length()="
+ lista.length());
lista.set(3, new String((char) (lista.get(0).charAt(0) + 32) + ""));
lista.remove(0);
lista.remove(3);
System.out.println("lista: " + lista.toString());
}
}


测试结果:

lista: (A,B,C,D,E,F),length()=6
lista: (B,C,a,F)

 

提醒:实现源码下载链接http://www.cnblogs.com/tanlon/p/4027046.html

 

4.实现类具体说明

a.如何将结点连接起来形成单链表

1.建立结点(对象)
2.抓住箭头的作用,在建立兑现的时候的变量内部设立或者利用next变量来设立后继元素,从而连成一条链。


b.对于头指针的理解

1.next在等号左边,其为访问一个结点。我们首先要有箭头指向,而其他结点的指针就是next特性,指针就是帮我们?到对象,从而使用对象,所以其看起来和用起来都可以看作是对象,但如果说它就是对象,那就是错的。指针就是
帮我们访问结点的。
2.next在等号左边,其为指向对象,我们只需要把握箭头就好。因为那都不是访问对象,而且改变单链表的结构。对于双链表也是,改变双链表的结构的话把握好箭头指向就好。
(两者要分开,不要混淆)


c.怎么抓住箭头?

1.从左边指向右边。比如,p.next=q,那么箭头由p指向q。
2.如果一个出发点有多个终点,取最后一个终点为主。比如说,p.next=q; p.next=b;都是从p出发,但终点不同,取其终点为b,而指向q的箭头断掉。
注意:这也说明了,有时需要注意操作顺序的问题。比如说:单链表中p后面插入一个q结点,必须先q.next=p.next,之后才能p.next=q;因为如p.next=q先的话,那么之前的箭头断掉,就访问不了没插入之前p的后继结点了。


d.对于单链表的常见操作说明

1).单链表的遍历操作
思路:只需要知道一个结点就可以获得其他结点.
(遍历单链表是指从第一个结点开始,我们访问是通过指针去访问,头指针不能改变,为head,之后改变指针(跟地址域相同)一个一个去访问后面的元素,直到最后一个结点。)
注意:head在本类中直接用。

代码实现:
Node<T> p=head; //p从head指向的结点开始
while(p!=null) //当单链表未结束时
{
System.out.print(p.data.toString()+""); //执行访问p结点的相关操作
p=p.next; //p到达后继结点
}


2).单链表的插入操作
插入思路:就是自己确定新的结点进行连接。
一共有四种情况的插入
情况一和二:空表插入/头插入
思路:若单链表为空,插入一个结点,head指向被插入 的结点,这是空表插入;
若单链表非空,则在head结点之间插入q结点,插入后q结点成为单链表的第一个结点,head指向该结点,这是头插入。
(注意:这两种情况都将改变单链表的头指针head)
if(head==null){
head=new Node<T>(x,null); //空表插入
}
else
{
Node<T> q=new Node<T>(x,null); //头插入
q.next=head;
head=q;
}

注意:上述程序段可合并成一句,即”head=new Node<T>(x,head);",其包含了空表插入和头插入情况。

情况三和四:中间插入/尾插入
思路:先把x节点和下个结点连在一起,然后再和前一个结点连在一起。
(注意:中间插入和尾部插入都不会改变单链表的头指针head)
Node<T> q=new Node<T>(x,null);
q.next=p.next; //q的后继结点应是p的原后继结点
p.next=q; //q作为p的后继结点


3).单链表的删除操作
思路:就是自己确定新的结点连接。
根据被删除的结点的位置的不同,分为两种情况来了解:

情况一:头删除
思路:删除单链表第一个结点,只要是head指向其后继结点即可。
即:head=head.next;
(注意:如果该链表只有一个元素,即删除该结点后,单链表为空,执行完上述语句后,head为null。)

情况二:中间/尾删除
思路:设p指向单链表中除最后一个结点外的某个结点,只需要改变一个结点的next值
if(p.next!=null){
p.next=p.next.next;

 

4).排序单链表
d1.概念:就是各结点按照data域值递增或递减顺序链接。
d2.思路:
d3.代码实现:
public class SortedSinglyLinked<T extends Comparable<T>> extends SinglyLinkedList<T> 
{
//默默构造方法,调用父类默认构造方法
public SortedSinglyLinkedList(){ super();}
//将elements数组中所有对象插入构造排序的单链表,直接插入排序

public SortedSinglyLinkedList(T[] element)
{ super(); //创建空单链表,调用父类默认构造方法,默认调用
if(element!=null){
for(int i=0;i<element.length;i++){
this.insert(element[i]); //插入一个结点,根据值的大小决定插入位置
}
}
}

提醒:具体实现链接为:

http://www.cnblogs.com/tanlon/p/4027219.html

5).循环单链表
思路:把最后一个结点(对象)的next数据变为head,就可以完成循环,就变成了一条圆圈的单链表。
代码实例:
public class CirSinglyLinkedList<T>
{
public Node<T> head; //头指针,指向循环单链表的头结点(简单点说就是最后一个结点的next值为null,而它的地址就是null,达到循环的效果)

public CirSinglyLinkedList() //默认构造方法,构造空循环单链表
{
this.head=new Node<T>(); //创建头结点

this.head.next=this.head; 

}

public boolean isEmpty()

return this.head.next==this.head; 
} //判断循环单链表是否为空

public String toString() //返回所有元素的描述字符串
{
String str="(";
Node<T> p=this.head.next;
while(p!=this.head) //遍历单链表的循环条件改变了
{
str+=p.data.toString();
if(p!=this.head){
str+=","; 
} //不是最后一个结点时后加分隔符

p=p.next;
}
return str+")";
}
.........

提醒:具体实现链接为:

http://www.cnblogs.com/tanlon/p/4028264.html

 

6).比较单链表是否相等(对不同链表,特殊,上述都是在同一单链表进行的操作)
思路:一个一个数据进行比较,直到最后都为null.
方法如下:
public boolean equals(Object obj)
{
if(obj==this)
return true;
if(!(obj instanceof SinglyLinkedList))
reture false;
Node<T> p=this.head.next;
Node<T> q=(SinglyLinkedList<T>)obj.head.next;
while(p!=null&&q!=null&&p.data.equals(q.data))
{
p=p.next;
q=q.next;
}
return p==null&&q==null;
}

其覆盖了Object类的equals(obj)方法。


7).带头结点的单链表
a.头结点概念:就是指在单链表的第一个结点之前增加一个特殊的结点,称为头结点。
b.头结点的作用:1.是使所有链表(包括空表)的头指针非空。
2.使对单链表的插入,删除操作不需要区分是否为空表
3.不用管是否在第一个位置进行,从而与其他位置的插入,删除操作一致。

 

8).单链表操作的效率分析
a.常见方法的时间复杂度
a1.isEmpty(),insertAfter(p,x),removeAfter(p)的时间复杂度是O(l)。
a2. length(),get(i),set(i),insert(i,x),remove(i)的时间复杂度是O(n)。
(注意:后四种方法的时间复杂度取决于i,当i最大值时其时间复杂度为O(n))

b.对于单链表操作效率的理解:对比顺序表,其提高了运行效率和存储空间的利用率。
详细说明:
对单链表进行插入和删除操作,只需要改变少量结点的链,不需要移动数据元素。单链表中结点的
存储空间实在插入和删除过程中动态申请和释放的,不需要预先给单链表分配存储空间,从而避免了
顺序表因存储空间的不足扩充空间和复制元素的过程,提高了运行效率和存储空间的利用率。


9).提高单链表操作效率的措施
a.在某些需要使用长度的情况下,应该注意避免两次遍历单链表。比如:在单链表最后添加元素的append()方法(其中还用到了length())
b.如果在单链表类中增加某些私有成员变量,则可提高某些操作效率。


10).单链表的浅拷贝和深拷贝
浅拷贝:它只对单链表的头指针进行赋值,则导致有两个头指针,从而形成公用其余结点数据的两条单链表。

构造函数浅拷贝代码实现:
public SinglyLinkedList(SinglyLinkedList<T> list)
{
this.head=list.head;
}

深拷贝:仅仅复制单链表list的所有结点,但没有复制元素对象。

深度拷贝:不仅仅复制所有的结点,还复制了所有元素的对象。

构造函数深拷贝代码实现:
public SinglyLinkedList(SinglyLinkedList<T> list)
{
this(); //创建空单链表,只有头结点
Node<T> p=list.head.next; //若list==null,抛出空对象异常
Node<T> rear=this.head;
while(p!=null)
{
rear.next=new Node<T>(p.data,null);
rear=rear.next;
p=p.next;
}
}

 

11).单链表的应用----利用单链表解决约瑟夫问题。
思路:1.先把指针通过while方法转移到我们开始的元素
2.之后进行遍历,在遍历里面使用while方法,将指针转移到我们要使其出局但是仍然存在的元素,要做到这个只能将其flag改为0.
注意:只有flag为1,其count才能加1
3.如果flag为0,将指针指向下一个结点。重复直到只有一个元素跳出遍历。

代码实现:
public void yuesefu(Node head,int num,int start,int distance){
//head头结点,num为元素总数,start为开始的元素号(这个数我们得根据头结点和要求我们开始的元素的距离所共同决定的),distance为距离多少就删除的数字

Node p=head;
int i=1;
int count=1; //count的存在是为了让元素移到我们指定的距离的元素

//指向开始元素

while(i<start){
p=p.next;
}

//下面进行遍历
while(num!=1){
while(count<distance){
p=p.next;
if(p.flag=1){ count++;}
}
p.flag=0;
while(p.flag==0){ 
p=p.next;
num--}
//让count恢复原值
count=1;

}

//输出最后剩下的元素
System.out.println(p);

}