【前言】
两个数据结构:顺序表和链表
- 数据结构是一门学科(逻辑很严谨的学科),和语言没有关系,如C语言数据结构和Java数据结构,这里的C语言和Java只是一门工具,我们只是通过某一种语言来学习这门学科。
- 数据+结构:一种描述和组织数据的方式。
那为甚麽有那么多的数据结构呢?
答:因为组织和描述数据的方式是不一样的,比如有些数据结构适合增、有些适合删、有些适合查、有些适合改等,所以才有了这么多的数据结构。
一、线性表
线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构,常 见的线性表:顺序表、链表、栈、队列、字符串… 线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存 储时,通常以数组和链式结构的形式存储。
二、顺序表
概念及结构
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构(顺序表在逻辑上和物理上都是连续的),一般情况下采用数组存储。在数组上完成。顺序表从本质上来说就是一个数组。 数据的增删查改。 顺序表一般可以分为: 静态顺序表:使用定长数组存储。 动态顺序表:使用动态开辟的数组存储。 静态顺序表适用于确定知道需要存多少数据的场景. 静态顺序表的定长数组导致N定大了,空间开多了浪费,开少了不够用. 相比之下动态顺序表更灵活, 根据需要动态的分配空间大小.
接口实现
实现一个动态顺序表. 以下是需要支持的接口.
public class TestSequenceList {
public void display(){ }
public void add(int pos,int data){ }
public boolean contains(int toFind){return true ;}
public int search(int toFind){return -1;}
public int getPos(int pos){return -1;}
public void setPos(int pos,int value){ }
public void remove(int toRemove){ }
public int size(){return 0;}
public void clear(){ }
}
在实现接口之前我们先通过画图举一个例子来理解一下实现的思路
顺序表接口的实现代码
public class MyArrayList {
public int[] elem;
public int usedSize;
public static int capacity=10;
public MyArrayList(){
this.elem=new int[capacity];
}
public boolean isFull(){
if(this.usedSize==capacity){
return true;
}
return false;
}
public void add(int pos,int data){
if(pos<0 || pos>this.usedSize){
System.out.println("pos的插入位置不合法");
}
if (isFull()) {
this.elem= Arrays.copyOf(this.elem,2*capacity);
capacity*=2;
}
for(int i=this.usedSize-1;i>=pos;i--){
this.elem[i+1]=this.elem[i];
}
this.elem[pos]=data;
this.usedSize++;
}
public void display(){
for(int i=0;i<this.usedSize;i++){
System.out.print(this.elem[i]+" ");
}
}
public boolean isEmpty(){
return this.usedSize==0;
}
public boolean contains(int toFind){
if(isEmpty())
return false ;
for(int i=0;i<this.usedSize;i++){
if(this.elem[i]==toFind){
return true;
}
}
return false;
}
public int search(int toFind){
if(isEmpty()) return -1;
for(int i=0;i<this.usedSize;i++){
if(this.elem[i]==toFind){
return i;
}
}
return -1;
}
public int getPos(int pos){
if(isEmpty()) {
throw new RuntimeException("顺序表是空的");
}
if(pos<0 || pos>=this.usedSize){
System.out.println("pos不合法");
throw new RuntimeException("顺序表是空的");
}
return this.elem[pos];
}
public void setPos(int pos,int value){
if(pos<0 || pos>=this.usedSize){
System.out.println("pos不合法");
return;
}
if(isEmpty()){
System.out.println("顺序表为空!");
return ;
}
this.elem[pos]=value;
}
public void remove(int toRemove){
if(isEmpty()) return;
int index=search(toRemove);
if(index==-1){
System.out.println("没有你要删除的数字!");
return;
}
for(int i=index;i<this.usedSize-1;i++){
this.elem[i]=this.elem[i+1];
}
this.usedSize--;
}
public int size(){
return this.usedSize;
}
public void clear(){
for(int i=0;i<this.usedSize;i++){
this.elem[i]=0;
}
this.usedSize=0;
}
}
删除数据的代码思路:
顺序表接口的测试代码
public class TestMyArrayList {
public static void main(String[] args) {
MyArrayList myArrayList=new MyArrayList();
myArrayList.add(0,1);
myArrayList.add(1,2);
myArrayList.add(2,3);
myArrayList.add(3,4);
myArrayList.remove(1);
myArrayList.clear();
myArrayList.display();
System.out.println(myArrayList.contains(5));
System.out.println(myArrayList.contains(4));
System.out.println(myArrayList.search(4));
System.out.println(myArrayList.search(5));
System.out.println(myArrayList.getPos(1));
System.out.println(myArrayList.getPos(10));
}
}
测试打印结果
打印没有测试删除(remove)接口的结果如下:
打印测试删除(remove)之后的结果如下:
打印测试清空顺序表clear接口之后的结果:
思考:顺序表增删查改的时间复杂度?
顺序表的优缺点其实就是数组的优缺点。
对顺序表来说:
1.如果要往顺序表里插入一个元素的时候,需要前后移动元素,最坏的情况就是往0下标放元素,需要把所有元素都移一遍.所以插入一个元素的时间复杂度的最坏情况是O(N)。
2.如果要删除一个元素,后面的元素需要往前移,最坏的情况就是删除第一个元素,此时需要把所有元素都往前移一遍,所以删除一个元素的时间复杂度的最坏情况是O(N)。
3.如果要查找一个元素:需要分两种情况
第一种:给定关键字:给定关键字需要一个一个找,所以它的时间复杂度是O(N);
第二种:给定下标:给定下标,直接确定元素的下标位置,找一次就能找到了,所以它的时间复杂度是O(1);
4.如果要修改一个元素,就需要给定下标才能改,所以它的时间复杂度是O(1);
所以,顺序表的优点:方便查找和修改;
如果经常涉及到查找和修改的问题,适合使用顺序表。
顺序表的缺点:1.不容易(不方便)插入和删除;
? 2.如果顺序表在满的情况需要插入数据,此时需要给顺序表扩容至2倍大,但如果只插入一个数据,那扩容之后的顺序表就只用了一个元素的空间,剩下的空间不用,就造成了浪费。
因此,如果经常涉及到插入和删除的问题,不适合使用顺序表,适合使用链表,因为链表的特点是随用随取,不会造成空间浪费;
总结:
- 顺序表中间/头部的插入删除,时间复杂度为O(N)
- 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
- 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。
如何解决以上问题呢?下面给出了链表的结构来看看
三、链表
什么是链表?
链表:就是把数据以链式的形式串起来。
单链表的组织方式是由节点来组织数据(也就是由一个一个节点串起来的),每一个节点分为data域(数据域)和next域(下一个节点的引用)(也就是说每一个节点是由数值域和下一个节点的引用(地址)构成),分别存储当前节点的数据(如8,13等)和当前节点的下一个节点的地址(如0x325,0x673等),再用链表把它给串起来,最后一个节点没有下一个了,因此它的next域就是null。
通过画图来解决这个问题和上面这段话以及单链表和单链表的结构可能更好一点!
带头结点的单向非循环链表
带头节点的单向循环链表
不带头节点的单向非循环链表和带头节点的单向非循环链表区别是什么呢?
- 不带头节点的单向非循环链表虽然在第一个节点的位置标注了它的头(head),但那只是暂时的,在将来有可能当前节点就不是它的头了;
- 带头节点的单向非循环链表的data域是没有值的,不管怎样它就是单链表的头,并且不会发生改变;
- 因此他俩的唯一区别就是带头节点的单向非循环链表有一个专门来标识单链表的头的,不管在哪个地方放数据都不能把这个头部给换掉,相当于蛇头。不带头节点的单向非循环链表的头是不固定的,比如在当前节点的位置标注是它的头,如果把这个标注是它的头所存储的数据删掉,那它的头就移到了下一个节点的位置上,相当于没有头的 蛇,但对于带头节点的单向非循环链表的头,是不能删除的,只能删除它后面的节点。
- 带头节点的单向非循环链表:最后一个节点next域是null;
- 带头节点的单向循环链表:最后一个节点的next域不为空,才能循环,同时它的头不能发生改变,因此带头节点的单向循环链表比较简单。
我们重点研究不带头节点的单向非循环链表,因为带头的比较容易,不带头的研究会了,带头的就根本不是问题。
不带头节点的单向非循环列表
首先先把模型(类)写好:单链表由节点组成,所以要将节点抽象出来
链表是由一个一个节点组成的,每一个节点都有data域和next域,可以先把一个一个节点抽象出来,然后用类给它创建出来,有了类就可以产生这些对象了。
而每一个节点都是对象,它有两个属性,一个是data,一个是next;
对于属性data来说,可以是整型、字符型等,假设定义data为int类型;
next域保存的是节点地址,那它的类型就是节点类型,假设定义next的类型是Node类型
class Node{
public int data;
public Node next;
public Node(int data){
this.data=data;
this.next=null;
}
}
- Node类型的next,为什么next是Node类型的?因为由next来保存下一节点的地址的话说明next也得是节点类型类似于Person p1=new Person();
如何产生一个节点?
如下图
△头插法addFirst方法
逻辑分析:使用头插法将生成的这个节点插入到单链表的头部,它的头就是head所指向的对象(这里先不考虑第一次插入)
要想更加全面就要把第一次插入的情况也考虑进去,那么如果是第一次插入节点,逻辑是怎样的?代码又该怎样写呢?
逻辑分析如图:
头插法的完整代码及解析如图:
头插法addFirst方法代码示例
public class MyLinkedList {
public Node head;
public void addFirst(int data){
Node node=new Node(data);
if(this.head==null){
this.head=node;
return;
}
node.next=this.head;
this.head=node;
}
△尾插法addLast方法
尾插法:从单链表的尾部插入节点。
不考虑第一个节点为空的情况下的尾插法
逻辑分析如下:
代码示例
public void addLast(int data){
Node node=new Node(data);
if(this.head==null){
this.head=node;
return ;
}
Node num=this.head;
while(num.next!=null){
num=num.next;
}
num.next=node;
}
考虑第一个节点为空的情况
△任意位置插入addIndex方法
任意位置插入addIndex方法,第一个数据节点为0号下标
任意位置插入addIndex方法的逻辑,是addIndex有利于更好的利用头插法和尾插方法的使用。
逻辑分析:想要往2号位置插入就要找到2号位置的前一节点即1号位置,因此就需要定义一个num先走到一号位置。
结合以上逻辑代码分析可以得出其步骤为:
1、先判断是否为头插或者尾插
2、定义一个num走index-1步,找到index位置的前一个节点的地址
3、找到之后,进行插入,插入的代码为
任意位置插入addIndex方法,代码示例
public void addIndex(int index,int data){
if(index==0){
addFirst(data);
return;
}
if(index==this.size()){
addLast(data);
return;
}
Node node=new Node(data);
Node num=searchIndex( index);
node.next=num.next;
num.next=node;
}
private Node searchIndex(int index){
if(index<0||index>this.size()){
throw new RuntimeException("index位置不合法!");
}
Node num=this.head;
while(index-1!=0){
num=num.next;
index--;
}
return num;
}
查找是否包含关键字key是否在单链表当中 contains方法
代码示例:
public boolean contains(int key){
Node num=this.head;
while(num!=null){
if(num.data==key){
return true;
}
num=num.next;
}
return false;
}
△删除第一次出现关键字为key的节点 remove方法
逻辑分析:我们可以倒着推。
问题1:假设我们将35从这个列表中删除,需要考虑一些什么情况才能把它删除呢?
如果将保存0x890这个节点的地址变成保存0x223这个地址,那么就说明跳过了保存35这个数据的节点的地址0x890,删除这个节点也就意味着把35删除了
但是要想删除35这个节点,就必须先定义一个引用找到35前面的一个节点,然后将35前面的一个节点的next等于0x223就行了。
假设已经找到了这个节点,给它定义为prev,prev指向的就是你要删除的这个35这个节点的前驱,找到前驱之后,就可以将前驱的next值等于当前你要删除的这个节点的next值,将要删除的这个节点定义为del,那么此时写成代码就是prev.next=del,next,此时他就指向了0x223这个节点,而0x890这个节点就被删除了。
步骤总结:
首先你要知道你要删除的这个数据的节点del;
其次,你要找到你要删除的这个节点的前驱prev;
最后,让前驱的next值等于要删除的这个数据的节点next值。
问题2:那么如何知道他的前驱在哪呢?
如果想要删除35这个节点,需要定义一个引用prev从头开始走,那么如何判断prev指向了要删除的这个节点的前驱了呢?(先不考虑删除的节点是头结点)需要判断prev的下一个节点的数据,写成代码就是prev.next.data,
prev指向的是head即0x124这个节点,他的next是0x325这个值,prev.next代表prev的下一个节点,所以判断prev.next.data指向的值是否等于35即可,如果等于,就说明prev所指的就是35这个节点的前驱,此时就找到了要删除的这个节点的前驱prev,写成代码就是prev.next==35
前驱找到之后,del也就出现了,他等于当前prev的next值,写成代码就是Node del=prev.next.
根据前面的分析,使用prev.next=del,next,此时prev.next就指向了0x223这个节点。
代码如下:
Node prev=searchPrev(key);
if(prev==null){
System.out.println("表示没有这个节点");
return;
}
Node del=prev.next;
prev.next=del.next;
}
步骤总结:
首先先让prev指向head,代码为prev=this.head;
其次判断prev.next.data指向的值是否等于你要删除的key,代码为if(prev.next.data== key),如果相等,就返回pretv,如果不相等,prev往后走,代码为:prev=prev.next,想要执行这个描述还需要一个循环,此时就需要考虑循环条件了,我们要找的是要删除节点的前驱,如果prev走到最后一个节点了,说明不存在prev.next.data==key,而prev的后面已经没有节点了,此时就会发生空指针异常,所以循环条件应该是while(prev.next!=null)。
如果整个循环都结束了还没有return出去,说明没有找到要删除的那个节点的前驱。那么就返回null,可以把这部分内容封装成一个函数。而这个函数的作用就是找关键字的前驱。
部分代码如下:
private Node searchPrev(int key){
Node prev=this.head;
while(prev.next!=null){
if(prev.next.data==key){
return prev;
}else{
prev=prev.next;
}
}
return null;
}
问题3:前面讨论的都是删除头结点后面的节点,如果想要删除第一个节点该怎么做呢?
假设要删除8这个节点,可以这样想,直接将head往后一挪就行了,此时8这个数据所在的节点就被删除了。
写成代码就是
public void remove(int key){
if(this.head==null){
return ;
}
if(this.head.data==key){
this.head=this.head.next;
return ;
}
完整代码如下:
private Node searchPrev(int key){
Node prev=this.head;
while(prev.next!=null){
if(prev.next.data==key){
return prev;
}else{
prev=prev.next;
}
}
return null;
}
public void remove(int key){
if(this.head==null){
return ;
}
if(this.head.data==key){
this.head=this.head.next;
return ;
}
Node prev=searchPrev(key);
if(prev==null){
System.out.println("表示没有这个节点");
return;
}
Node del=prev.next;
prev.next=del.next;
}
测试打印结果:
△删除所有值为key的节点removeAllKey方法
完整代码如下:
public void removeAllKey(int key){
Node prev=this.head;
Node cur=prev.next;
while(cur!=null){
if(cur.data==key){
prev.next=cur.next;
cur=cur.next;
}else{
prev=cur;
cur=cur.next;
}
}
if(this.head.data==key){
this.head=this.head.next;
}
}
得到单链表的长度 size方法
public int size(){
int count=0;
Node num=this.head;
while(num!=null){
count++;
num=num.next;
}
return count;
}
打印单链表display方法
打印单链表:遍历链表(访问链表中的每一个元素即访问每一个节点中的data域)
public void display(){
Node num=this.head;
while(num!=null){
System.out.print(num.data+" ");
num=num.next;
}
System.out.println( );
}
清除单链表clear方法
public void clear(){
this.head=null;
}
△链表接口的实现代码
public class MyLinkedList {
public Node head;
public void addFirst(int data){
Node node=new Node(data);
if(this.head==null){
this.head=node;
return;
}
node.next=this.head;
this.head=node;
}
public void addLast(int data){
Node node=new Node(data);
if(this.head==null){
this.head=node;
return ;
}
Node num=this.head;
while(num.next!=null){
num=num.next;
}
num.next=node;
}
public void addIndex(int index,int data){
if(index==0){
addFirst(data);
return;
}
if(index==this.size()){
addLast(data);
return;
}
Node node=new Node(data);
Node num=searchIndex( index);
node.next=num.next;
num.next=node;
}
private Node searchIndex(int index){
if(index<0||index>this.size()){
throw new RuntimeException("index位置不合法!");
}
Node num=this.head;
while(index-1!=0){
num=num.next;
index--;
}
return num;
}
public boolean contains(int key){
Node num=this.head;
while(num!=null){
if(num.data==key){
return true;
}
num=num.next;
}
return false;
}
private Node searchPrev(int key){
Node prev=this.head;
while(prev.next!=null){
if(prev.next.data==key){
return prev;
}else{
prev=prev.next;
}
}
return null;
}
public void remove(int key){
if(this.head==null){
return ;
}
if(this.head.data==key){
this.head=this.head.next;
return ;
}
Node prev=searchPrev(key);
if(prev==null){
System.out.println("表示没有这个节点");
return;
}
Node del=prev.next;
prev.next=del.next;
}
public void removeAllKey(int key){
Node prev=this.head;
Node cur=prev.next;
while(cur!=null){
if(cur.data==key){
prev.next=cur.next;
cur=cur.next;
}else{
prev=cur;
cur=cur.next;
}
}
if(this.head.data==key){
this.head=this.head.next;
}
}
public int size(){
int count=0;
Node num=this.head;
while(num!=null){
count++;
num=num.next;
}
return count;
}
public void display(){
Node num=this.head;
while(num!=null){
System.out.print(num.data+" ");
num=num.next;
}
System.out.println( );
}
public void clear(){
this.head=null;
}
}
△链表接口的测试代码
public class TestMyLinkedList {
public static void main(String[] args) {
MyLinkedList myLinkedList=new MyLinkedList();
myLinkedList.addFirst(10);
myLinkedList.addFirst(16);
myLinkedList.addFirst(23);
myLinkedList.addFirst(46);
myLinkedList.addFirst(37);
myLinkedList.addLast(12);
myLinkedList.addLast(19);
myLinkedList.display();
myLinkedList.addIndex(5,99);
myLinkedList.display();
boolean flg=myLinkedList.contains(10);
System.out.println(flg);
System.out.println(myLinkedList.contains(9));
System.out.println(myLinkedList.size());
myLinkedList.addLast(13);
myLinkedList.addLast(8);
myLinkedList.addLast(16);
myLinkedList.addLast(13);
myLinkedList.addLast(35);
myLinkedList.addLast(13);
myLinkedList.addLast(13);
myLinkedList.addLast(9);
myLinkedList.addLast(19);
myLinkedList.display();
System.out.println("删除13");
myLinkedList.removeAllKey(13);
myLinkedList.display();
myLinkedList.addLast(10);
myLinkedList.addLast(16);
myLinkedList.addLast(23);
myLinkedList.addLast(46);
myLinkedList.addLast(37);
myLinkedList.addLast(12);
myLinkedList.addLast(19);
myLinkedList.display();
System.out.println("删除10");
myLinkedList.remove(10);
myLinkedList.display();
System.out.println("删除46");
myLinkedList.remove(46);
myLinkedList.display();
System.out.println("删除19");
myLinkedList.remove(19);
myLinkedList.display();
myLinkedList.addLast(10);
myLinkedList.addLast(16);
myLinkedList.addLast(23);
myLinkedList.addLast(46);
myLinkedList.addLast(37);
myLinkedList.addLast(12);
myLinkedList.addLast(19);
myLinkedList.display();
myLinkedList.clear();
}
}
四、 顺序表和链表的区别和联系
顺序表
1、空间连续、支持随机访问。
2、中间或前面部分的插入删除时间复杂度为O(N)
3、增容的代价比较大。
链表:
1、以节点为单位存储,不支持随机访问
2、任意位置插入删除时间复杂度为O(1)
3、没有增容问题,插入一个开辟一个空间。
|