Java数据结构和算法学习(五)——链表
我们知道数组作为数据结构有一定的缺陷。在无序数组中,搜索性能差,而有序数组,插入效率又很低,而且这两种数组的删除效率都很低,并且数组在创建后,其大小就固定了,设置过大浪费内存,过小不能满足数据量的存储。
链表也是一种使用广泛的通用的数据结构,它也可以用来作为实现栈,队列等数据结构的基础,基本上除非需要频繁的通过下标来随机访问各个数据,否则很多使用数组的地方都可以用来链表来代替。
一、链表
链表(Linked list):一种常见的基础数据结构,是一种线性表,但是并不会按线性的顺序存储结构,而是在每一个节点里存到下一个节点的指针(Pointer)。
使用链表结构可以克服数组链表需要预先知道数据大小的缺点,链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。但是链表失去了数组随机读取的优点,同时链表由于增加了节点的指针域,空间开销比较大。
二、单向链表
单向链表:单链表是结构最简单的链表,一个单链表的节点(node)分为两部分,第一部分(data)保存或者显示关于节点的信息,另一部分存储下一节点的地址。最后一个节点存储地址的部分指向空值。
单向链表只可向一个方向遍历,一般查找一个节点的时候需要从第一个节点开始每次访问下一个节点,一直访问到需要的位置。而插入一个节点,对于单向链表,只提供在链表头插入,只需要将当前插入的节点设置为头节点,next指向原头节点即可。删除一个节点,将该节点的上一个节点的next指向该节点的下一个节点。
1、单向链表的具体代码实现:
public class SingleLinkedList {
private int size;//链表节点个数
private Node head;//头节点
public SingleLinkedList()
{
size=0;
}
//链表的每个节点类
public class Node{
private Object data;//每个节点的数据
private Node next;//每个节点指向下一节点的连接
public Node(Object data)
{
this.data=data;
}
}
//在链表头添加元素
public Object addHead(Object obj)
{
Node newHead = new Node(obj);
if(size==0)
{
head=newHead;
}else{
newHead.next=head;
head=newHead;
}
size++;
return obj;
}
//在链表头删除元素
public Object deleteHead()
{
Object object=head.data;
head=head.next;
size--;
return object;
}
//查找指定元素,找到返回节点Node,找不到返回null
public Node find(Object obj){
Node cur=head;
int tempSize=size;
while (tempSize>0)
{
if(obj.equals(cur.data))
{
return cur;
}else{
cur=cur.next;
}
tempSize--;
}
return cur;
}
//删除指定元素
public boolean delete(Object value)
{
if(size==0){
return false;
}
Node cur=head;
Node pre=head;
while (cur.data!=value){
if(cur.next==null){
return false;
}
}
//如果是删除第一个节点
if(cur==head){
head=cur.next;
size--;
}else {
pre.next=cur.next;
size--;
}
return true;
}
//判断链表是否为空
public boolean isEmpty()
{
return (size==0);
}
//显示节点信息
public void display() {
if (size > 0) {
Node node = head;
int tempSize = size;
//如果当前链表只有一个节点
if (tempSize == 1) {
System.out.println("[" + node.data + "]");
return;
}
while (tempSize > 0) {
if (node.equals(head)) {
System.out.print("[" + node.data + "->");
} else if (node.next == null) {
System.out.print(node.data + "]");
} else {
System.out.print(node.data + "->");
}
node = node.next;
tempSize--;
}
System.out.println();
} else {
System.out.println("[]");
}
}
测试代码:
@Test
public void testSingleLinkedList(){
SingleLinkedList singleLinkedList=new SingleLinkedList();
singleLinkedList.addHead("A");
singleLinkedList.addHead("B");
singleLinkedList.addHead("C");
singleLinkedList.addHead("D");
singleLinkedList.addHead("E");
//打印当前链表信息
singleLinkedList.display();
//删除C
singleLinkedList.delete("C");
singleLinkedList.display();
//查找B
System.out.println(find("B"));
}
三、双端链表
对于单链表,如果想在尾部添加一个节点,那么必须从头部一直遍历到尾部,找到尾节点,然后在尾节点后面插入一个节点,这样操作很麻烦,但如果在设计链表的时候多个对尾节点的引用,那么会简单很多。
即:链表中保存着对最后一个链接点引用的链表。
注意:双端链表并不是双向链表,其特点是第一个链接点与最后一个链接点直接相连。
代码实现:
public class DoublePointLinkedList {
private Node head;//头节点
private Node tail;//尾节点
private int size;//节点的个数
private class Node{
private Object data;
private Node next;
public Node(Object data){
this.data=data;
}
}
public DoublePointLinkedList(){
size=0;
head=null;
tail=null;
}
//链表头新增节点
public void addHead(Object data){
Node node=new Node(data);
if(size==0){
head=node;
tail=node;
size++;
}else {
node.next=head;
head=node;
size++;
}
}
//链表尾新增节点
public void addTail(Object data){
Node node=new Node(data);
if(size==0){
head=node;
tail=node;
size++;
}else {
tail.next=node;
tail=node;
size++;
}
}
//删除头部节点,成功true,失败false
public boolean deleteHead(){
if(size==0){
return false;
}
//当前链表节点数为1
if(head.next==null){
head=null;
tail=null;
}else {
head=head.next;
}
size--;
return true;
}
//判断是否为空
public boolean isEmpty(){
return (size==0);
}
//获得链表的节点个数
public int getSize(){
return size;
}
//显示节点信息
public void display(){
if(size>0){
Node node=head;
int tempSize=size;
if(tempSize==1){
System.out.println(node.data);
return;
}
while (tempSize>0){
if(node.equals(head)){
System.out.println(node.data);
}else if(node.next==null){
System.out.println(node.data);
}else {
System.out.println(node.data);
}
node=node.next;
tempSize--;
}
System.out.println();
}else {
System.out.println("[]");
}
}
}
双端链表实现队列:
public class QueueLinkedList {
private DoublePointLinkedList dp;
public QueueLinkedList(){
dp=new DoublePointLinkedList();
}
public void insert(Object data){
dp.addTail(data);
}
public void delete(){
dp.deleteHead();
}
public boolean isEmpty(){
return dp.isEmpty();
}
public int getSize(){
return dp.getSize();
}
public void display(){
dp.display();
}
}
四、有序链表
前面的链表实现插入数据都是无序的,而在有序链表中,数据是按照关键值有序排列的。一般在大多数需要使用有序数组的场合也可以使用有序链表。有序链表优于有序数组的地方是插入的速度(因为元素不需要移动),另外链表可以扩展到全部有效的使用内存,而数组只能局限于一个固定的大小中。
在有序链表中插入和删除某一项最多需要O(N)次比较,平均需要O(N/2)次,因为必须沿着链表上一步一步走才能找到正确的插入位置,然而可以最快速度删除最值,因为只需要删除表头即可,如果一个应用需要频繁的存取最小值,且不需要快速的插入,那么有序链表是一个比较好的选择方案。比如优先级队列可以使用有序链表来实现。
代码实现:
public class OrderLinkedList {
private Node head;
public class Node{
private int data;
private Node next;
public Node(int data){
this.data=data;
}
}
public OrderLinkedList(){
head=null;
}
//插入节点,并按照从小到大的顺序排列
public void insert(int value){
Node node=new Node(value);
Node pre=null;
Node cur=head;
while (cur!=null&&value>cur.data){
pre=cur;
cur=cur.next;
}
if(pre==null){
head=node;
head.next=cur;
}else {
pre.next=node;
node.next=cur;
}
}
//删除头节点
public void deleteHead(){
head=head.next;
}
public void display(){
Node cur=head;
while (cur!=null){
System.out.println(cur.data);
cur=cur.next;
}
System.out.println("");
}
}
五、有序链表和无序数组组合排序
对于一个无序数组排序,使用冒泡排序、选择排序、插入排序时,需要的时间级别都是O(N²)。
现在,对于一个无序数组,先将数组元素取出,一个一个的插入有序链表中,然后将它们从有序链表中逐一删除,重新放入数组,那么数组就排好序了。和插入排序一样,如果插入了N新个数据,那么进行大概N²/4次比较。但是相当于插入排序,每个元素只进行了两次排序,一次从数组到链表,一次从链表到数组,大概需要2*N次移动,而插入排序需要N²次移动。
效率肯定是比简单排序要高,但是缺点就是需要开辟差不多两倍的空间,而且数组和链表必须在内存中同时存在。
六、双向链表
双向链表也是链表的一种,它每个数据结点中都有两个结点,分别指向其直接前驱和直接后继。所以我们从双向链表的任意一个结点开始都可以很方便的访问其前驱元素和后继元素。
双向链表也是采用的链式存储结构,它与单链表的区别就是每个数据结点中多了一个指向前驱元素的指针域 。
代码实现:
public class TwoWayLinkedList {
private Node head;//表示链表头
private Node tail;//表示链表尾
private int size;//表示链表的节点个数
public class Node{
private Object data;
private Node next;
private Node prev;
public Node(Object data){
this.data=data;
}
}
public TwoWayLinkedList(){
size=0;
head=null;
tail=null;
}
//在链表头增加节点
public void addHead(Object value){
Node newNode=new Node(value);
if(size==0){
head=newNode;
tail=newNode;
size++;
}else {
head.prev=newNode;
newNode.next=head;
head=newNode;
size++;
}
}
//在链表尾增加节点
public void addTail(Object value){
Node newNode=new Node(value);
if(size==0){
head=newNode;
tail=newNode;
size++;
}else {
newNode.prev=tail;
tail.next=newNode;
tail=newNode;
size++;
}
}
//删除链表头
public Node deleteHead(){
Node temp=head;
if(size==0){
head=head.next;
head.prev=null;
size--;
}
return temp;
}
//删除链表尾
public Node deleteTail(){
Node temp=tail;
if(size!=0){
tail=tail.prev;
tail.next=null;
size--;
}
return temp;
}
//获得链表的节点个数
public int getSize(){
return size;
}
//判断链表是否为空
public boolean idEmpty(){
return (size==0);
}
}
七、总结
没一个链表都包括一个LinkedList对象和许多Node对象,LinkedList对象通常包含头和尾节点的引用,分别指向链表的第一个和最后一个节点。而每个节点对象通常包含数据部分data,以及对上一个节点的引用prev和下一个节点的引用next,只有下一个节点的引用称为单向链表,两个都有的称双向链表。next值为null则说明是链表的结尾,如果想找到某一个节点,必须从第一个节点开始遍历,不断通过next找到下一个节点,直到找到。栈和队列都是ADT,用数组和链表都能实现。