1.1线性表的基本概念
线性表是由n (n>= 0) 个数据元素(节点)a1,a2,a3, … an 组成的有限序列。
线性表中每个元素必须具有相同的结构(即拥有相同的数据项)。线性表是线性结构中最常用而又最简单的一种数据结构。比如表1.1就是一个线性表。
表1.1员工信息表
员工信息 | 姓名 | 年龄 | 学历 |
---|---|---|---|
0001 | 孙悟空 | 500 | 本科 |
0002 | 猪八戒 | 400 | 专科 |
0003 | 沙和尚 | 340 | 研究生 |
也就是说,线性表中每个数据元素实际可以包含若干个数据项。关于线性表有如下定义。
- 线性表中包含的数据元素的个数n被称为表的长度,当表的 长度为0时,该表也被称为空表。
- 当n>0时,表可以表示为(a1,a2,a3,…,an)。
对于一个非空,有限的线性表而言,它总具有如下基本特征。
总存在唯一的“第一个数据元素”。
- 总存在唯一的“最后一个数据元素”。
- 除第一个数据外,集合中的每一个数据元素都只有一个前驱的数据元素。
- 除最后一个数据元素外,集合中的每一个数据元素都只有一个后继的数据元素。
1.2线性表的基本操作
- 初始化,通常是一个构造器,用于返回一个空的线性表。
- 返回线性表的长度:用该方法返回线性表中数据元素的个数。
- 获取指定索引处的元素:根据索引返回线性表中的数据元素。
- 按值查找数据元素的位置:如果线性表中存在一个或多个与查找相等的数据元素,那么该方法返回第一个搜索到的值相等的数据元素的索引,否则返回-1.
- 直接插入数据元素:向线性表的头部插入一个数据元素,线性表长度+1。
- 向 指定位置插入数据元素:向线性表的指定索引处插入一个数据元素,线性表长度+1。
- 直接删除数据元素:删除线性表开头的数据元素,线性表长度-1.
- 删除指定位置中指定位置的数据元素:删除线性表中指定索引处的数据元素,线性表长度-1。
- 判断线性表是否为空:该方法判断线性表是否为空,如果线性表为空,则返回true,否则返回false。
- 清空线性表:将线性表清空。
- 简单了解之后,将用Java工具,分别实现顺序存储结构的线性表,链式存储结构的线性表。
1.3顺序线性表的基本实现
线性表的顺序存储结构是用一组地址连续的存储单元依次存放线性表的元素。当程序采用顺序结构来实现线性表时,线性表中相邻两个元素ai和a(i+1)对应的存储地址loc(ai)和loc(ai+1)也是相邻的。
package list;
import java.util.Arrays;
//对于多线程环境下,可能会存在线程安全问题
public class SequenceList<T>
{
private int DEFAULT_SIZE = 16;
//保存数组的长度
private int capacity;
//定义一个数组用于保存线性表的元素
private Object[] elementData;
//保存顺序线性表中元素的当前个数
private int size = 0;
//以默认数组长度创建空顺序线性表
public SequenceList()
{
capacity = DEFAULT_SIZE;
elementData = new Object[capacity];
}
//以一个初始化元素来创建顺序线性表
public SequenceList(T element)
{
this();
elementData[0] = element;
size++;
}
/**
* 以指定长度的数组创建顺序线性表
* @param element 指定顺序线性表中第一个元素
* @param initSize 指定顺序线性表底层数组的长度
*/
public SequenceList(T element, int initSize)
{
capacity = 1;
while (capacity < initSize)
{
capacity <<= 1; // capacity等于 capacity乘以2的1次方
}
elementData = new Object[capacity];
elementData[0] = element;
size++;
}
public int length()
{
return size;
}
public T get(int i)
{
if (i<0 || i > size-1)
{
throw new IndexOutOfBoundsException ( "线性表索引越界" );
}
return (T) elementData[i];
}
public int locate(T element)
{
for (int i=0; i<size; i++)
{
if (elementData[i].equals ( element ))
{
return i;
}
}
return -1;
}
public void insert(T element,int index)
{
if (index <0 || index > size )
{
throw new IndexOutOfBoundsException ( "线性表索引越界" );
}
ensureCapacity(size + 1);
//将index后的所有元素向后移动一格
System.arraycopy ( elementData,index,elementData,index+1,size-index );
elementData[index] = element;
size++;
}
//在线性顺序表的开始添加一个元素
public void add(T element)
{
insert ( element,size );
}
//扩充底层数组长度,性能差
private void ensureCapacity (int minCapacity)
{
if (minCapacity > capacity)
{
while (capacity < minCapacity)
{
capacity <<= 1;
}
elementData = Arrays.copyOf ( elementData,capacity );
}
}
public T delete(int index)
{
if (index < 0 || index > size -1)
{
throw new IndexOutOfBoundsException ( "线性表索引越界" );
}
T oldValue = (T) elementData[index];
int numMoved = size -index -1;
if (numMoved > 0)
{
System.arraycopy ( elementData,index+1,elementData,index,numMoved );
}
elementData[--size] = null;
return oldValue;
}
//删除最后一个元素
public T remove()
{
return delete ( size-1 );
}
public boolean isEmpty()
{
return size ==0;
}
//清空线性表
public void clear()
{
Arrays.fill ( elementData,null );
size = 0;
}
public String toString()
{
if (size == 0)
{
return "[]";
}
else {
StringBuilder sb = new StringBuilder ( "[" );
for (int i = 0; i < size; i++) {
sb.append ( elementData[i].toString () + "," );
}
int len = sb.length ();
return sb.delete ( len-2,len ).append ( "]" ).toString ();
}
}
}
这就相当于简单的ArrayList。
测试
@Test
public void testSeqlist(){
SequenceList<String> list = new SequenceList <> ( );
list.add ( "sasa" );
list.add ( "ccc" );
list.add ( "uuu" );
list.insert ( "as",2 );
System.out.print(list+" ");
System.out.println ();
list.delete ( 1 );
System.out.print(list+" ");
System.out.println (list.locate ( "as" ));
System.out.println (list.isEmpty ());
System.out.println (list.toString ());
}
测试结果:
1.4.链式存储结构
链表将采用一组地址任意的存储单元存放线性表中的数据元素。链式存储的线性表不会按照线性的逻辑顺序来保存数据元素,它需要在每一个数据元素里保存一个引用下一数据元素的引用(或者叫指针)。
由于不是按顺序存储,链表在插入,删除数据元素时比顺序线性表快得多,但是查找一个节点或者访问特定编号的节点比顺序线性表慢很多。
使用链表结构可以克服顺序线性表(基于数组)需要预先知道数据大小的缺点,链式结构可以充分利用计算机的内存空间,实现灵活的内存动态管理。但是链表结构失去了数组随机存取的优点,同时链表由于增加了节点的指针域,空间开销比较大。
对于链式存储结构的线性表而言,它的每个节点都必须包含数据本身和一个或两个用来引用上一个/下一个节点的引用。即:
节点 = 数据元素 + 引用下一个节点的引用 + 引用上一个节点的引用
如图1.4-1所示为双向链表节点示意图,其中每个节点中prev 代表对前一个节点的引用,只有双向链表的节点才存在prev引用。
图1.4-1 双向链表节点示意图
链表是多个相互引用的节点的集合,整个链表总长度是从头节点开始,然后依次向后指向每个节点。
空链表就是头节点为null的链表。
1.4.链式存储结构
链表将采用一组地址任意的存储单元存放线性表中的数据元素。链式存储的线性表不会按照线性的逻辑顺序来保存数据元素,它需要在每一个数据元素里保存一个引用下一数据元素的引用(或者叫指针)。
由于不是按顺序存储,链表在插入,删除数据元素时比顺序线性表快得多,但是查找一个节点或者访问特定编号的节点比顺序线性表慢很多。
使用链表结构可以克服顺序线性表(基于数组)需要预先知道数据大小的缺点,链式结构可以充分利用计算机的内存空间,实现灵活的内存动态管理。但是链表结构失去了数组随机存取的优点,同时链表由于增加了节点的指针域,空间开销比较大。
对于链式存储结构的线性表而言,它的每个节点都必须包含数据本身和一个或两个用来引用上一个/下一个节点的引用。即:
节点 = 数据元素 + 引用下一个节点的引用 + 引用上一个节点的引用
如图1.4-1所示为双向链表节点示意图,其中每个节点中prev 代表对前一个节点的引用,只有双向链表的节点才存在prev引用。
图1.4-1 双向链表节点示意图
链表是多个相互引用的节点的集合,整个链表总长度是从头节点开始,然后依次向后指向每个节点。
空链表就是头节点为null的链表。
1.4.1单链表上的基本运算
单链表指的是每个节点只保留一个引用,该节点指向当前接节点的下一个节点,没有引用指向头节点,尾节点的next引用为null。如图1.4.1-1为单链表的示意图。
对于单链表,系统建立单链表的过程是不断添加节点的过程。动态建立链表有两种方式。
- 头插
- 尾插
尾插, 生成节点的次序与输入的顺序相同,头插相反。
对于单链表常用的操作:
- 查找
- 插入
- 删除
1.查找
查找分两种:
(1)按序号查找第index个节点:从header节点依次向下在单链表中查找第index个节点。算法为,设header为头,current为当前节点(初始时current从header开始),0为头节点序号,i为计数器,则可使current依次下移寻找节点,并使i同时递增记录节点 序号,知道返回指定节点。
(2)在链表中查找指定的element元素:查找是否有等于给定值element的节点。若有,则返回首次找到的其值为element的节点的索引;否则返回-1。查找过程从开始节点出发,顺着链表逐个节点的值和给定的element作比较。
2.插入
插入是将值为element的新节点插入到链表的第index个节点的位置上。因此,首先找到index-1处节点的next引用新节点,新节点的next引用原来的index处的节点。
如图2-1所示向i索引处插入节点的示意图。
3.删除
删除操作是将链表的第index个节点删去。因为在单链表中,第index个节点是由index-1处的节点引用的,因此删除index处的节点首先要获取index-1处节点,然后让index-1处节点的next引用到原index+1处的节点,并释放index处节点即可。
如图3-1所示是删除i索引处节点的示意图。
(viso 箭头似乎不是很灵活,只能以拼接的方式展示曲线,可以推荐更好的哦)
Java语言实现单链表
public class LinkList<T>
{
//定义一个内部类Node,Node实例化代表链表的结点
private class Node{
//保存结点的数据
private T data;
//保存下一个结点的引用
private Node next;
//无参数的构造器
public Node(){}
//初始化全部参数的构造器
public Node(T data,Node next)
{
this.data = data;
this.next = next;
}
}
//保存该链表的头节点
private Node header;
//保存该链表的尾节点
private Node tail;
//保存该链表中已包含的结点数
private int size;
//创建空链表
public LinkList()
{
header = null;
tail = null;
}
//以特定数据来创建链表,该链表只有一个元素
public LinkList(T element)
{
header = new Node ( element,null );
//只有一个节点 ,header ,tail 都指向该节点
tail = header;
size++;
}
public int length()
{
return size;
}
public T get(int index)
{
return getNodeByIndex(index).data;
}
private Node getNodeByIndex (int index) {
if (index < 0 || index > size -1){
throw new IndexOutOfBoundsException("线性表索引越界");
}
//从header节点开始
Node current = header;
for (int i=0; i < size && current != null; i++,current = current.next){
if (i == index){
return current;
}
}
return null;
}
//按给定元素查找对应的索引
public int locate(T element){
Node current = header;
for (int i=0; i<size&& current != null; i++,current = current.next){
if (current.data.equals ( element )){
return i;
}
}
return -1;
}
//向指定位置插入一个元素
public void insert(T element, int index){
if (index < 0 || index > size){
throw new IndexOutOfBoundsException ( "线性表索引越界" );
}
if (header == null){
add(element);
}
else {
if (index == 0){
//头插
addAtHeader(element);
}
else {
//获取插入点的前一个节点
Node prev = getNodeByIndex ( index -1 );
//让prev 的next指向新节点
//让新节点的next引用指向原来prev 的下一个节点
prev.next = new Node ( element,prev.next );
size++;
}
}
}
//尾插
public void add (T element) {
if (header == null){
header = new Node ( element,null );
tail = header;
}
else {
//创建新节点
Node newNode = new Node ( element,null );
//让尾节点的next指向新节点
tail.next = newNode;
//以新节点作为新的尾节点
tail = newNode;
}
size++;
}
//头插法为链表添加新节点
public void addAtHeader (T element) {
//创建新节点,让新节点的next指向原来的header
//并以新节点作为新的header
header = new Node ( element,header );
//如果插入之前是空链表
if (tail== null){
tail = header;
}
size++;
}
//删除链式线性表中指定索引处的元素
public T delete(int index){
if (index < 0 || index > size -1){
throw new IndexOutOfBoundsException ( "线性表索引越界" );
}
Node del = null;
//被删除的是header节点
if (index == 0)
{
del = header;
header = header.next;
}
else {
//获取删除的前一个节点
Node prev = getNodeByIndex ( index -1 );
//获取将要被删除的节点
del = prev.next;
//让被删除的节点的next指向被删除节点的下一个节点
prev.next = del.next;
//将被删除节点的next引用赋为null
del.next = null;
}
size--;
return del.data;
}
//删除最后一个元素
public T remove(){
return delete ( size -1 );
}
public boolean idEmpty(){
return size == 0;
}
public void clear(){
header = null;
tail = null;
size = 0;
}
public String toString(){
if (idEmpty ()){
return "[]";
}
else {
StringBuilder sb = new StringBuilder ( "[" );
for (Node current = header; current != null; current = current.next){
sb.append ( current.data.toString () + "," );
}
int len = sb.length ();
return sb.delete ( len-2,len ).append ( "]" ).toString ();
}
}
}
测试结果:
可以对比Java提供的 LinkedList 。
参考文献
–疯狂Java程序员的基本修养 李刚 著