应用程序后在那个的数据大致有四种基本的逻辑结构:
- 集合:数据元素之间只有"同属于一个集合"的关系
- 线性结构:数据元素之间存在一个对一个的关系
- 树形结构:数据元素之间存在一个对多个关系
- 图形结构或网状结构:数据元素之间存在多个对多个的关系
对于数据不同的逻辑结构,计算机在物理磁盘上通常有两种屋里存储结构
- 顺序存储结构
- 链式存储结构
本篇博文主要讲的是线性结构,而线性结构主要是线性表,非线性结构主要是树和图。
线性表的基本特征:
- 总存在唯一的第一个数据元素
- 总存在唯一的最后一个数据元素
- 除第一个数据元素外,集合中的每一个数据元素都只有一个前驱的数据元素
- 除最后一个数据元素外,集合中的每一个数据元素都只有一个后继的数据元素
1.线性表的顺序存储结构:是指用一组地址连续的存储单元一次存放线性表的元素。为了使用顺序结构实现线性表,程序通常会采用数组来保存线性中的元素,是一种随机存储的数据结构,适合随机访问。java中ArrayList类是线性表的数组实现。
1 import java.util.Arrays; 2 public class SequenceList<T> 3 { 4 private int DEFAULT_SIZE = 16; 5 //保存数组的长度。 6 private int capacity; 7 //定义一个数组用于保存顺序线性表的元素 8 private Object[] elementData; 9 //保存顺序表中元素的当前个数 10 private int size = 0; 11 //以默认数组长度创建空顺序线性表 12 public SequenceList() 13 { 14 capacity = DEFAULT_SIZE; 15 elementData = new Object[capacity]; 16 } 17 //以一个初始化元素来创建顺序线性表 18 public SequenceList(T element) 19 { 20 this(); 21 elementData[0] = element; 22 size++; 23 } 24 /** 25 * 以指定长度的数组来创建顺序线性表 26 * @param element 指定顺序线性表中第一个元素 27 * @param initSize 指定顺序线性表底层数组的长度 28 */ 29 public SequenceList(T element , int initSize) 30 { 31 capacity = 1; 32 //把capacity设为大于initSize的最小的2的n次方 33 while (capacity < initSize) 34 { 35 capacity <<= 1; 36 } 37 elementData = new Object[capacity]; 38 elementData[0] = element; 39 size++; 40 } 41 //获取顺序线性表的大小 42 public int length() 43 { 44 return size; 45 } 46 //获取顺序线性表中索引为i处的元素 47 public T get(int i) 48 { 49 if (i < 0 || i > size - 1) 50 { 51 throw new IndexOutOfBoundsException("线性表索引越界"); 52 } 53 return (T)elementData[i]; 54 } 55 //查找顺序线性表中指定元素的索引 56 public int locate(T element) 57 { 58 for (int i = 0 ; i < size ; i++) 59 { 60 if (elementData[i].equals(element)) 61 { 62 return i; 63 } 64 } 65 return -1; 66 } 67 //向顺序线性表的指定位置插入一个元素。 68 public void insert(T element , int index) 69 { 70 if (index < 0 || index > size) 71 { 72 throw new IndexOutOfBoundsException("线性表索引越界"); 73 } 74 ensureCapacity(size + 1); 75 //将index处以后所有元素向后移动一格。 76 System.arraycopy(elementData , index , elementData 77 , index + 1 , size - index); 78 elementData[index] = element; 79 size++; 80 } 81 //在线性顺序表的开始处添加一个元素。 82 public void add(T element) 83 { 84 insert(element , size); 85 } 86 //很麻烦,而且性能很差 87 private void ensureCapacity(int minCapacity) 88 { 89 //如果数组的原有长度小于目前所需的长度 90 if (minCapacity > capacity) 91 { 92 //不断地将capacity * 2,直到capacity大于minCapacity为止 93 while (capacity < minCapacity) 94 { 95 capacity <<= 1; 96 } 97 elementData = Arrays.copyOf(elementData , capacity); 98 } 99 } 100 //删除顺序线性表中指定索引处的元素 101 public T delete(int index) 102 { 103 if (index < 0 || index > size - 1) 104 { 105 throw new IndexOutOfBoundsException("线性表索引越界"); 106 } 107 T oldValue = (T)elementData[index]; 108 int numMoved = size - index - 1; 109 if (numMoved > 0) 110 { 111 System.arraycopy(elementData , index+1 112 , elementData, index , numMoved); 113 } 114 //清空最后一个元素 115 elementData[--size] = null; 116 return oldValue; 117 } 118 //删除顺序线性表中最后一个元素 119 public T remove() 120 { 121 return delete(size - 1); 122 } 123 //判断顺序线性表是否为空表 124 public boolean empty() 125 { 126 return size == 0; 127 } 128 //清空线性表 129 public void clear() 130 { 131 //将底层数组所有元素赋为null 132 Arrays.fill(elementData , null); 133 size = 0; 134 } 135 public String toString() 136 { 137 if (size == 0) 138 { 139 return "[]"; 140 } 141 else 142 { 143 StringBuilder sb = new StringBuilder("["); 144 for (int i = 0 ; i < size ; i++ ) 145 { 146 sb.append(elementData[i].toString() + ", "); 147 } 148 int len = sb.length(); 149 return sb.delete(len - 2 , len).append("]").toString(); 150 } 151 } 152 }
2.线性表链式存储结构:将采用一组地址的任意的存储单元存放线性表中的数据元素。
链表又可分为:
- 单链表:每个节点只保留一个引用,该引用指向当前节点的下一个节点,没有引用指向头结点,尾节点的next引用为null。
- 循环链表:一种首尾相连的链表。
- 双向链表:每个节点有两个引用,一个指向当前节点的上一个节点,另外一个指向当前节点的下一个节点。
下面给出线性表双向链表的实现:java中LinkedList是线性表的链式实现,是一个双向链表。
1 public class DuLinkList<T> 2 { 3 //定义一个内部类Node,Node实例代表链表的节点。 4 private class Node 5 { 6 //保存节点的数据 7 private T data; 8 //指向上个节点的引用 9 private Node prev; 10 //指向下个节点的引用 11 private Node next; 12 //无参数的构造器 13 public Node() 14 { 15 } 16 //初始化全部属性的构造器 17 public Node(T data , Node prev , Node next) 18 { 19 this.data = data; 20 this.prev = prev; 21 this.next = next; 22 } 23 } 24 //保存该链表的头节点 25 private Node header; 26 //保存该链表的尾节点 27 private Node tail; 28 //保存该链表中已包含的节点数 29 private int size; 30 //创建空链表 31 public DuLinkList() 32 { 33 //空链表,header和tail都是null 34 header = null; 35 tail = null; 36 } 37 //以指定数据元素来创建链表,该链表只有一个元素 38 public DuLinkList(T element) 39 { 40 header = new Node(element , null , null); 41 //只有一个节点,header、tail都指向该节点 42 tail = header; 43 size++; 44 } 45 //返回链表的长度 46 public int length() 47 { 48 return size; 49 } 50 51 //获取链式线性表中索引为index处的元素 52 public T get(int index) 53 { 54 return getNodeByIndex(index).data; 55 } 56 //根据索引index获取指定位置的节点 57 private Node getNodeByIndex(int index) 58 { 59 if (index < 0 || index > size - 1) 60 { 61 throw new IndexOutOfBoundsException("线性表索引越界"); 62 } 63 if (index <= size / 2) 64 { 65 //从header节点开始 66 Node current = header; 67 for (int i = 0 ; i <= size / 2 && current != null 68 ; i++ , current = current.next) 69 { 70 if (i == index) 71 { 72 return current; 73 } 74 } 75 } 76 else 77 { 78 //从tail节点开始搜索 79 Node current = tail; 80 for (int i = size - 1 ; i > size / 2 && current != null 81 ; i++ , current = current.prev) 82 { 83 if (i == index) 84 { 85 return current; 86 } 87 } 88 } 89 return null; 90 } 91 //查找链式线性表中指定元素的索引 92 public int locate(T element) 93 { 94 //从头节点开始搜索 95 Node current = header; 96 for (int i = 0 ; i < size && current != null 97 ; i++ , current = current.next) 98 { 99 if (current.data.equals(element)) 100 { 101 return i; 102 } 103 } 104 return -1; 105 } 106 //向线性链式表的指定位置插入一个元素。 107 public void insert(T element , int index) 108 { 109 if (index < 0 || index > size) 110 { 111 throw new IndexOutOfBoundsException("线性表索引越界"); 112 } 113 //如果还是空链表 114 if (header == null) 115 { 116 add(element); 117 } 118 else 119 { 120 //当index为0时,也就是在链表头处插入 121 if (index == 0) 122 { 123 addAtHeader(element); 124 } 125 else 126 { 127 //获取插入点的前一个节点 128 Node prev = getNodeByIndex(index - 1); 129 //获取插入点的节点 130 Node next = prev.next; 131 //让新节点的next引用指向next节点,prev引用指向prev节点 132 Node newNode = new Node(element , prev , next); 133 //让prev的next指向新节点。 134 prev.next = newNode; 135 //让prev的下一个节点的prev指向新节点 136 next.prev = newNode; 137 size++; 138 } 139 } 140 } 141 //采用尾插法为链表添加新节点。 142 public void add(T element) 143 { 144 //如果该链表还是空链表 145 if (header == null) 146 { 147 header = new Node(element , null , null); 148 //只有一个节点,header、tail都指向该节点 149 tail = header; 150 } 151 else 152 { 153 //创建新节点,新节点的pre指向原tail节点 154 Node newNode = new Node(element , tail , null); 155 //让尾节点的next指向新增的节点 156 tail.next = newNode; 157 //以新节点作为新的尾节点 158 tail = newNode; 159 } 160 size++; 161 } 162 //采用头插法为链表添加新节点。 163 public void addAtHeader(T element) 164 { 165 //创建新节点,让新节点的next指向原来的header 166 //并以新节点作为新的header 167 header = new Node(element , null , header); 168 //如果插入之前是空链表 169 if (tail == null) 170 { 171 tail = header; 172 } 173 size++; 174 } 175 //删除链式线性表中指定索引处的元素 176 public T delete(int index) 177 { 178 if (index < 0 || index > size - 1) 179 { 180 throw new IndexOutOfBoundsException("线性表索引越界"); 181 } 182 Node del = null; 183 //如果被删除的是header节点 184 if (index == 0) 185 { 186 del = header; 187 header = header.next; 188 //释放新的header节点的prev引用 189 header.prev = null; 190 } 191 else 192 { 193 //获取删除点的前一个节点 194 Node prev = getNodeByIndex(index - 1); 195 //获取将要被删除的节点 196 del = prev.next; 197 //让被删除节点的next指向被删除节点的下一个节点。 198 prev.next = del.next; 199 //让被删除节点的下一个节点的prev指向prev节点。 200 if (del.next != null) 201 { 202 del.next.prev = prev; 203 } 204 //将被删除节点的prev、next引用赋为null. 205 del.prev = null; 206 del.next = null; 207 } 208 size--; 209 return del.data; 210 } 211 //删除链式线性表中最后一个元素 212 public T remove() 213 { 214 return delete(size - 1); 215 } 216 //判断链式线性表是否为空链表 217 public boolean empty() 218 { 219 return size == 0; 220 } 221 //清空线性表 222 public void clear() 223 { 224 //将底层数组所有元素赋为null 225 header = null; 226 tail = null; 227 size = 0; 228 } 229 public String toString() 230 { 231 //链表为空链表时 232 if (empty()) 233 { 234 return "[]"; 235 } 236 else 237 { 238 StringBuilder sb = new StringBuilder("["); 239 for (Node current = header ; current != null 240 ; current = current.next ) 241 { 242 sb.append(current.data.toString() + ", "); 243 } 244 int len = sb.length(); 245 return sb.delete(len - 2 , len).append("]").toString(); 246 } 247 } 248 public String reverseToString() 249 { 250 //链表为空链表时 251 if (empty()) 252 { 253 return "[]"; 254 } 255 else 256 { 257 StringBuilder sb = new StringBuilder("["); 258 for (Node current = tail ; current != null 259 ; current = current.prev ) 260 { 261 sb.append(current.data.toString() + ", "); 262 } 263 int len = sb.length(); 264 return sb.delete(len - 2 , len).append("]").toString(); 265 } 266 } 267 }
线性表的两种实现比较
- 空间性能: 顺序表:顺序表的存储空间是静态分布的,需要一个长度固定的数组,因此总有部分数组元素被浪费。
- 时间性能: 顺序表:顺序表中元素的逻辑顺序与物理存储顺序是保持一致的,而且支持随机存取。因此顺序表在查找、读取时性能很好。
- 链表:链表的存储空间是动态分布的,因此不会空间浪费。但是由于链表需要而外的空间来为每个节点保存指针,因此要牺牲一部分空间。
- 链表:链表采用链式结构来保存表内元素,因此在插入、删除元素时性能要好