数据结构之线性表
理解了数据结构其实就是讨论数据之间的关系,知道算法是什么,如何估算程序的时间复杂度,以及什么是抽象数据类型之后,就可以开始学习前人为我们总结出来的这些典型的数据结构了。
现实生活中的排队就是一个典型的线性表的案例,线性表的抽象数据类型如下:
ADT 线性表 (List)
Data
线性表的数据对象集合用数学方法可以这样表示{a1,a2a3,...an,},其中除了第一个元素没有前驱元素,最后一个元素没有后驱元素,内一个元素都是有且只有一个前驱元素和后驱元素。数据元素之间的关系是一对一的关系。(其实这么一大段话就是用文字把线性表这个概念描述出来而已,心里只要想着排队就可以了)
Operation
InitList(); //初始化操作,建立一个新的线性表 L
ListIsEmpty(); //判断线性表是否为空
ClearList(); //清空线性表
GetElem(); //获得 L 中第 i 个元素的值,并返回给 e
LocateElem(); //判断 L 中是否有和 e 相同的元素,有就返回1,没有返回0
ListInsert(); //在第 i 个位置插入元素 e
ListDelete(); //删除第 i 个元素,并把值放到 e 中
ListLength(); //获得 L 的长度
endADT
一般抽象数据类型中定义的是关于这个数学模型最基本的操作,其他复杂的操作都可以由这些基本操作组合而成。
线性表中的顺序存储结构
这里指的是线性表的物理结构,也就是在内存中的存储结构。顺序结构就是在内存中划出一个指定大小的内存块,用来存储数据。让这些数据不只是在逻辑上相邻,在物理存储上也是相邻的。
那实现顺序存储结构的代码怎么写呢?下面就是啦,当然是用最爱的 Java 写的啦。
public class ArrayList1<E> {
private static final int defaultSize = 20;
private int length;//当前长度
private int size;//线性表大小
private Object data[];
public ArrayList1(){
InitList(defaultSize);
}
public ArrayList1(int size){
InitList(size);
}
//初始化线性表
private void InitList(int size){
this.size = size;
data = new Object[size];
}
//判断表示否为空
protected boolean ListIsEmpty(){
if(this.length == 0){
return true;
}
return false;
}
/*清空线性表,表面上是把所有数据清空,其实是让程序认定这没有数据。
也就是把 length 置为零*/
protected void ClearList(){
this.length = 0;
}
//返回顺序表中第 i 个位置的元素,
//时间复杂度为 O(1)
protected E GetElem(int i){
if(i <= 0 || i > this.length){
return null;
}
return (E) this.data[i-1];
}
//在第 i 个位置插入元素 e,没写好
//时间复杂度为 O(n)
protected boolean ListInsert(int i,E e){
if(i <= 0 || i > this.size){
return false;
}else if(i == this.length + 1){
this.data[i-1] = e;
this.length++;
return true;
}else if(i > this.length + 1 && i <= this.size){
this.data[this.length] = e;
this.length++;
return true;
}
for(int j = (this.length - i + 1);j > 0;j--){
this.data[j + 1] = this.data[j];
}
this.data[i-1] = e;
this.length++;
return true;
}
//在第 i 个位置删除元素
//时间复杂度为 O(n)
protected boolean ListDelete(int i){
if(i <= 0 || i > this.length){
return false;
}
for(int j = i;j < this.length;j++){
this.data[j-1] = this.data[j];
}
this.length--;
return true;
}
//获得线性表长度
protected int ListLength(){
return this.length;
}
//获得线性表长度
protected int ListSize(){
return this.size;
}
//判断是否有和指定元素相同的,有,则返回它所在位置序号,没有则返回 -1
protected int LocateElem(E e){
for(int i = 0;i < this.length;i++){
if(e.equals(this.data[i])){
return i+1;
}
}
return -1;
}
}
通过时间复杂度的推导,可以知道顺序表中插入和删除的时间复杂度为 O(n),查询的时间复杂度为 O(1)。
由此可得出,顺序表优点是查询快,缺点是添加、删除慢。
线性表中的链式存储结构
由于顺序存储结构在插入和删除时,时间复杂度过大,然后科学家们就想出了链式存储的方式来解决这个问题。
链式存储不强制采用一块地址连续的空间来存储数据,可以连续,也可以不连续,只要保证他在逻辑上是相邻的就可以了。
实现的方式是每一个通过结点来存储数据,以及下一个结点所在的地址。如下图所示,
具体实现代码如下:(这里讨论的是有头结点的单链表)
public class LinkedList1<E>{
private class Node{//结点
private E e;//数据域
private Node next;//指针域
public Node(E e){
this.e = e;
this.next = null;
}
public Node() {
}
public E getE() {
return e;
}
public void setE(E e) {
this.e = e;
}
public Node getNext() {
return next;
}
public void setNext(Node next) {
this.next = next;
}
}
private Node head;//头结点
private int size;//链表大小
public LinkedList1(){
head = new Node();
this.size = 0;
}
//添加到后面
protected boolean add(E e){
Node n = new Node(e);
if(this.size == 0){
head.setNext(n);
}else{
Node node = get(this.size);
node.setNext(n);
}
this.size++;
return true;
}
//返回指定位置的结点
protected E getElem(int position){
if(this.size == 0 || position <= 0 || position > this.size){
return null;
}
Node node = get(position);
return node.getE();
}
//判断是否为空
protected boolean ListIsEmpty(){
if(this.size == 0){
return true;
}else{
return false;
}
}
//得到大小
protected int ListLength(){
return this.size;
}
//清空
protected void ClearList(){
this.head.setNext(null);
this.size = 0;
}
//删除某个位置的元素
// head a b c d e 5 /2
protected boolean ListDelete(int p){
if(p <= 0 || p > this.size){
return false;
}
Node node = get(p);
Node nodeBefore = get(p-1);
nodeBefore.setNext(node.getNext());
this.size--;
return true;
}
//在某个位置上插入元素
//head a b c d e 5 3
protected boolean ListInsert(E e,int p){
if(p <= 0 || p > this.size + 1){
return false;
}
if(p == this.size + 1){
add(e);
return true;
}
Node newNode = new Node(e);
Node node = get(p-1);
newNode.setNext(node.getNext());
node.setNext(newNode);
this.size++;
return true;
}
//这个方法的时间复杂度还是 O(n)
private Node get(int p){
if(p == 0){
return head;
}
Node node = head;
for(int i = 0;i<p;i++){
node = node.getNext();
}
return node;
}
链表还有几种其他变形,静态链表,循环链表,双向链表等,其实本事上都差不多。
静态链表,就是利用一个数组来存储结点,结点包含了数据域和指针域。
循环链表,就是尾结点的指针域不再为空,而是指向头结点,构成循环。
双向链表,就是一个结点包含一个数据域和两个指针域,分别用来指向后一个结点和前一个节点。
总之,实际开发中,利用它们之间的特性,选择合适的数据结构完成上一层次的需求,就可以了。
补充,上述对于链表的实现其实写的不对,上面 get() 方法的时间复杂度是O(n),而插入和删除都使用了这个方法,导致时间复杂度也是 O(n) ,而不是链表本来的 O(1) ,这里的确写错了。
正确的写法应该是加上一个尾指针,专门用来指向末尾结点,至少可以保证直接在末尾添加时时间复杂度是 O(1)