数组
先由一个例子复习一下数组的操作:
class HighArray
{
private long[] a;
private int nElems;
//-----------------------------------
public HighArray(int max) //构造函数
{
a=new long[max];
nElems = 0;
}
//-----------------------------------
public boolean find(long searchKey) //查找元素
{
int j;
for(j=0;j<nElems;j++)
if(a[j]==searcheKey)
break; //退出for循环,记录了j的值
if(j==nElems)
return false; //没找到
else
return true; //找到了
}
//--------------------------------------
public void insert(long value) //插入元素
{
a[nElems]=value;
nElems++;
}
//--------------------------------------
public boolean delete(long value) //删除元素
{
int j;
for(j=0;j<nElems;j++)
if(value=a[j])
break;
if(j=nElems)
return false; //不存在那个元素
else
{
for(int k=j;k<nElesm;k++)
a[k]=a[k+1]; //遍历,从查找的那个位置起,把数组中的元素向前挪动一位
nElems--; //总长度减一
return true;
}
} //end delete()
//----------------------------------------
public void display()
{
for(int j=0;j<nElems;j++)
System.out.println(a[j]+" ");
System.out.rpintln("");
}
}
接着在客户端始测试 数组的 增 删 查 操作
class HighArrayApp
{
public static void main(String[] args)
{
int maxSize=100;
HighArray arr;
arr= new HightArray(maxSize);
arr.insert(57);
arr.insert(27);
arr.insert(17);
arr.insert(67);
arr.insert(87);
arr.insert(33);
arr.insert(22);
arr.insert(11);
arr.insert(66);
arr.insert(66);
int searchKey=35;
if(arr.find(searchKey))
System.out.println("Found"+searchKey);
else
System.out.println("Can‘t found"+searchKey);
arr.delete(55);
arr.delete(66);
arr.display();
}
}
上例演示了一个数组增删查的过程,它的特点是高度的抽象,操作“增删改”都被封装到 HightArray类中,并且暴露出接口(insert(),delete(),display())供用户使用,这里的用户就是HightArrayApp,它只需调用方法完成操。
接着复习一下数组的二分查找
二分查找适用于有序数组,关于排序我在上一节博文有写,那么今天在这里默认数组有序的情况学习一下二分查找把
class OrdArray
{
private long[] a;
private int nElems;
//-----------------------------------
public HighArray(int max)
{
a=new long[max];
nElems = 0;
}
//-----------------------------------
public int size()
{
return nElems
}
//-----------------------------------
public int find(long searchKey) //查找元素
{
int lowerBound=0;
int upperBound=nElems-1;
int curIn;
while(true)
{
curIn=(lowerBound+upperBound)/2;
if(a[curIn]==searchKey)
return curIn;
else if(lowerBound>upperBound)
return nElems;
else
{
if(a[curIn]<searchKey)
lowerBound=curIn+1;
else
upperBound=curIn-1;
}
}
}
//--------------------------------------
public void insert(long value) //插入元素
{
int j;
for(j=0;j<nElmes;j++)
if(a[j]>value);
break;
for(int k=nElems;k>j;k--)
a[k]=a[k-1];
a[j]=value;
nElems++;
}
//--------------------------------------
public boolean delete(long value) //删除元素
{
int j;
for(j=0;j<nElems;j++)
if(value=a[j])
break;
if(j=nElems)
return false; //不存在那个元素
else
{
for(int k=j;k<nElesm;k++)
a[k]=a[k+1]; //遍历,从查找的那个位置起,把数组中的元素向前挪动一位
nElems--; //总长度减一
return true;
}
} //end delete()
//----------------------------------------
public void display()
{
for(int j=0;j<nElems;j++)
System.out.println(a[j]+" ");
System.out.rpintln("");
}
}
接着在客户端测试 数组的 增 删 查 操作
class HighArrayApp
{
public static void main(String[] args)
{
int maxSize=100;
HighArray arr;
arr= new HightArray(maxSize);
arr.insert(57);
arr.insert(27);
arr.insert(17);
arr.insert(67);
arr.insert(87);
arr.insert(33);
arr.insert(22);
arr.insert(11);
arr.insert(66);
arr.insert(66);
int searchKey=35;
if(arr.find(searchKey)!=arr.size())
System.out.println("Found"+searchKey);
else
System.out.println("Can‘t found"+searchKey);
arr.display();
arr.delete(55);
arr.delete(66);
arr.display();
}
}
数组的问题
- 在一个无序数组中插入只需要O(1)的时间,查找却需要O(N)
- 在一个有序数组中查找只需要O(logN),但插入却需要O(N)
- 不论是有序还是无序,删除操作,都需要花费O(N)
- 一旦数组被创建,大小就被固定住,数组初始化的大小不容易控制
链表
既然数组作为数据存储结构有一定的缺陷,那么接下来将介绍一种新的数据存储结构:链表。
链结点
在链表中,每个数据项都obeisance包含在链结点Link中。一个链结点是某个类的对象,这个类就叫做Link,因为一个链表中有许多类似的链结点,所以需要使用不同于链表的类来表达链结点。
每个Link对象都包含对下一个链结点引用的字段,通常叫做next。
但是链表对象包含有对第一个链结点的引用。
关系而非位置
数组是根据下标号直接访问,在链表中,寻找一个特定元素的唯一方法就是沿着这个元素链从头开始一直向下寻找。从第一项开始,刀第二个,然后到第三个。
单链表
这个链表仅有的操作是:
- 在链表头插入一个数据线
- 在链表头删除一个数据线
- 遍历链表显示它的内容
下面我们来看一下代码描述:
Link类 是链结点的抽象描述
class Link
{
public int iData;//假设Link中的数据是int和double,实际有可能是Object对象
public double dData;
public Link next; //这里看上去很突兀,但实际是一个链结点的引用,不是"Link中包含了一个Link"
//------------------------------
public Link(int id,double dd)
{
iData=id;
dData=dd;
}
//-------------------------------
public void display()
{
System.out.print("{"+iData+","+dData+"}");
}
} //end class Link
LinkList是链表的抽象
class LinkList
{
private Link first; //头结点的引用
//-------------------------------
public LinkList()
{
first=null;
}
//-------------------------------
public void insertFirst(int id,double dd)
{
//插入新结点的过程:
//first已经指向了链表的第一个结点,插入新的结点:
//1.创建的newLink结点的next等于first;
//2.然后改变first的值,使得first指向新创建的链结点
Link newLink =new Link(id,dd);
newLink.next=first;
first=newLink;
}
//-------------------------------
public Link deletefirst()
{
//通过把first重新指向第二个链结点,删除和第一个链结点的链接,记住first是链表的属性,next是链结点的属性
Link temp=first;
first=first.next; //如何删除:first-->old next
return temp; //返回删除的链结点Link
}
//---------------------------------
public Lind find(int key) //查找某个元素
{
Link current =first;
while(current.iData!=key)
{
if(current.next==null)
return null;
else
current =current.next;
}
return current;
}
//--------------------------------
public Link delete(int key) //删除某个元素
{
Link current =frist;
Link previous=first;
while(current.iData!=key)
{
if(current.next==null)
return null;
else
{
previous=current;
current=current.next;
}
}
if(current==first)
first=first.next;
else
previous.next=current.next;
return current;
}
//---------------------------------
public void displayList() //输出
{
System.out.print("List(first-->last): ");
Link current=first;
while(current!=null)
{
current.displayLink();
current=current.next;
}
System.out.println(" ");
}
} //end class LinkList
LinkListApp是测试链表和链结点的客户端
class LinkListApp
{
public static void main(String[] args)
{
LinkList theList=new LinkList();//创建新的链表
theList.insertFirst(22,2.33);
theList.insertFirst(44,4.33);
theList.insertFirst(55,3.33);
theList.insertFirst(88,34.33);
theList.displayList();
Link f=theList.find(44);
if(f!=null)
System.out.println("Found Link with key "+f.iData);
else
Systemm.out.println("Can‘t find link");
Link d=theList.delete(88);
theList.displayList();
while(!theList.isEmpety())
{
Link aLink=theList.deleteFirst();
System.out.print("Deleted");
aLink.displayLink();
System.out.println(" ");
}
theList.displayList();
} //end main()
}
双端链表
双端链表与单链表的唯一区别是,增加了对最后一个链结点的引用。
这样就允许在表尾插入一个链结点。
当然我们也可以遍历整个链表直到表尾再插入。显然双端链表这样在末尾插入链结点效率更高。
有序链表
有序链表优于有序数组的地方就是插入的速度,因为链表的插入是不需要移动元素的,链表也可以扩展内存,数组的内存是固定的。
有序链表的缺点就是实现起来稍微复杂。
双向链表
双向链表的优点 就是反向遍历非常简单。
为什么?
因为双向链表的每个链结点都有两个属性,分别指向前一个结点和指向后一个结点。
关于有序链表和双向链表的代码叙述有兴趣的可以自行查阅
链表的效率
在表头插入和删除速度很快,花费O(1);
查找,删除和在指定链结点后面插入都需要搜索表中一半以上的链结点,需要O(N)次比较,虽然数组执行这些操作演示O(N)次比较,但是链表仍然要快一些,因为插入删除链结点时,链表不需要移动,增加的效率是显著的,特别是复制实际远远大于比较实际的时候。
链表比数组的优越性还体现在:链表需要多少内存就可以用多少内存,并且可以扩展。
数组太大导致效率低下,数组太小,使用的时候有可能空指针;向量是可扩展的数组,它改变长度的方式是增量扩展,扩大一倍。内存效率上比链表低的多,
ADT 抽象数据类型
接下来讨论一个比链表更广泛的话题:抽象数据类型
简单来说,是一种考虑数据结构的方式:着重于它做了什么,而忽略它 是怎么做的。
栈和队列都是ADT的例子,数组和链表都可以实现它们。
用链表实现栈
栈的push()和pop()操作实际是通过数组操作完成的
arr[++top]=data;
data=arr[top- -];
而链表是类似于这样完成:
theList.insertFist(data);
data=theList.deleteFirst();
栈的使用者调用push()和pop()方法来插入和删除栈中的元素,它们不需要知道栈是用链表还是数组实现的
class Link
{
public long dData;
public Link next;
//--------------------------
public Link(long dd)
{
dData=dd;
}
//--------------------------
public void displayLink(){
System.out.print(dData+" ");
}
} //end class Link
class LinkList{
private Link first; //头结点的引用
//--------------------------------------
public LinkList()
{
first=null;
}
//--------------------------------------
public boolean isEmpty()
{
return fisrt==null;
}
//-------------------------------------
public void insertFirst(long dd)
{
Link newLink=new Link(dd);
newLink.next=first;
first=newLink;
}
//--------------------------------------
pubic long deleteFirst()
{
Link temp=first;
first=first.next;
return temp.dData;
}
//-------------------------------------
public voi displayList()
{
Link current =first;
while(current!=null)
{
current.displayLink();
current=current.next;
}
System.out.println("");
}
} //end class
class Stack
{
private LinkList theList;
//------------------------------
public Stack()
{
theList=new LinkList();
}
//------------------------------
public void push(long i)
{
theList.insertFirst(j);
}
//------------------------------
public void pop()
{
return theList.deleteFirst();
}
//------------------------------
public boolean isEmpty()
{
return theList.isEmpty();
}
//-------------------------------
public void displayStack()
{
System.out.print("Stack(top-->bottom):");
theList.displayList();
}
} //end class
class LinkStackApp
{
public static void main(String[] args)
{
LinkStack theStack=new LinkStack();
theStack.push(20);
theStack.push(40);
theStack.displayStack();
theStack.push(60);
theStack.push(80);
theStack.displayStack();
theStack.pop();
theStack.pop();
theStack.displayStack();
} //end main()
} //end class
观察:整个程序,LinkStackApp中的main方法只和LinkStack类有关,LinkStack类只和LinkList类有关。main()和LinkList类是不能通信的。
数据类型和抽象
抽象数据类型分为两步,首先看看“数据类型”再考虑“抽象”
数据类型可以表示内置的类型,比如int,double,也可以用类来创建自己的数据类型。
抽象这个词的意思是“不考虑细节的描述和实现”,抽象是事物的本质和重要特征。
当“抽象数据类型”用于描述栈和队列这样的数据额结构时,它的意义被进一步扩展了。它意味着类的用户不知道方法是怎样运作的,也不知道数据是如何存储的。
对于栈来说,用户只知道push()和pop()方法的存在是被用户使用的,但不需要知道内部的实现,以及内部数据是如何存储。
接口,在ADT中有一个经常被称为“接口”的规范,同城是类的公有方法,在栈中,push() 和pop()就形成了接口。