线性表
线性表是一个非常灵活的数据结构,其长度可根据需要增长或缩短,即对线性表的数据元素不仅可以进行访问还可以进行插入,删除等操作
- 线性表接口代码
interface List<T> { void destroyList(); boolean listEmpty(); void add(T dataElement); void traverse(); void delete(int location); void insert(T dataElement, int location); }
顺序表
线性表的顺序表示是指用一组地址连续的的存储单元存储线性表的数据元素,其特点是逻辑上相邻的元素在物理次序上也相同
-
采用数组作为存储容器
-
实现代码
class SqList<T> implements List<T> { private final int MAX_SIZE = 100; private Object[] dataElements; private int length; public SqList() { // TODO Auto-generated constructor stub this.dataElements = new Object[MAX_SIZE]; //泛型T不能作为创建数组的修饰符 this.length = 0; } @Override public void destroyList() { // TODO Auto-generated method stub this.length = 0; } @Override public boolean listEmpty() { // TODO Auto-generated method stub if(this.length == 0) return true; else return false; } @Override public void add(T dataElement) { // TODO Auto-generated method stub this.dataElements[length++] = dataElement; } @Override public void traverse() { // TODO Auto-generated method stub for (int i = 0; i < this.length; i++) { System.out.print(dataElements[i] + " "); } System.out.println(); } @Override public void delete(int location) { // TODO Auto-generated method stub for(int i = location; i < length ; i++) { //删除需移动 length-location 次 dataElements[i-1] = dataElements[i]; } dataElements[--length] = null; //易错成:dataElements[length--] = null } @Override public void insert(T dataElement,int location) { // TODO Auto-generated method stub for(int i = length; i >= location ; i--) { //插入需移动(length-location)+1 次 dataElements[i] = dataElements[i - 1]; } dataElements[location-1] = dataElement; length++; } }
-
代码解析
-
Object类型的数组
因为无法申请泛型T类型的数组所以用Object类型的数组代替,添加数据时将泛型T的元素加入到数组中(自动向上转型),取出元素是强制类型转化类泛型T类型输出
-
插入与删除操作时的移动次数
-
插入
插入时从指定的location位置往后移动(先从末尾开始往后移)需要移动 (length-location)+1 次
-
删除
删除时从指定的location位置的后一位向前移动,移动length-location 次(移动(length-location)+1 次也可以)
-
-
链式表
用一组任意的存储单元存储线性表的元素,每个独立的存储单元称为一个结点,每个结点通过引用与前后结点相连,形成一条链表
单链表
每个结点中的引用都单向的指向当前结点的后继结点
-
代码实现
class LinkList_BackUp<T> implements List<T> { //后插法创建链表 private class Node{ //节点类 private Node next; private T dataElement; public Node() { //构造头节点 // TODO Auto-generated constructor stub this.next = null; //节点中的next(连接下一个节点) this.dataElement = null; } public Node(T dataElement) { //构造尾节点 // TODO Auto-generated constructor stub this.next = null; this.dataElement = dataElement; } } private int size; //节点个数 private Node headNode; private Node node; //尾节点 public LinkList_BackUp(){ this.size = 0; this.headNode = new Node(); //初始化生成头节点 node = headNode; //node从头节点开始一步一步往下串联节点 } @Override public void destroyList() { // TODO Auto-generated method stub headNode.next = null; node = headNode; //将node节点重置 size = 0; //size重置 } @Override public boolean listEmpty() { // TODO Auto-generated method stub if(size == 0) return true; else return false; } @Override public void add(T dataElement) { //在末尾增加新节点 // TODO Auto-generated method stub Node newNode = new Node(dataElement); //临时节点 //判断末尾节点是否被删除 if(node == null) { node = headNode; //当末尾节点被删除时这时node结点的前一个结点会指向null所以node结点会被丢弃,当需要再次添加结点时需要重新定位到末尾节点 while(node.next != null) { node = node.next; } } node.next = newNode; node = node.next; size++; } @Override public void traverse() { // TODO Auto-generated method stub Node tempNode = headNode.next; while(tempNode != null) { //判断节点是否为空 System.out.print(tempNode.dataElement + " "); tempNode = tempNode.next; } System.out.println(); } @Override public void delete(int location) { // TODO Auto-generated method stub if(location > size) System.out.println("越界警告"); Node tempNode = headNode; //临时节点作查找节点 for(int i = 0; i < location - 1; i++) { //查找到对应的节点的上一个节点 tempNode = tempNode.next; } //对对应节点进行删除操作 tempNode.next = tempNode.next.next; if(location == size)//当末尾节点被删除时将node置为null node = null; //相当于释放被抛弃的末尾节点的空间(去引用后GC将自动回收) size--; } @Override public void insert(T dataElement, int location) { // TODO Auto-generated method stub Node newNode = new Node(dataElement); Node tempNode = headNode; //临时节点作查找节点 for(int i = 0; i < location - 1; i++) { //查找到location对应的节点的上一个节点 tempNode = tempNode.next; } newNode.next = tempNode.next; //将新节点的next连接到localtion对应节点上 tempNode.next = newNode; //将location对应的节点的上一个节点的next链接到newNode上 size++; } }
-
代码解析
-
Node内部类
每个Node的实现类表示一个结点,结点中包括数据和引用
-
删除尾结点后再次添加元素时的 node 结点处理问题
在添加元素时node结点不断向后移动以便将新节点链接到尾结点上所以node总是处于尾结点,当要删除尾结点时因为单向链表不可回溯此时node结点不在是实际上的尾结点了于是在再一次进行添加操作时需要重新定位尾结点
-
插入与删除结点时的location定位
单向结点进行插入和删除操作时都需要找到对应结点的的上一个结点再进行操作,而双向链表则更加灵活
-
双向链表
每个结点中有两个引用,分别指向当前结点的前驱结点和后继结点
-
代码实现
class DuLinkList<T> implements List<T> { private class Node{ //节点类 private Node prior; //指向前驱 private Node next; //指向后继 private T dataElement; //泛型的结点元素 public Node() { //构造头节点 // TODO Auto-generated constructor stub this.prior = this; //头节点中的前后引用都指向本身 this.next = this; //节点中的next(连接下一个节点) this.dataElement = null; } public Node(T dataElement) { //构造存储节点 // TODO Auto-generated constructor stub this.prior = null; this.next = null; this.dataElement = dataElement; } } private int size; //节点个数 private Node headNode; //头节点 private Node node; //尾节点 public DuLinkList() { //初始化双向链表 // TODO Auto-generated constructor stub size = 0; headNode = new Node(); node = headNode;//初始尾节点即头节点 } @Override public void destroyList() {//重置双向链表 // TODO Auto-generated method stub headNode.next = headNode; headNode.prior = headNode; node = headNode; size = 0; } @Override public boolean listEmpty() { // TODO Auto-generated method stub if(size == 0) return true; else return false; } @Override public void add(T dataElement) {//添加结点 // TODO Auto-generated method stub Node newNode = new Node(dataElement); node.next = newNode; //将新节点添加到尾结点末尾 headNode.prior = newNode; //头节点的前驱结点指向新结点,形成了一个循环 newNode.prior = node; //新节点的前驱指向尾结点 newNode.next = headNode;//新节点的后驱指向头结点,形成了一个循环 node = node.next;//将新添加的结点作为尾结点 size++; } @Override public void traverse() { // TODO Auto-generated method stub Node tempNode = headNode.next; while(tempNode != headNode) { //当 当前结点为头节点是结束循环 System.out.print(tempNode.dataElement + " "); tempNode = tempNode.next; } System.out.println(); } @Override public void delete(int location) { // TODO Auto-generated method stub if(location > size) System.out.println("越界警告"); else { Node tempNode = headNode; for(int i = 0; i < location; i++) { //找到location位置对应的节点 tempNode = tempNode.next; } //对对应节点进行删除操作 tempNode.prior.next = tempNode.next; //将被删除节点的上一个节点的后继节点指向被删除节点的下一个节点 tempNode.next.prior = tempNode.prior;//将被删除节点的下一个节点的前驱节点指向被删除节点的上一个节点 if(location == size) node = node.prior; //当末尾节点node被删除时将末尾节点更新为node的上一个节点 //此举方便下次再添加时,新结点能添加到实际的尾节点上 size--; } } @Override public void insert(T dataElement, int location) { // TODO Auto-generated method stub Node newNode = new Node(dataElement); Node tempNode = headNode; //临时节点作查找节点 for(int i = 0; i < location; i++) { //查找到location对应的节点 tempNode = tempNode.next; } //完成插入操作 newNode.next = tempNode; //将新插入节点的后继节点指向被插入节点 newNode.prior = tempNode.prior; //将新插入节点的前驱节点指向被插入节点的上一个节点 tempNode.prior.next = newNode; //将被插入节点的上一个节点的后继节点指向新节点 tempNode.prior = newNode; //将被插入节点的前驱节点指向新节点 size++; } }
-
代码解析
-
双向链表的头节点
双向结点的前驱和后继引用都指向本身
-
双向链表的添加
- 将新结点添加到尾结点的末尾
- 头结点的前驱引用指向新结点
- 新结点的前驱指向尾结点
- 新结点的后驱引用指向头结点
-
双向链表的插入
- 先找到location所对应的被插入结点
- 将新插入结点的后继结点指向被插入结点
- 将新插入结点的前驱结点指向被插入结点的上一个结点
- 将被插入结点的上一个结点的后继结点指向新结点
- 将被插入结点的前驱结点指向新结点
-
双向链表的删除
- 找到location所对应的结点
- 将被删除节点的上一个节点的后继节点指向被删除节点的下一个节点
- 将被删除节点的下一个节点的前驱节点指向被删除节点的上一个节点
-
双向链表的删除尾结点后再添加新元素的 node 处理问题
原因和处理方法和单向链表一样,但在处理上双向链表更加简单,因为双向链表可以回溯所以当进行删除操作时判断到删除的操作是尾结点就将node重置为node的前驱结点
-
总结
- 优劣势
- 顺序表的总容量是固定的
- 顺序表在进行定位查找时更加高效,在进行插入和删除时伴随着大量元素的移动效率不较差
- 链式表的容量是无限制的
- 链式表的插入和删除较顺序表更高效
- 易错点
- 删除尾结点后再添加新元素的 node 处理问题
- 双向链表插入操作的各结点链接的顺序问题
- 顺序表用Object类型替代泛型T类型定义数组的转换问题