线性表
线性表是最常用且最简单的一种数据结构,它是n个数据元素的有限序列。
实现线性表的方式一般有两种,一种是使用数组存储线性表的元素,即用一组连续的存储单元依次存储线性表的数据元素。另一种是使用链表存储线性表的元素,即用一组任意的存储单元存储线性表的数据元素(存储单元可以是连续的,也可以是不连续的)。
数组
数组是一种大小固定的数据结构,对线性表的所有操作都可以通过数组来实现。虽然数组一旦创建之后,它的大小就无法改变了,但是当数组不能再存储线性表中的新元素时,我们可以创建一个新的大的数组来替换当前数组。这样就可以使用数组实现动态的数据结构。
代码1 创建一个更大的数组来替换当前数组
int[] oldArray = new int[10];
int[] newArray = new int[20];
for (int i = 0; i < oldArray.length; i++) {
newArray[i] = oldArray[i];
}
// 也可以使用System.arraycopy方法来实现数组间的复制
// System.arraycopy(oldArray, 0, newArray, 0, oldArray.length);
oldArray = newArray;
代码2 在数组位置index上添加元素e
//oldArray 表示当前存储元素的数组
//size 表示当前元素个数
public void add(int index, int e) {
if (index > size || index < 0) {
System.out.println("位置不合法...");
}
//如果数组已经满了 就扩容
if (size >= oldArray.length) {
// 扩容函数可参考代码1
}
for (int i = size - 1; i >= index; i--) {
oldArray[i + 1] = oldArray[i];
}
//将数组elementData从位置index的所有元素往后移一位
// System.arraycopy(oldArray, index, oldArray, index + 1,size - index);
oldArray[index] = e;
size++;
}
数组实现的线性表优点在于可以通过下标来访问或者修改元素,比较高效,主要缺点在于插入和删除的花费开销较大,比如当在第一个位置前插入一个元素,那么首先要把所有的元素往后移动一个位置。为了提高在任意位置添加或者删除元素的效率,可以采用链式结构来实现线性表。
链表
链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列节点组成,这些节点不必在内存中相连。每个节点由数据部分Data和链部分Next,Next指向下一个节点,这样当添加或者删除时,只需要改变相关节点的Next的指向,效率很高。
单链表
链表的删除指定链结点,是通过将目标链结点的上一个链结点的next指针指向目标链结点的下一个链结点,示意图如下:
通过这种方法,在链表的指针链中将跳过与删除的元素,达到删除的目的。不过,实际上删除掉的元素的next指针还是指向原来的下一个元素的,只是它不能再被单链表检索到而已,JVM的垃圾回收机制会在一定的时间之后回收它。
添加链结点比删除复杂一点,首先我们要使插入位置之前的链结点的next指针指向目标链结点,其次还要将目标链结点的next指针指向插入位置之后的链结点。本例中的插入位置仅限于链表的第一个位置,示意图如下:
//链结点的封装类
public class Link {
public int age;
public String name;
public Link next; //指向该链结点的下一个链结点
//构造方法
public Link(int age,String name){
this.age = age;
this.name = name;
}
//打印该链结点的信息
public void displayLink(){
System.out.println("name:"+name+",age:"+age);
}
}
//链表的封装类
public class LinkList {
private Link first; //指向链表中的第一个链结点
public LinkList(){
first = null;
}
//插入到链表的前端
public void insertFirst(Link link){
link.next = first;
first = link;
}
//删除第一个链结点,返回删除的链结点引用
public Link deleteFirst() throws Exception{
if(isEmpty()){
throw new Exception("链表为空!不能进行删除操作");
}
Link temp = first;
first = first.next;
return temp;
}
//打印出所有的链表元素
public void displayList(){
Link cur = first;
while(cur != null){ //循环打印每个链结点
cur.displayLink();
cur = cur.next;
}
}
//删除属性为指定值的链结点
public Link delete(int key){
Link link = null;
Link cur = first;
Link next = first.next;
Link previous = null;
while(cur != null){
if(cur.age == key){ //找到了要删除的链结点
link = cur;
//如果当前链结点的前驱为null,证明当其为链表的第一个链结点
//删除该链结点后需要对first属性重新赋值
if(previous == null){
this.first = next;
}else{
//删除操作,即将前驱的next指针指向当前链结点的next
//链表中将去当前链结点这一环
previous.next = next;
}
break;
}else if(cur.next == null){
//当前链结点不是目标且下一个链结点为null,证明没有要删除的链结点
break;
}
//当前链结点不是要删除的目标,则向后继续寻找
previous = cur;
next = next.next;
cur = cur.next;
}
return link;
}
//查找属性为指定值的链结点
public Link find(int key){
Link link = null;
Link cur = first;
Link next = null;
Link previous = null;
while(cur != null){
if(cur.age == key){
link = cur;
break;
}else if(cur.next == null){
//当前链结点不是要找的目标且下一个链结点为null,则证明没有找到目标
break;
}
//当前链结点不是要找的目标且存在下一个链结点,则向后继续寻找
previous = cur;
next = next.next;
cur = cur.next;
}
return link;
}
//判空
public boolean isEmpty(){
return (first == null);
}
}
双端链表
首先需要注意,双端链表与双向链表是完全不同的两个概念。
双端链表与单链表的区别在于它不只第一个链结点有引用,还对最后一个链结点有引用,如下图所示:
对最后一个链结点的引用允许像插入表头那样在表尾插入一个链结点,使用双端链表更适合于一些单链表不方便操作的场合,队列的实现就是这样一个情况。
下面是双端链表的实现类,与单链表不同的是,DoubleEndList类多了指向表尾的引用,并且多了插入表尾的操作。
//链表的封装类
public class DoubleEndList {
private Link first; //指向链表中的第一个链结点
private Link last; //指向链表中最后一个链结点
public DoubleEndList(){
first = null;
last = null;
}
//插入到链表的前端
public void insertFirst(Link link){
if(isEmpty()){ //如果为空链表,则插入的第一个链结点既是表头也是表尾
last = link;
}
link.next = first;
first = link;
}
//插入到链表的末端
public void insertLast(Link link){
if(isEmpty()){ //如果为空链表,则插入的第一个链结点既是表头也是表尾
first = link;
}else{
last.next = link;
}
last = link;
}
//删除第一个链结点,返回删除的链结点引用
public Link deleteFirst() throws Exception{
if(isEmpty()){
throw new Exception("链表为空!不能进行删除操作");
}
Link temp = first;
if(first.next == null){
last = null; //如果只有一个链结点,则删除后会影响到last指针
}
first = first.next;
return temp;
}
//打印出所有的链表元素
public void displayList(){
Link cur = first;
while(cur != null){ //循环打印每个链结点
cur.displayLink();
cur = cur.next;
}
}
//判空
public boolean isEmpty(){
return (first == null);
}
}
双端链表可以插入表尾,但是仍然不能方便的删除表尾,因为我们没有方法快捷地获取倒数第二个链结点,双端链表没有逆向的指针,这一点就要靠双向链表来解决了。
有序链表
对于某些应用来说,在链表中保持数据有序是很有用的,具有这个特性的链表叫作有序链表。
通常,有序链表的删除只限于最大值或最小值,不过,有时候也会查找和删除某一特定点,但这种操作对于有序链表来说效率不高。
有序链表优于有序数组的地方在于插入的效率更高(不需要像数组那样移动元素),另外链表可以灵活地扩展大小,而数组的大小是固定的。但是这种效率的提高和灵活的优势是以算法的复杂为代价的。
下面我们给出一个有序单链表的实现类。
//有序链表的封装类
public class SortedList {
private Link first; //指向链表中的第一个链结点
public SortedList(){
first = null;
}
//插入
public void insert(Link link){
Link previous = null;
Link cur = first;
while(cur != null && link.age>cur.age){ //链表由大到小排列
previous = cur;
cur = cur.next;
}
if(previous == null){ //如果previous为null,则证明当前链结点为表头
this.first = link;
}else{
previous.next = link;
}
link.next = cur;
}
//删除第一个链结点,返回删除的链结点引用
public Link deleteFirst() throws Exception{
if(isEmpty()){
throw new Exception("链表为空!不能进行删除操作");
}
Link temp = first;
first = first.next;
return temp;
}
//打印出所有的链表元素
public void displayList(){
Link cur = first;
while(cur != null){ //循环打印每个链结点
cur.displayLink();
cur = cur.next;
}
}
//判空
public boolean isEmpty(){
return (first == null);
}
}
该实现类与单链表的差别在于插入操作,因为是有序的,所以需要先找到要插入的位置,然后才能进行插入。
有序链表可以用于一种高效的排序机制。假设有一个无序数组,如果从这个数组中取出数据插入有序链表,他们会自动按照顺序排列,然后把它们从有序链表中重新放入数组,该数组就变成了有序数组。这种排序方法比在数组中常用的插入排序效率更高,因为这种方式进行的复制次数少一些。
双向链表
下面来讨论一种更复杂的链表结构:双向链表。
传统链表存在着一个潜在的问题,就是没有反向引用,由上一个链结点跳到下一个链结点很容易,而从下一个链结点跳到上一个链结点很困难。
双向链表提供了这种能力,即允许先后遍历,也允许向后遍历。实现这种特性的关键在于每个链结点既有下一个链结点的引用,也有上一个链结点的引用,如下图所示:
诚然,双向链表有其自身的优势,但是任何优势都要付出一定的代价,比如双向链表的插入和删除会变得更复杂,因为同时要处理双倍的指针变化。例如,我们在进行表头插入的操作示意图如下:
在表尾插入与之类似,是表头插入的镜像操作。
在表头表尾之外的其他位置插入新的链结点的示意图如下:
对于删除操作,如果要删除表头或表尾,比较简单,如果要删除指定值的链结点比较复杂,首先需要定位到要删除的链结点,然后改变了其前后链结点的指针,示意图如下:
下面给出双向链表的实现类,与前面的链表不同的是,链结点的封装类需要做些改变,之前的链结点类只包含向后的引用next,在双向链表中还需要加入向前的引用previous。
//链结点的封装类
public class Link {
public int age;
public String name;
public Link next; //指向下一个链结点
public Link previous; //指向前一个链结点
//构造方法
public Link(int age,String name){
this.age = age;
this.name = name;
}
//打印该链结点的信息
public void displayLink(){
System.out.println("name:"+name+",age:"+age);
}
}
//双向链表的封装类
public class DoublelyLinkList {
private Link first; //指向链表中的第一个链结点
private Link last; //指向链表中的最后一个链结点
public DoublelyLinkList(){
first = null;
last = null;
}
//插入到链表的前端
public void insertFirst(Link link){
if(isEmpty()){ //如果为空链表,则插入的第一个链结点既是表头也是表尾
last = link;
}else{ //如果不是空链表,则将链表的first指针指向该链结点
first.previous = link;
}
link.next = first;
first = link;
}
//插入到链表的末端
public void insertLast(Link link){
if(isEmpty()){ //如果为空链表,则插入的第一个链结点既是表头也是表尾
first = link;
}else{
last.next = link;
link.previous = last;
}
last = link;
}
//删除第一个链结点,返回删除的链结点引用
public Link deleteFirst() throws Exception{
if(isEmpty()){
throw new Exception("链表为空!不能进行删除操作");
}
Link temp = first;
if(first.next == null){
last = null; //如果只有一个链结点,则删除后会影响到last指针
}else{ //如果至少有两个链结点,则将第二个链结点的previous设为null
first.next.previous = null;
}
first = first.next;
return temp;
}
//删除最后一个链结点,返回删除的链结点引用
public Link deleteLast() throws Exception{
if(isEmpty()){
throw new Exception("链表为空!不能进行删除操作");
}
Link temp = last;
if(last.previous == null){
first = null; //如果只有一个链结点,则删除后会影响到first指针
}else{ //如果至少有两个链结点,则将倒数第二个链结点的next设为null
last.previous.next = null;
}
last = last.previous;
return temp;
}
//查找属性为指定值的链结点
public Link find(int key){
Link cur = first;
while(cur != null && cur.age != key ){
if(cur.next == null){
return null; //当前链结点不是要找的目标且已到达表尾
}
cur = cur.next;
}
return cur;
}
//在指定链结点之后插入,操作成功返回true,操作失败返回false
public boolean insertAfter(Link link){
Link target = find(link.age);
boolean flag = true;
if(target == null){ //没找到插入的参照链结点
flag = false;
}else{ //找到了插入的参照链结点
if(target.next == null){ //参照链结点为表尾
insertLast(link);
}else { //该链表至少有两个链结点
target.next.previous = link;
link.next = target.next;
//必须执行完上面两步,才能执行下面这两步
//上面两步处理了link和它下一个链结点的关系
//下面两步处理了link和它上一个链结点的关系
target.next = link;
link.previous = target;
}
}
return flag;
}
//打印出所有的链表元素
public void displayList(){
Link cur = first;
while(cur != null){ //循环打印每个链结点
cur.displayLink();
cur = cur.next;
}
}
//判空
public boolean isEmpty(){
return (first == null);
}
}