単一のリスト
データ構造は、連続したメモリ位置に格納された順次記憶素子のアドレスであり、ライト我々の以前の線状、スタック順序は、順次、循環キュー構造に格納されたコンピュータに格納されるデータの形式、二つがあるが、動的配列に基づいて実施しましたこの構造は、連続したメモリ位置、空間のより多くの廃棄物、記憶部に記憶された任意のデータ要素は、空間を完全に利用することができ、このストレージ構造が連結されるストレージ構造を占有します。
データ要素間の論理関係を反映していない鎖の記憶構造は、それがアドレスは、関連するデータ要素位置によって行うことができるように、ポインタアドレスとストアデータ要素に必要であるので、データが存在するチェーン店に多くの柔軟性限り、それを見つけるために、対応するアドレスを格納するポインタがある限り、重要ではありません。
データ要素Bとその直接の後継、データ要素、独自の記憶された情報に加えて、その後継者を示す情報を格納する必要の各データ要素間の論理的な関係を表現するために。我々は、データフィールドと呼ばれるドメイン情報データ要素、直後の位置がポインタフィールドと呼ばれるフィールドストアを格納します。ポインタ情報は、ポインタ又はチェーンと呼ばれる領域に記憶されています。呼ばれるノード(ノード)の二つの情報からなる画像データ要素を格納します。
n個のノード(メモリマップされた)リンクリストに、チェーン・ストレージ構造の、すなわち直線状、このリンクされたリスト内の各ノードがそう単鎖と呼ばれる、唯一のポインタフィールドを含むからです。
第1のノードと先頭ポインタ
リンクされたリストの最初のノードへの最初のノードを参照し、サブヘッダーの実際の第1のノードの仮想ノードが存在する
現実的なヘッドノードでは、データを記憶するための最初のノード
仮想ヘッドノード:最初のデータを記憶させノード
の方法は、頭部と尾ポインタは、単に参照変数、即ちポインタリストのヘッドノードと関係する最後のノードです。
実際には、我々はリストと注文フォームと基本的な動作はほとんどですが、ポイントの面で取っへの要素なので、単一のリストのためか、Listインタフェース上でそれを得ることができ、あなたは抽象メソッドインタフェースを無効にすることができました。
次の表は、Java言語に保存されているリニア鎖構造を達成するため、仮想ヘッドノードモードは、ストレージ構造のノードはこの事を必要とし、二つの内部クラス、クラス定義のノードの一部として、単一のクラスに入れて次に、ポインタデータフィールド及びデータ・フィールド。いくつかは実装されたリンクリストのノードの基本的な操作に基づいています。
package DS02.动态链表;
import DS01.动态数组.List;
import java.util.Iterator;
//用动态链表实现线性表 链表
//头插法:像栈
//尾插法:像队列
//头插尾插结合
public class LinkedList<E> implements List<E> {
private Node head; //链表的头指针
private Node rear; //链表的尾指针
private int size; //链表的元素个数(结点个数)
//构造函数
public LinkedList(){
head=new Node();
//刚开始给一个空的结点
rear=head; //头尾结点一样,即空表 再加结点头指针不动尾指针动就ok
}
//内部类
class Node{ //链表内部的东西,内部类私有,外部不需要知道结点的存在
E data; //数据域 类型由外界决定
Node next; //指针域
//构造函数
Node(){
this(null,null);
}
Node(E data,Node next){
this.data=data;
this.next=next;
}
@Override
public String toString() {
return data.toString(); //由调用者决定,结点的toString
}
}
@Override
public int getSize() {
return size;
}
@Override
public boolean isEmpty() {
return size==0&&head==rear;
}
}
这里比较重要的是,单链表中插入和删除元素,先对插入元素进行分析,有头插法,尾插法,头插尾插结合还有一般插入,分别分析其过程:
- 头插法: 先把虚拟头结点的指针域存放的物理地址给新结点的指针域,头结点的指针域指向新结点的地址,就完成了表头插入元素的操作
- 尾插法:先把新结点的物理地址给尾结点的指针域,新结点的地址给尾指针
- 头插尾插结合:头插和尾插在链表为空时,即插入第一个元素时实现方法是一样的,后面的元素进入就分头插尾插两方面
- 一半插入:借助指针p遍历找要插入的位置,把p指针指向结点下一结点的地址给新结点的指针域,再将新结点的地址给p结点的指针域
代码实现如下:(代码中没有头尾结合插入的代码)
//插入元素
@Override
public void add(int index, E e) {
if(index<0||index>size){ //角标越界
throw new IllegalArgumentException("角标越界");
}
//创建新的结点
Node n=new Node();
n.data=e; //结点的数据域存e
if(isEmpty()){ //空表状态 特殊处理
head.next=n;
rear=n;
}else if(index==0){ //头插法
n.next=head.next;
head.next=n;
}else if(index==size){ //尾插法
rear.next=n;
rear=n;
}else{ //中间插入
Node p=head;
for(int i=0;i<index;i++){
p=p.next;
}
n.next=p.next; //把p指针指向结点下一结点的地址给新结点的指针域
p.next=n; //新结点的地址给p结点的指针域
}
size++; //有效元素+1
}
@Override
public void addFirst(E e) {
add(0,e);
}
@Override
public void addLast(E e) {
add(size,e);
}
获取角标对应元素,对一些特殊情况进行判断,链表为空,角标越界时抛异常
//获取角标对应的元素
@Override
public E get(int index) {
if(isEmpty()){
throw new IllegalArgumentException("空表");
}
if(index<0||index>size){
throw new IllegalArgumentException("角标越界");
}
if(index==0){
return head.next.data;
}else if(index==size-1) {
return rear.data;
}else{
Node p=head; //借助指针p找到index对应结点
for(int i=0;i<=index;i++){
p=p.next;
}
return p.data; //返回指针p处结点数据域
}
}
@Override
public E getFirst() {
return get(0);
}
@Override
public E getLast() {
return get(size-1);
}
修改元素和查看是否包含该元素方法的前提是找到元素,借助指针p遍历链表查找角标index对应元素,实现代码如下:
@Override
public void set(int index, E e) {
if(isEmpty()){
throw new IllegalArgumentException("空表");
}
if(index<0||index>size){
throw new IllegalArgumentException("角标越界");
}
Node p=head;
for(int i=0;i<=index;i++){ //修改元素,先找再改
p=p.next;
}
p.data=e;
}
@Override
public boolean contains(E e) {
return find(e)!=-1;
}
@Override
public int find(E e) {
Node p=head;
for(int i=0;i<=size;i++){
p=p.next;
if(p.data.equals(e)){ //p指针结点处存放的元素与e比较
return i; //返回角标
}
}
return -1;
}
删除元素和插入元素一样分头删、尾删和中间删,其实三种方式的删除方法一样,都是断了要删除结点与其他结点的联系,让其被回收,下面具体讨论:
-
头删:创建变量del存放要删除的结点,也就是头指针处结点,把要删除元素结点的元素给变量ret,用于返回。让头结点指针域指向删除元素的下一个,再将del的指针域指向空,断了del和其他结点的联系,del会被回收器回收
-
尾删:要删除元素,要先找到要删除元素的前一个,将它的指针域指向空,将尾结点指针域指向空,尾结点会被回收器回收
-
中间删:先将要删除的元素用变量del存放,中间删除也是找要删除结点的前一个,借助指针p遍历寻找,将它的指针域指向del的下一个结点地址,即跳过del,再将del的指针域指向空,del被回收
还有一种特殊情况,就是链表内只有一个元素的情况,把它单独考虑一下,减小时间复杂度,在执行删除后记得让size-1
//删除结点
@Override
public E remove(int index) {
if(isEmpty()){
throw new IllegalArgumentException("空表");
}
if(index<0||index>=size){
throw new IllegalArgumentException("角标越界");
}
E ret=null;
if(size==1){ //只有一个结点的情况
ret=rear.data;
head.next=null;
rear=head;
}else if(index==0){ //要删除元素在表头
Node del=head.next;
ret=del.data;
head.next=del.next;
del.next=null;
del=null;
}else if(index==size-1){ //要删除元素在表尾
Node p=head;
while(true){
if(p.next!=rear){
p=p.next;
}else{
break;
}
}
ret=p.next.data;
p.next=null;
rear=p;
}else{ //要删除元素在中间的情况
Node p=head;
for(int i=0;i<index;i++){
p=p.next;
}
Node del=p.next;
ret=del.data;
p.next=del.next;
del.next=null;
del=null;
}
size--; //有效元素-1
return ret; //返回被删除的元素
}
@Override
public E removeFirst() {
return remove(0);
}
@Override
public E removeLast() {
return remove(size-1);
}
@Override
public void removeElement(E e) {
int index=find(e);
if(index!=-1){
remove(index);
}else{
throw new IllegalArgumentException("找不到");
}
}
清空链表和以字符串形式打印,方便测试方法的正确性
@Override
public void clear() { //清空表
head.next=null;
rear=head;
size=0;
}
//按数组的格式打印
@Override
public String toString() {
StringBuilder sb=new StringBuilder();
sb.append("LinkedList: "+size+"\n");
sb.append('[');
if(isEmpty()){
sb.append(']');
}else{
Node p=head;
while(true){
if(p.next!=rear){
p=p.next;
sb.append(p.data);
sb.append(',');
}else{
sb.append(rear.data);
sb.append(']');
break;
}
}
}
return sb.toString();
}
迭代器,写内部类,创建对象LinkedListIterator,目的是可以循环输出链表内的元素,并支持foreach循环。
//迭代器
@Override
public Iterator<E> iterator() {
return new LinkedListIterator();
}
//内部类
public class LinkedListIterator implements Iterator<E>{
private Node p=head;
@Override
public boolean hasNext() {
return p.next!=null;
}
@Override
public E next() {
p=p.next;
return p.data;
}
}
もちろん、それぞれの方法を書いて、我々は、書き込みコードに良い習慣を開発するために、タイムリーに正確なエラーのために、クラスでテストすることになっています。