2 线性表
线性表是由n(n>=0)个具有相同数据类型的数据元素所构成的有限序列。当n=0时,此线性表是空表。
线型表是一种线性结构,线性结构的特征:
1)第一个数据元素没有前驱,这个数据元素也称为开始节点。
2)最后一个数据元素没有后继,这个数据元素也称为终端节点。
3)出第一个和最后一个数据元素之外,其他的数据元素有且仅有一个前驱和后继。
2.1 线性表的抽象数据类型描述
线性表的抽象数据Java接口描述如下:
package adt;
/**
* 线性表的抽象数据类型描述
*
* @author Oner.wv
*
*/
public interface IList {
/**
* 将一个已经存在的表置为空表。
*/
public void clear();
/**
* 判断线性表是否为空。
*/
public boolean isEmpty();
/**
* 返回线性表中的数据元素个数。
*/
public int length();
/**
* 读取并返回线性表中的第i个数据元素的值。0≤ i ≤length()-1。
*
* @throws Exception
*/
public Object get(int i) throws Exception;
/**
* 返回线性表中首次出现指定数据元素的为序号,若线性表中不包含此数据元素,则返回-1
*/
public int indexOf(Object x);
/**
* 在线性表中第i个数据元素之前插入一个值为x的数据元素。0≤ i ≤length()。
* i=0时,在表头插入x;i=length()时,在表尾插入x。
*
* @throws Exception
*/
public void insert(int i, Object x) throws Exception;
/**
* 删除线性表中第i个数据元素。0≤ i ≤length()-1。
*
* @throws Exception
*/
public void remove(int i) throws Exception;
/**
* 输出线性表中各个数据元素。
*/
public void display();
}
2.2 线性表的顺序存储及其实现
2.2.1 线性表的顺序存储
顺序表是在计算机内存中以数组的形式保存的线性表,是指用一组地址连续的存储单元依次存储数据元素的线性结构。线性表采用顺序存储的方式存储就称之为顺序表。顺序表是将表中的结点依次存放在计算机内存中一组地址连续的存储单元中。线性表的顺序存储示意图:
采用顺序存储结构的线性表简称为“ 顺序表”。顺序表的存储特点是:只要确定了起始位置,表中任一元素的地址都通过下列公式得到:
Loc(ai)=Loc(a0)+i*c 0<=i<=n-1。
Loc(a0)代表a0的存储地址,c代表每个数据元素所占的存储单元。
2.2.2 顺序表类的实现
package sequenceList;
import adt.IList;
/**
* 顺序表类的描述
*
* @author Oner.wv
*
*/
public class SqList implements IList {
private Object[] listElem;
private int curLen;
public SqList(int maxSize) {
curLen = 0;// 设置顺序表的当前长度为0
listElem = new Object[maxSize];// 为顺序表分配maxSize个存储单元
}
@Override
public void clear() {
curLen = 0;
}
@Override
public boolean isEmpty() {
return curLen == 0;
}
@Override
public int length() {
return curLen;
}
@Override
public Object get(int i) throws Exception {
if (i < 0 || i > curLen - 1) {// 判断i是否合法
throw new Exception("第" + i + "个元素不存在");
}
return listElem[i];// 返回顺序表中第i个数据元素
}
@Override
public int indexOf(Object x) {
int j = 0;// j指示顺序表中待比较的数据元素,其初始值指示顺序表中第0个数据元素
while (j < curLen && !listElem[j].equals(x)) {// 依次比较
j++;
}
if (j < curLen) {// 判断j的位置是否位于顺序表中的位置
return j;
} else {
return -1;// 值为x的数据元素在顺序表中不存在
}
}
@Override
public void insert(int i, Object x) throws Exception {
if (curLen == listElem.length) {// 判断顺序表是否已满
throw new Exception("顺序表已满");
}
if (i < 0 || i > curLen) {// 判断i是否合法
throw new Exception("插入位置不合法");
}
for (int j = curLen; j > i; j--) {
listElem[j] = listElem[j - 1];// 插入位置及其之后的所有数据元素后移一位。
}
listElem[i] = x;// 插入x
curLen++;// 表长加1
}
@Override
public void remove(int i) throws Exception {
if (i < 0 || i > curLen - 1) {// 判断i是否合法
throw new Exception("删除位置不合法");
}
for (int j = i; j < curLen - 1; j++) {
listElem[j] = listElem[j + 1];// 被删除元素之后的所有元素左移一个存储空间
}
curLen--;// 表长减1
}
@Override
public void display() {
for (int j = 0; j < curLen; j++) {
System.out.print(listElem[j] + " ");
}
System.out.println();// 换行
}
}
2.3 线性表的链式存储及其实现
2.3.1单链表的表示
采用链式存储方式存储的线性表成为链表,链表中每一个节点包含存放数据元素值的数据域和存放逻辑上相邻结点的指针域。 若一个节点中只包含一个指针域,则称此链表为单链表(Single Linked List)。链表的节点结构如下:
线性表的单链表类的存储示意图:
2.3.2 节点类的实现
package linkedList;
/**
* 节点类的描述
*
* @author Oner.wv
*
*/
public class Node {
public Object data;// 存放节点值
public Node next;// 后继结点的引用
public Node() {
this(null, null);
}
public Node(Object data) {
this(data, null);
}
public Node(Object data, Node next) {
this.data = data;
this.next = next;
}
}
2.3.3 单链表类的实现
1. 插入操作
单链表上的插入操作的基本要求是在带头结点的单链表上的第i个节点之前插入一个数值域为x的新节点,其中i的限制条件是:0<=i<=n。要想实现插入操作,必须先确定第i个节点的前驱结点,即第i-1个节点的位置。假设p指针指向第i-1个节点,s指针指向新节点,则可画出一个插入操作的示意图:
实现思路:
1)查找到带插入位置的前驱结点
2)创建数据域值为x的新节点
3)修改相关节点的指针域从而使新节点插入到单链表中给定的位置上
实现代码:
public void insert(int i, Object x) throws Exception {
Node p = head.next;// 将p指向首节点
int j = 0;
while (p != null && j < i - 1) {// 寻找第i个节点的前驱,即第i-1个节点
p = p.next;
j++;
}
if (i < 0 || p == null) {
throw new Exception("插入的位置不合法");
}
Node s = new Node(x);// 生成新节点
s.next = p.next;// 将第i个节点的地址赋值给新生成节点的指针域
p.next = s;// 将新节点的地址赋值给第i-1个节点的指针域
}2.删除操作
单链表上删除操作的基本要求是删除带头结点的单链表上的第i个节点,i的限制条件是:0<=i<=n-1。所以,与插入操作相同,从单链表中删除一个节点也需要先查找到被删除节点的前驱节点,然后通过修改它的前驱结点的后继指针值来实现线性表中逻辑关系的改变。示例图如下:
实现思路:
1)查找到待删除节点的前驱结点
2)修改链指针,使待删除节点从单链表中脱离出来
实现代码:
public void remove(int i) throws Exception {
Node p = head.next;
int j = 0;
/**
* p.next!=null限定了p最多只能指向尾节点,即第n-1个节点 。j<i-1限定了p最多只能指向第i-1个节点,即第i个节点的前驱
*/
while (p.next != null && j < i - 1) {
p = p.next;
j++;
}
if (i < 0 || p.next == null) {
throw new Exception("第" + i + "个元素不存在");
}
p.next = p.next.next;// 修改链指针,使待删除节点从单链表中脱离出来。
}全部代码实现:
package linkedList;
import java.util.Scanner;
import adt.IList;
/**
* 单链表类的描述
*
* @author Oner.wv
*
*/
public class LinkList implements IList {
private Node head;// 单链表的头指针
public LinkList() {// 单链表的构造函数
head = new Node();// 初始化头结点
}
public LinkList(int n, boolean order) throws Exception {
this();// 初始化头结点
if (order) {
create1(n);// 用尾插法顺序建立单链表
} else {
create2(n);// 用头插法逆位序建立单链表
}
}
public void create2(int n) throws Exception {
Scanner sc = new Scanner(System.in);
for (int j = 0; j < n; j++) {
insert(0, sc.next());
}
}
public void create1(int n) throws Exception {
Scanner sc = new Scanner(System.in);
for (int j = 0; j < n; j++) {
insert(length(), sc.next());
}
}
/**
* 将一个已经存在的带头结点单链表置为空表
*/
@Override
public void clear() {
head.data = null;// 设置头节点的数据域为null
head.next = null;// 设置头结点的指针域为null
}
/**
* 判断带头结点的单链表是否为空
*/
@Override
public boolean isEmpty() {
return head.next == null;// 判断头结点的指针域是否为null
}
/**
* 求带头结点的单链表的长度
*/
@Override
public int length() {
Node p = head.next;// 将p指向首节点
int length = 0;// length为计数器
/**
* 这里来探究下p!=null的作用:假设n为链表的长度,则尾节点为第n-1个节点,用Node(n-1)表示。
* 当p==Node(n-1)时,length==n-1,这时根据while循环的条件来确定是否执行下次循环,
* 由于此时p!=null,满足while循环的条件,所以继续执行下次循环。 此时p==Node(n-1).next,length==n,
* 因为尾节点的指针域的值为null,不满足while循环的条件,所以结束while循环
* 所以p!=null限定了p所取的最大值为Node(n-1).next,即尾节点的指针域。
*/
while (p != null) {
p = p.next;
length++;
}
return length;
}
@Override
public Object get(int i) throws Exception {
Node p = head.next;// 将p指向首节点
int j = 0;
/**
* p.next!=null限定了p最多只能指向尾节点, j<i限定了p最多只能指向第i个节点
*/
while (p.next != null && j < i) {// 寻找第i个节点
p = p.next;
j++;
}
if (i < 0 || p.next == null) {// i小于0或者大于表长减1时,即i不合法
throw new Exception("第" + i + "个元素不存在");
} else {
return p.data;// 返回p节点的数据域的值
}
}
@Override
public int indexOf(Object x) {
Node p = head.next;// 将p指向首节点
int j = 0;
/**
* p!=null限定了p最多指向尾节点的指针域, !p.data.equals(x)限定了p的数据域的值不等于x
*/
while (p != null && !p.data.equals(x)) {// 寻找和x值相等的节点
p = p.next;
j++;
}
if (p == null) {// 如果p==null,说明值为x的节点不在单链表中,返回-1
return -1;
}
return j;// 否则返回值为x的节点在单链表中的位置
}
@Override
public void insert(int i, Object x) throws Exception {
Node p = head.next;// 将p指向首节点
int j = 0;
while (p != null && j < i - 1) {// 寻找第i个节点的前驱,即第i-1个节点
p = p.next;
j++;
}
if (i < 0 || p == null) {
throw new Exception("插入的位置不合法");
}
Node s = new Node(x);// 生成新节点
s.next = p.next;// 将第i个节点的地址赋值给新生成节点的指针域
p.next = s;// 将新节点的地址赋值给第i-1个节点的指针域
}
@Override
public void remove(int i) throws Exception {
Node p = head.next;
int j = 0;
/**
* p.next!=null限定了p最多只能指向尾节点,即第n-1个节点 。j<i-1限定了p最多只能指向第i-1个节点,即第i个节点的前驱
*/
while (p.next != null && j < i - 1) {
p = p.next;
j++;
}
if (i < 0 || p.next == null) {
throw new Exception("第" + i + "个元素不存在");
}
p.next = p.next.next;// 修改链指针,使待删除节点从单链表中脱离出来。
}
/**
* 输出单链表中的所有节点
*/
@Override
public void display() {
Node p = head.next;
while (p != null) {
System.out.print(p.data + " ");// 输出节点的值
p = p.next;// 取下一个节点
}
System.out.println();// 换行
}
}
2.3.4 其他链表
1.循环链表
循环链表(Circular List)也称为环形链表,其结构与单链表相似,只是将单链表的首尾相连,即将单链表的最后一个节点的后继指针指向第一个节点,从而构成一个环状链表,如图:
循环链表的操作算法与单链表的操作算法基本一致,差别斤在于判断单链表中访问的手否是最后一个节点的条件不再是它的后继节点是否为空,而是它的后继节点是否为头结点。
2. 双向链表
在单链表中的节点仅仅包含其后继节点的指针,所以要查找一个指定节点的后继节点,只要顺着它的后继指针即可一次找到,其时间复杂度为O(1)。但是若要查找一个指定节点的前驱结点,则要从单链表的表头开始顺着链一次进行查找,其时间复杂度为O(n),这是快速进行链表操作的一大障碍。为克服单链表的单向性的缺点,可对单链表进行重新定义,使其节点具有两个指针域,一个指针指向前驱节点,另一个指针指向后继节点。这种类型的链表称为双向链表。如图:
双向链表节点类的实现:
package linkedList;
public class DulNode {
public Object data;
public DulNode prior;
public DulNode next;
public DulNode() {
this(null);
}
public DulNode(Object data) {
this.data = data;
this.prior = null;
this.next = null;
}
}
双向链表也与单链表一样,只要首尾相连即可构成双向循环链表。如图:
双向链表中对于操作的实现若只涉及一个方向的指针,则它们的算法描述与单链表相应操作的算法一致,例如,length()、get(i)、和indexOf(x)等。但是对于插入和删除操作的算法要比单链表相应操作的算法更复杂一些,因为它涉及到两个指针域的修改。下图分别显示了在双向链表总插入和删除节点时指针修改的情况。
2.4 顺序表与链表的比较
链表比较灵活,插入和删除操作的效率较高,但链表的空间利用率较低,适合于实现动态的线性表;顺序表实现比较简单,因为任何高级程序语言中都有数组类型,并且空间利用率较高,可高效地进行随机存取,但顺序表不易扩充,插入和删除操作的效率较低,适合于实现相对稳定的静态线性表。
2.5 练习
练习1 编写一个顺序表的成员函数,实现对顺序表就地逆置的操作。
public void reverse(SqList s) {
int start = 0;
int end = s.curLen - 1;
while (start < end) {
Object temp = s.listElem[start];
s.listElem[start] = s.listElem[end];
s.listElem[end] = temp;
start++;
end--;
}
}练习2 编写一个单链表的成员函数,实现对带头结点的单链表就地逆置的操作。
思路:如果单链表为空或单链表中只有头结点,那么单链表不需要逆置,如果单链表中只有一个元素,逆置之后它的位置还是不会改变,所以可以不逆置。当单链表中有2个或两个以上的元素时,从第1个元素断开,令它的next为空,依次访问第2个元素到第n个元素,当访问到其中的任意一个元素时,将它插入到头结点之后,也就是把它插入到第1个位置,这样原始的第1个元素就会被后面的n-1个元素插入到它的前面,原始的第2个元素就会被后面的n-2个元素插入到它的前面,...直到原始的第n个元素插入到第1个位置。这样就实现了带头结点的单链表的就地逆置。
public void reverse(LinkList L) {
// 单链表为空或只有头节点或只有一个元素,不用进行操作
if (L.head == null || L.head.next == null || L.head.next.next == null) {
return;
}
Node p = L.head.next.next;// p指向a1节点
L.head.next.next = null;// 令单链表中的a1节点的指针域为空,即断开a1节点和a2节点之间的链接
while (p != null) {
Node q = p.next;// q指向a2节点
// 将p节点插入到头结点之后
p.next = L.head.next;
L.head.next = p;
p = q;// 继续访问下一个元素
}
}练习3 删除带头结点的单链表中的重复的节点
public void removeRepeatElem(LinkList L) {
Node p = L.head.next;// p指向首节点
Node q = null;//
while (p != null) {
q = p.next;
while (p != null) {
if (p.data.equals(q.data)) {
p.next = q.next;
}
q = q.next;
}
p = p.next;
}
}练习4 删除不带头结点的单链表中的数值域等于x的第一个节点,删除成功,返回被删除的节点的位置,否则返回-1
public int remove(LinkList L, Object x) {
Node p = L.head;// p指向首节点
Node q = null;// q记录p的前驱节点
int j = 0;
while (p != null && !p.data.equals(x)) {
q = p;
p = p.next;
j++;
}
if (p != null) {
if (q == null) {// 删除的是单链表中的首节点
L.head = p.next;
} else {// 删除的是单链表中的非首节点
p.next = p.next.next;
}
} else {
return -1;// 值为x的节点在单链表中不存在
}
return j;
}练习5 删除带头结点的单链表中的数据域等于x的所有节点,并返回删除节点的数目
public int removeAll(LinkList L, Object x) {
Node p = L.head.next;// p指向首节点
Node q = L.head;// q用来记录p节点的前驱
int j = 0;// j用来记录被删除节点的个数
while (p != null) {
if (p.data.equals(x)) {
q.next = p.next;
j++;
} else {
q = p;
}
p = p.next;
}
return j;
}
9万+



被折叠的 条评论
为什么被折叠?



