一、什么是线性表?
线性表是由n(n≥0)个数据元素所构成的有限序列,通常表示为(a0,a1,…,ai,…,an-1)。
实现方式:
一种是基于顺序存储的实现——顺序表;
另一种是基于链式存储的实现——链表。
二、顺序表
1. 顺序表的定义
顺序表,即顺序存储的线性表。顺序存储是用一组地址连续的存储单元依次存放线性表中各个数据元素的存储结构。
2. 顺序表的特点
- 存储密度高,但需要预先分配“足够应用”的存储空间,这可能会造成存储空间的浪费;其中,存储密度=数据元素本身值所占用的存储空间/该数据元素实际所占用的空间;
- 便于随机存取;
- 不便于插入和删除操作,这是因为在顺序表上进行插入和删除操作会引起打量数据元素的移动。
- 在线性表中逻辑上相邻的数据元素,在物理存储位置上也是相邻的;
3. 顺序表的构建及其基本操作
//顺序表
public class SqList {
private int[] listElem; //顺序表存储空间
private int curLen; //顺序表当前长度
public static void main(String[] args) throws Exception {
//测试
SqList sq = new SqList(5); //构造容量空间为5的顺序表
sq.display(); //遍历
System.out.println();
sq.insert(3, 2); //插入
sq.display(); //遍历
System.out.println();
System.out.println(sq.indexOf(2)); //查找-按值查下标
System.out.println(sq.get(3)); //查找-按下标查值
sq.remove(3); //删除
sq.display(); //遍历
}
//构造方法-构造一个存储空间容量为maxSize的顺序表
public SqList(int maxSize) {
curLen = 0; //置顺序表的当前长度为0
this.listElem = new int[maxSize]; //为顺序表分配maxSize个存储单元
}
//1. 将一个已经存在的顺序表置空
public void clear() {
curLen = 0;
}
//2. 判断顺序表是否为空
public boolean isEmpty() {
return curLen == 0;
}
//3. 返回顺序表中的数据元素个数
public int length() {
return curLen;
}
//4. (查找)返回顺序表中第i个数据元素
public int get(int i) throws Exception{
if(i<0 || i>listElem.length-1)
throw new Exception("第" + i + "个元素不存在"); //不存在则抛出异常
return listElem[i];
}
//5. 打印顺序表中的数据元素
public void display() {
for(int i=0;i<listElem.length;i++)
System.out.print(listElem[i]);
}
//6. (插入)在顺序表的第i个数据元素之前插入一个值为x的数据元素
public void insert(int i,int x) throws Exception{
if(curLen == listElem.length) //判断顺序表是否已满
throw new Exception("顺序表已满");
if(i<0 || i>listElem.length) //判断i是否合法
throw new Exception("插入位置不合理");
for(int j=listElem.length-1;j>i;j--)
listElem[j] = listElem[j-1]; //插入位置及其之后的所有数据元素后移
listElem[i] = x; //插入i
curLen++; //表长加1
}
//7. (删除)删除顺序表中第i个元素
public void remove(int i) throws Exception{
if(i<0 || i>listElem.length) //判断i是否合法
throw new Exception("删除位置不合理");
for(int j=i;j<listElem.length-1;j++)
listElem[j] = listElem[j+1]; //删除位置及其之前的所有数据元素前移
curLen--; //表长减1
}
//8. (查找)返回顺序表中首次出现指定数据元素的位序号,若线性表中不包含此数据元素,则返回-1
public int indexOf(int x) {
int index = -1;
for(int i=0;i<listElem.length;i++) {
if(x == listElem[i])
index = i;
}
return index;
}
}
测试结果:
三、链表
1. 链表的定义
链表,即链式存储的线性表。链表中每一个结点包含存放数据元素值的数据域和存放指向逻辑上相邻结点的指针域。
单链表:一个结点中只包含一个指针域;
循环链表(环形链表):结构与单链表相似,将单链表的首尾相连,即将单链表的最后一个结点的后继指针指向第一个结点,从未构成一个环状链表;
双向链表:一个结点具有两个指针域,一个指向前驱结点,一个指向后继结点。
2. 链表的特点
- 采用动态存储分配,不会造成内存浪费和溢出;
- 执行插入和删除操作十分方便,修改指针即可,不需要移动大量元素。
3. 单链表的构建及其基本操作
//使用泛型构造结点类
public class Node<T> {
public T data; //存放结点值
public Node<T> next; //后继结点的引用
//无参时的构造函数
public Node(){
this(null,null);
}
//带一个参数时的构造函数
public Node(T data){
this(data,null);
}
//带两个参数时的构造函数
public Node(T data,Node<T> next)
{
this.data=data;
this.next=next;
}
}
import java.util.Scanner;
import Node;
//链表
public class LinkList<T>{
public Node<T> head; //单链表的头指针
public static void main(String[] args) throws Exception{
//测试
LinkList<Integer> link = new LinkList<Integer>(5,true); //尾插法
link.display(); //打印
System.out.println(link.isEmpty()); //判空
link.insert(link.length(),7); //插入
link.display(); //打印
System.out.println(link.length()); //长度
System.out.println(link.get(2)); //查找-按下标查找值
System.out.println(link.indexOf(7)); //查找-按值查找下标
link.remove(0); //删除
link.display(); //打印
}
//构造方法-构造一个带头结点的单链表
public LinkList() {
head = new Node<T>(); //初始化头结点
}
//构造方法-构造一个长度为n的单链表
public LinkList(int n,boolean Order) throws Exception{
this(); //调用无参构造方法初始化头结点
if(Order) //用尾插法顺序建立单链表
create1(n);
else
//用头插法逆序建立单链表
create2(n);
}
//1. 用尾插法顺序建立单链表,其中n为单链表的结点个数
public void create1(int n) throws Exception{
Scanner sc = new Scanner(System.in); //构造用于输入的对象
for(int j=0;j<n;j++) //输入n个结点的数据域值
insert(length(), (T) sc.next()); //生成新结点,插入到表尾
}
//2. 用头插法逆序建立单链表,其中n为单链表的结点个数
public void create2(int n) throws Exception{
Scanner sc = new Scanner(System.in); //构造用于输入的对象
for(int j=0;j<n;j++) //逆序输入n个结点的数据域值
insert(0,(T) sc.next()); //生成新结点,插入到表头
}
//3. 将一个已经存在的带头结点单链表置成空表
public void clear() {
head.data = null;
head.next = null;
}
//4. 判断带头结点的单链表是否为空
public boolean isEmpty() {
return head.next == null;
}
//5. 求带头结点的单链表的长度
public int length() {
Node<T> p = head.next; //指针p指向头结点的下一个结点
int length = 0;
while(p != null) {
p = p.next; //指针向后移动
++length; //长度增加
}
return length; //返回长度
}
//6. (查找)读取带头结点的单链表中的 第i个结点
public T get(int i) throws Exception{
Node<T> p = head.next; //指针p指向头结点的下一个结点
int j = 0; //j为计数器
while(p!=null && j<i) { //从首结点开始向后查找,直到p指向第i个结点或p为空
p = p.next; //指向后继结点
++j; //计数器的值增1
}
if(j>i || p==null) { //i小于0或者大于表长减1时,即i不合法
throw new Exception("第" + i + "个元素不存在"); //抛出异常
}
return p.data; //返回结点p的数据域值
}
//7. (查找)在带头结点的单链表中查找值为x的结点
public int indexOf(T x) {
Node<T> p = head.next; //指针p指向头结点的下一个结点
int j = 0; //j为计数器
//下面从单链表中的首结点开始查找,直到p.data为x或到达单链表的表尾
while(p!=null && !p.data.equals(x)) {
p = p.next; //指向下一个结点
++j; //计数器的值增1
}
if(p!= null)
return j; //返回值为x的结点在单链表的位置
else
return -1; //值为x的结点不在单链表中,则返回-1
}
//8. (插入)在带头结点的单链表中的第i个结点之前插入一个值为x的新结点
public void insert(int i,T k) throws Exception {
Node<T> p = head; //初始化头结点
int j = -1; //j为计数器
while(p!=null && j<i-1) { //寻找第i个结点的前驱
p = p.next;
++j; //计数器的值增1
}
if(j>i-1 || p==null) //i不合法
throw new Exception("插入位置不合法"); //抛出异常
Node<T> s = new Node<T>(k); //生成新结点
s.next = p.next; //修改链,使新结点插入单链表中
p.next = s;
}
//9. (删除)删除带头结点的单链表中的第i个结点
public void remove(int i) throws Exception{
Node<T> p = head; //初始化头结点
int j = -1; //j为计数器
while(p.next!=null && j<i-1) { //寻找第i个结点的前驱
p = p.next; //指针后移
++j;
}
if(j>i-1 || p.next==null)
throw new Exception("删除位置不合法");
p.next = p.next.next; //修改链指针,使待删除结点从单链表中脱离出来
}
//10. 输出单链表中的所有结点
public void display() {
Node<T> p = head.next; //指针p指向头结点的下一个结点
while(p != null) {
System.out.print(p.data + " "); //输出结点的值
p = p.next; //指针后移
}
System.out.println(); //换行
}
}
测试结果:
四、顺序表与链表的比较
- 链表比较灵活,插入和删除操作的效率较高,但链表的空间利用率较低,适合于视线动态的线性表;
- 顺序表实现比较简单,因为任何高级程序语言中都有数组类型,并且空间利用率也较高,可高效地进行随机存取,但顺序表不易扩充,插入和删除操作的效率较低,适合于实现相对“稳定”的静态线性表。