线性表之单链表--SingleLinkedList

下面,笔者和大家来一起简单回忆一下单链表,链表用一组任意的存储单元存放元素,存储单元可以连续也可不连续。

在这里笔者定义了一个节点类: Node   

nodeValuenext
       nodeValue :存放数据,next :存放下一节点的引用。  单链表通过引用将线性表的数据元素按照逻辑关系链接在一起。 下面是笔者定义的一个节点类及SingleLinkedList同样都使用泛型。

/**
 * 单链表时Node 存储;两个变量: nodeValue next 这里没有设置Node 的属性为 private
 * 主要是为了其他类访问方便,若要进行保护,可以提供相关的get set方法进行访问
 * 
 */
public class Node<T> {
	T nodeValue; // 数据域
	Node<T> next; // 指针域保存着下一节点的引用

	public Node(T nodeValue, Node<T> next) {
		this.nodeValue = nodeValue;
		this.next = next;
	}

	public Node(T nodeValue) {
		this(nodeValue, null);
	}
}
接下来笔者定义了一个 单链表 SingleLinkedList 这是一个带头结点的单链表。每个单链表有一个头指针 head 
	// 下面是SingleLinkedList类的数据成员和方法 head 指向表头
	private Node<T> head;

	public SingleLinkedList() {
		head = null;
	}

插入: 由于单链表只有一个头指针,所以插入分一下3中情况讨论。

1、表头插入时,由于有表头指针,可直接进行插入,效率非常高时间复杂度为常数

2、表尾插入时,由于只有一个表头指针没有表尾指针,在表尾插入时需要遍历整个单链表知道找到next引用为空时才进行插入。时间复杂度为O(n).

3、指定节点插入时, 同样需要遍历链表找到插入位置,时间复杂度同样为O(n)。

	/**
	 * 在表头插入结点,效率非常高,由于有对表头的引用
	 */
	public void insertFirst(T t) {
		Node<T> newNode = new Node<T>(t);

		if (isEmpty()) {
			head = newNode; // 链表为空为空,将head指向 newNode
		} else {
			newNode.next = head;
			head = newNode;
		}

	}
	
	/**
	 * 这个是没有尾指针时,在链表尾部插入,可以看出: 在表尾插入结点,效率很低,需要遍历整个链表才能插入。
	 */
	public void insertLast(T t) {
		Node<T> newNode = new Node<T>(t);
		Node<T> p = head;
		while (p.next != null) {
			p = p.next;
		}
		p.next = newNode;
		newNode.next = null;
	}

	/**
	 * 在指定位置前插入一个新结点 插入操作可能有四种情况: 
	 * ①表为空, 返回false 
	 * ②表非空,指定的数据不存在
	 * ③指定的数据是表的第一个元素
	 * ④指定的数据在表的中间
	 * 
	 * @param known  指定的已知节点
	 * @param t   要插入的结点
	 */
	public boolean insertBefore(T known, T t) {
		Node<T> prev = head, curr = head.next, newNode;
		newNode = new Node<T>(t);
		if (!isEmpty()) { // 非空
			while ((curr != null) && (!known.equals(curr.nodeValue))) { // 两个判断条件不能换
				prev = curr;
				curr = curr.next;
			}
			newNode.next = curr;
			prev.next = newNode;
			return true;
		}
		return false; // 表空
	}
  获取元素,

1、直接获取表头元素,时间复杂度为常数,获取成功返回元素,失败则表示链表为空,抛出异常。

2、已知元素,获取元素所在的位置,需要遍历链表时间复杂度为O(n),若获取成功,返回元素位置,失败返回-1。

<span style="white-space:pre">	</span>/**
	 * 取得链表的表头存储的数据
	 */
	public Node<T> getFirst() {
		if (isEmpty()) {
			throw new RuntimeException("链表为空!");
		}
		return head;
	}

	/**
	 * 返回此列表中首次出现的指定元素的索引,如果列表中不包含此元素,则返回 -1.
	 */
	public int indexOf(T t) {
		int index = 1; // 从1开始计算
		Node<T> p;
		for (p = head; p != null; p = p.next) {
			if (t.equals(p.nodeValue))
				return index;
			index++;
		}
		return -1;
	}
删除: 同插入一样,也分为3中情况

1、 在表头删除,直接将表头指针指向下一个节点,效率非常高,时间复杂度为O(n)。

2、 在表尾删除,效率很低 ,需要遍历整个链表找到最后节点,然后删除节点,将倒数第二个节点的next引用赋值为空。由于需要遍历,所以时间复杂度为O(n)。

3、 删除指定节点, 效率同样非常低,需要遍历链表找到第一次出现的指定节点,然后进行删除,所以时间复杂度为O(n)。

	/**
	 * 在表头删除结点,效率非常高
	 */
	public Node<T> removeFirst() {
		if (isEmpty()) {
			throw new RuntimeException("链表为空,不能删除表头!");
		}
		Node<T> temp = head;
		head = head.next;
		return temp;
	}

	/**
	 * 在表尾删除结点,效率很低 ,需要遍历整个链表找到最后节点。
	 * 此时双端链表和但链表一样不利于删除最后一个链节点,因为没有一个引用指向倒数第二个链节点
	 * 如果最后一个节点被删除,倒数第二个节点的next 字段变成空。
	 */
	public Node<T> removeLast() {
		Node<T> previous = null, current = head;
		while (current.next != null) {
			previous = current;
			current = current.next;
			if (current.next == null) {
				previous.next = null;
			}
		}
		return current;
	}

	/**
	 * 移除此列表中首次出现的指定元素 删除操作可能出现的情况: 
	 * ①previous为空,这意味着current为head. head = curr.next; --> removeFirst();
	 * ②匹配出现在列表中的某个中间位置,此时执行的操作是 --> prev.next = curr.next;, 
	 *  在列表中定位某个结点需要两个引用:一个对前一结点(previous左)的引用以及一个对当前结点(current右)的引用.
	 * previous = current; current = curr.next;
	 */
	public Node<T> remove(T t) {
		Node<T> current = head, previous = null;
		boolean found = false;
		while (current != null && !found) {
			if (t.equals(current.nodeValue)) {
				if (previous == null)
					removeFirst();
				else
					previous.next = current.next;
				found = true;
			} else {
				previous = current;
				current = current.next;
			}
		}
		return current;
	}
其他方法: 判空、获取链表大小... 
	/**
	 * 判断链表是否为空
	 */
	public boolean isEmpty() {
		return head == null;
	}
	
	/**
	 * 取得链表的大小
	 */
	public int getSize() {
		int count = 0;
		Node<T> p;
		for (p = head; p != null; p = p.next) {
			count++;
		}
		return count;
	}

	/**
	 * 如果此列表包含指定元素,则返回 true。
	 */
	public boolean contains(T t) {
		return indexOf(t) != -1;
	}

	/**
	 * 打印列表
	 */
	public void printList() {
		if (isEmpty()) {
			throw new RuntimeException("链表为空!");
		} else {
			for (Node<T> p = head; p != null; p = p.next)
				System.out.print(p.nodeValue + "  ");
		}
	}

从上一节实现的 顺序表以及这节的 单链表可以看出它俩之间的区别以及优缺点:

顺序表:1、 插入和删除操作需要移动大量元素,等概率情况下顺序表不利于插入特别是在表头插入和删除时。2、顺序表中存储元素的数组容量难以确定,虽然在 MyArrayList中在数组满的时候扩展了数组,但是还是难以确定顺序表大小,可能不够更可能引起浪费,从而引起了存储空间的浪费。 3、虽然顺序表不利于插入删除,但是顺序表中查找是十分高效的,已知元素位置,可以直接返回元素。

链表: 1、链表中查找必须遍历真个链表,效率不高。   2、链表中删除虽然同样需要查找删除位置,但是不需要移动大量元素,在给出指向链表中的某个合适位置的指针后,插入和删除所需时间为O(1)。   3、链表不必要求存储空间的连续性。

关于双链表,笔者将在下一节进行介绍。

全部代码以及测试

package org.TT.LinkedList;

public class SingleLinkedList<T> {

	// 下面是SingleLinkedList类的数据成员和方法 head 指向表头
	private Node<T> head;

	public SingleLinkedList() {
		head = null;
	}

	/**
	 * 在表头插入结点,效率非常高,由于有对表头的引用
	 */
	public void insertFirst(T t) {
		Node<T> newNode = new Node<T>(t);

		if (isEmpty()) {
			head = newNode; // 链表为空为空,将head指向 newNode
		} else {
			newNode.next = head;
			head = newNode;
		}

	}
	
	/**
	 * 这个是没有尾指针时,在链表尾部插入,可以看出: 在表尾插入结点,效率很低,需要遍历整个链表才能插入。
	 */
	public void insertLast(T t) {
		Node<T> newNode = new Node<T>(t);
		Node<T> p = head;
		while (p.next != null) {
			p = p.next;
		}
		p.next = newNode;
		newNode.next = null;
	}


	/**
	 * 在指定位置前插入一个新结点 插入操作可能有四种情况: 
	 * ①表为空, 返回false 
	 * ②表非空,指定的数据不存在
	 * ③指定的数据是表的第一个元素
	 * ④指定的数据在表的中间
	 * 
	 * @param known  指定的已知节点
	 * @param t   要插入的结点
	 */
	public boolean insertBefore(T known, T t) {
		Node<T> prev = head, curr = head.next, newNode;
		newNode = new Node<T>(t);
		if (!isEmpty()) { // 非空
			while ((curr != null) && (!known.equals(curr.nodeValue))) { // 两个判断条件不能换
				prev = curr;
				curr = curr.next;
			}
			newNode.next = curr;
			prev.next = newNode;
			return true;
		}
		return false; // 表空
	}

	/**
	 * 取得链表的表头存储的数据
	 */
	public Node<T> getFirst() {
		if (isEmpty()) {
			throw new RuntimeException("链表为空!");
		}
		return head;
	}

	/**
	 * 返回此列表中首次出现的指定元素的索引,如果列表中不包含此元素,则返回 -1.
	 */
	public int indexOf(T t) {
		int index = 1; // 从1开始计算
		Node<T> p;
		for (p = head; p != null; p = p.next) {
			if (t.equals(p.nodeValue))
				return index;
			index++;
		}
		return -1;
	}
	
	/**
	 * 在表头删除结点,效率非常高
	 */
	public Node<T> removeFirst() {
		if (isEmpty()) {
			throw new RuntimeException("链表为空,不能删除表头!");
		}
		Node<T> temp = head;
		head = head.next;
		return temp;
	}

	/**
	 * 在表尾删除结点,效率很低 ,需要遍历整个链表找到最后节点。
	 * 此时双端链表和但链表一样不利于删除最后一个链节点,因为没有一个引用指向倒数第二个链节点
	 * 如果最后一个节点被删除,倒数第二个节点的next 字段变成空。
	 */
	public Node<T> removeLast() {
		Node<T> previous = null, current = head;
		while (current.next != null) {
			previous = current;
			current = current.next;
			if (current.next == null) {
				previous.next = null;
			}
		}
		return current;
	}

	/**
	 * 移除此列表中首次出现的指定元素 删除操作可能出现的情况: 
	 * ①previous为空,这意味着current为head. head = curr.next; --> removeFirst();
	 * ②匹配出现在列表中的某个中间位置,此时执行的操作是 --> prev.next = curr.next;, 
	 *  在列表中定位某个结点需要两个引用:一个对前一结点(previous左)的引用以及一个对当前结点(current右)的引用.
	 * previous = current; current = curr.next;
	 */
	public Node<T> remove(T t) {
		Node<T> current = head, previous = null;
		boolean found = false;
		while (current != null && !found) {
			if (t.equals(current.nodeValue)) {
				if (previous == null)
					removeFirst();
				else
					previous.next = current.next;
				found = true;
			} else {
				previous = current;
				current = current.next;
			}
		}
		return current;
	}

	/**
	 * 判断链表是否为空
	 */
	public boolean isEmpty() {
		return head == null;
	}
	
	/**
	 * 取得链表的大小
	 */
	public int getSize() {
		int count = 0;
		Node<T> p;
		for (p = head; p != null; p = p.next) {
			count++;
		}
		return count;
	}

	/**
	 * 如果此列表包含指定元素,则返回 true。
	 */
	public boolean contains(T t) {
		return indexOf(t) != -1;
	}

	/**
	 * 打印列表
	 */
	public void printList() {
		if (isEmpty()) {
			throw new RuntimeException("链表为空!");
		} else {
			for (Node<T> p = head; p != null; p = p.next)
				System.out.print(p.nodeValue + "  ");
		}
	}

	public static void main(String[] args) {
		SingleLinkedList<String> t = new SingleLinkedList<String>();

		t.insertFirst("B");
		t.insertFirst("A");
		t.insertLast("C");
		t.insertLast("D");
		t.insertLast("E");
		t.insertLast("F");
		
		t.insertBefore("B", "TT");  // 在B前面插入
		t.printList();
		
		System.out.println();
		System.out.println("‘C’ 在链表的位置:" + t.indexOf("C")); // 寻找 C的位置并且返回位置为 4
															//若返回 -1 则表示没有找到
		System.out.println("是否含有‘A’?======" + t.contains("A")); // 含有A 返回 true
		
		System.out.println("删除 最后一个  、 第一个 、 和 ‘D’");
		t.removeLast();
		t.removeFirst();
		t.remove("D");
		 
		System.out.print("删除后: ");
		t.printList(); 

	}

}


节点类

package org.TT.LinkedList;

/**
 * 单链表时Node 存储;两个变量: nodeValue    next 
 * 这里没有设置Node 的属性为 private 主要是为了其他类访问方便,若要进行保护,可以提供相关的get set方法进行访问
 *  
 */
public class Node<T> {     
    T nodeValue; // 数据域     
    Node<T> next; // 指针域保存着下一节点的引用     
         
   public Node(T nodeValue, Node<T> next) {     
        this.nodeValue = nodeValue;     
        this.next = next;     
    }     
   public Node(T nodeValue) {     
        this(nodeValue, null);     
    }
    
}<strong> 
</strong>


测试结果



  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值