链表——带头结点的单链表

 
 
package list;

/*
 * 带头结点的单链表
 */
public class SinglyList<T> {

	public Node<T> head; // 创建头结点
	public int n; // 链表长度,包括头结点

	/*
	 * 构造函数1,只有一个头结点
	 */
	public SinglyList() {
		this.head = new Node<T>();

		n = 1;
	}

	/*
	 * 构造函数2,依次将values中的值填入单链表中
	 */
	public SinglyList(T[] values) {
		this(); // 构造一个空的单链表
		n = n + values.length; // 确定链表长度
		Node<T> rear = this.head; // 为空的单链表设置头结点
		for (int i = 0; i < values.length; i++) {
			rear.next = new Node<T>(values[i], null); // 尾插入节点
			rear = rear.next; // 尾指针指向尾节点
		}
	}

	/*
	 * 判断单链表是否为空,O(1)
	 */
	public boolean IsEmpty() {

		return this.head.next == null; // 因为这是带头结点的单链表,所以判断为空的条件是头结点以后为空
	}

	/*
	 * 得到单链表第i个节点的值,并返回,时间复杂度为O(n)
	 */
	public T get(int i) {
		Node<T> cur = this.head.next; // 用cur指向当其节点
		for (int j = 0; cur != null && j < i; j++) { // 这里一定要注意,i是不能越界的(这里是指大于链表长度),很多新手都不注意这里,容易出错
			cur = cur.next;
		}
		return (i >= 0 && cur != null) ? cur.data : null; // 这里也设置了越界保险,当i(小于零,或者链表为空)都要返回空,否则返回对应元素
	}

	public void set(int i, T x) {
		if (x == null)
			throw new NullPointerException("x==null"); // 如果要插入的元素为空,则抛出空异常
		Node<T> front = this.head;

		for (int j = 0; front.next != null && j < i; j++) // 寻找到第i个节点
			front = front.next;
		front.data = x;

	}

	/*
	 * 求链表长度,复杂度为O(n),这个其实就没有什么用了,因为有了n,可以直接求出长度,但是这里还是保留一些吧
	 */
	public int size() {

		int length = 1; // 头结点算是第一个节点
		Node<T> p = head.next;
		while (p != null) {
			length++;
			p = p.next;
		}
		return length;

	}

	/*
	 * 深拷贝单链表,这里只是创建了新的链表和新的节点,但是节点的指向是一样的,因为考虑到了T不明确 没有办法让T完全拷贝下来
	 */
	public SinglyList(SinglyList<T> list) {

		this.head.data = list.head.data;
		Node<T> cur1 = list.head; // 沿着list链表遍历
		Node<T> cur2 = this.head; // 沿着当前链表开始创建节点,然后拷贝
		while (cur1.next != null) {
			cur1 = cur1.next;
			Node<T> p = new Node<T>();
			p.data = cur1.data;
			cur2.next = cur2;
		}

	}

	/*
	 * 将单链表制作成字符串形式,然后返回,这个方法,在测试的时候,非常有用,时间复杂度为O(n)
	 */
	public String toString() {
		String str = this.getClass().getName() + "("; // 返回类名
		for (Node<T> p = this.head.next; p != null; p = p.next) { // 遍历单链表
			str += p.data.toString();
			if (p.next != null)
				str += ","; // 不到最后结点,要用分隔号隔开

		}
		return str + ")"; // 返回
	}

	/*
	 * 在指定位置进行插入,将x插入到第i个元素之后(这里第一个元素是指head),插入后x的序号为i(因为从0开始的) 时间复杂度为O(n)
	 */
	public Node<T> insert(int i, T x) {
		if (x == null)
			throw new NullPointerException("x==null"); // 如果要插入的元素为空,则抛出空异常
		Node<T> front = this.head;
		for (int j = 0; front.next != null && j < i; j++) // 寻找到第i个节点
			front = front.next;
		front.next = new Node<T>(x, front.next); // 将x节点设为front之后
		// 一开始我很不习惯直接用这种方式(直接新建一个然后赋值),我一般是新建一个插入节点
		// 然后再赋值给front.next,但是后来发现上面这种方法更好,省去了起名字的麻烦,而且形式也很简单
		n++; // 长度加1
		return front.next; // 返回插入结点
	}

	/*
	 * 插入操作,默认插入到最后一位 直接调用上一个插入方法, 因为上面插入方法中,有容错机制,当你的i大于链表长度时,它就默认在末尾插入
	 * 下面这个方法,显然就利用了这一点 时间复杂度为O(n)
	 */
	public Node<T> insert(T x) {

		return insert(Integer.MAX_VALUE, x);

	}

	/*
	 * 删除第i位结点,时间复杂度为O(n)
	 */
	public T remove(int i) {
		Node<T> front = this.head;
		for (int j = 0; front.next != null && j < i; j++)
			front = front.next; // 遍历并找到第i位元素,或者是最后一位(当i大于链表长度时)
		if (i >= 0 && front.next != null) { // 确认i的合法性,如果i小于0,会跳过上面的for语句
			T old = front.next.data;
			front.next = front.next.next; // 删除操作
			n--; // 长度减1
			return old;
		}
		return null; // 如果没有执行上面的if语句,说明i小于0,或者大于链表长度,前者错误,后者不用删除
	}

	/*
	 * 根据关键字进行删除,时间复杂度为O(n)
	 */
	public T remove(T key) {
		Node<T> p = this.head;
		if (head.data == key) {
			head = head.next;
			n--;
			return key;
		}
		while (!p.next.data.equals(key) && p.next.next != null) {
			p = p.next;
		}
		if (p.next.data.equals(key)) {
			p.next = p.next.next;
			n--;
			return key;
		}
		return null;

	}

	/*
	 * 查找返回首个与key相等的元素节点,查找不成功返回null
	 */
	public Node<T> search(T key) {

		Node<T> p = this.head;
		while (!p.data.equals(key) && p.next != null) {
			p = p.next; // 当前节点数据域不等于key,并且p的下一个节点不为空
		}
		if (p.data.equals(key)) // 上面while完毕,只会有两种情况,要么找到,要么链表结束
			return p;

		return null;
	}

	/*
	 * 清空链表
	 */
	public void clear() {
		this.head.next = null;
		n = 1;
	}

	public static void main(String[] args) {
		// TODO Auto-generated method stub

		String[] s = { "aa", "bb", "cc" };
		SinglyList<String> test = new SinglyList<String>(s);
		test.remove("aa");
		System.out.println(test.toString());

	}

}

还有节点类:

package list;

public class Node<T> {

	public T data; // 数据域
	public Node<T> next; // 地址域,指向下一个节点地址

	public Node(T data, Node<T> next) { // 构造函数,提供数据域和地址域
		this.data = data;
		this.next = next;
	}

	public Node() { // 构造函数,建造空节点
		this(null, null); // 调用上面的构造函数,也算是调用自身
	}

	public String toString() {
		return this.data.toString(); // 返回数据域的字符串形式
	}

}


1、对于上面的第i个元素之后插入的方法,测试如下:

		String[] s = { "aa", "bb", "cc" };
		SinglyList<String> test = new SinglyList<String>(s);
		test.insert(2, "ee");
		System.out.println(test.toString());

显示结果如下:


ee插入到了第2个元素之后,但是如果是按序号来算的话,那么它就是2号(因为第一位是0号)

2、测试上面的移除操作

		String[] s = { "aa", "bb", "cc" };
		SinglyList<String> test = new SinglyList<String>(s);
		test.remove(1);
		System.out.println(test.toString());

结果显示:


删除了“bb”,这里的i是序号,不是个数(如果算上头结点的话)

3、单链表方法的时间复杂度说明

(1)链表中的删除操作比顺序表要方便很多,直接更改节点的next域,就可以改变节点间的链接关系,无需移动元素,Java的资源回收机制将自动释放不再使用的对象,但是它的时间复杂度同样为O(n),因为它不是随机存取结构,要依次遍历到要移除的元素位置上,这相比顺序表消耗了很多时间。

扫描二维码关注公众号,回复: 1463118 查看本文章

(2)在单链表的操作中,除了判断是否为空的方法外,其他方法的时间复杂度均为O(n),因为它们均涉及到了遍历元素问题,更改花费时间少,但是遍历元素花费时间很多

4、提高单链表操作效率的措施

(1)插入操作对于序号的容错,提高操作效率,如果insert(T)方法采用下面的方法

public Node<T> insert(T x){         //在单链表最后插入数据
	return insert(this.size(),x);  //需要遍历单链表两次,效率很低
}

上面是在单链表末尾插入元素,其中insert(i,x)方法,是在第i个位置插入元素x,如果你要在末尾插入元素,那么你要获得链表的长度(也就是this.size()方法,这个方法需要遍历整个链表获得),所以你要遍历两次(第一次获得长度,第二次插入元素),所以效率很低,而采用以下方法,就只需要遍历一次:

return insert(Integer.MAX_VALUE,x);   //Integer.MAX_VALUE表示整数最大值

在insert(i,x)方法中有一个容错机制,就是如果你输入的i大于链表长度(当遍历链表寻找i的时候,发现到了最后,仍然没有找到i),那么就默认为i为链表长度,在末尾插入!利用整数的最大值,无论你的链表有多长,都会默认在链表末尾插入元素,这样就只需要遍历一次就可以了!

(2)单链表不能调用get方法遍历,因为会消耗更多的时间,可以看下面这个例子:

/*
 * 返回数值链表的平均值
 */
    public static double average(SinglyList<Integer> list) {
    	int sum = 0;
    	for(int i =0;i<list.size();i++)      //size方法时间是O(n)
    		sum + = list.get(i).intValue();  //每次调用get方法,都要进行遍历,然后花费O(n)时间
    	return (double)sum/list.size();    //实数除,存在除数为0的错误
    }

这里提到了,当你遍历时,要遍历n个元素,但是调用一次get方法就要消耗O(n)时间,加起来就是n的平方次

(3)增加属性

如果在单链表中增加成员变量,记住某些属性,则可提高某些操作效率(有些则会下降),比如增加成员变量n表示单链表的长度,当插入一个元素时,n++;当删除一个元素时,n--,就可以使得size()方法的时间复杂度为O(1)。

同理,如果增加成员变量rear作为单链表的尾指针,指向单链表的最后一个节点,则单链表尾插入操作的时间复杂度是O(1)。

任何东西有得就有失,以上三种方法虽然可以提高效率,但是也增加了程序维护的困难程度,特别是在子类中。

当然,我还是比较倾向于有这些举措的。


参考书籍:《数据结构(java版)》叶核亚,有不懂的,可以再看一下这本书

猜你喜欢

转载自blog.csdn.net/yuangan1529/article/details/80190127