单链表相关操作

实现单链表的反转

1.最简单暴力的方法,遍历链表,并创建节点采用头插法即可,时间复杂度为O( n),空间需要创建n个节点。

2.采用指针转向方法
我们知道单链表只能单向顺序遍历,不能往回倒着走。那么,如何实现单链表的反转呢,我们可以借助引用(指针)将链表分成相互隔离的两个部分,前面部分是已经逆好序的链表,而后一部分是未遍历的部分,由于指针的单向性,我们必须确保两个部分都有指针指向,否则就会导致链表丢失。
用cur指针指向正在遍历的节点(第二部分),而preNode始终指向第一部分的头结点,因为这样可以让cur这个节点和preNode节点连接,
而nextNode始终指向第二部分的第一个节点,这样确保第二部分不丢失,并且能让cur指过去。
是不是有点像直接插入排序,也是分为两个部分,前面部分是已经排序好的,而后面一部分是待遍历的
代码如下

public void RecverseIteratively(Node head) {
		Node cur = head;
		Node nextNode = null;
		Node preNode = null;
		while(cur!=null) {
			nextNode = cur.next;
			if(nextNode == null) {//表明cur指向的节点是最后一个需要逆序的节点,也就是逆序后链表的头结点,因此需要用头结点指向
				head = cur;
			}
			cur.next = preNode;
			preNode = cur;
			cur = nextNode;
		}
	}`

链表中快慢指针的引用

由于单链表的性质,单向顺序遍历,而我们用两个指针(一个跑的快,一个跑得慢,参考时钟表的分针和秒针)能方便的处理一些问题
1.检测链表中是否有环
这是经典的用快慢指针解决的问题,慢指针走一步,快指针走两步,如果有环,必定会在慢指针遍历完链表之前与其相遇(参考时钟表的分秒针的重合)如果没环,快指针必定会先到尾结点,因此循环条件只用快指针就行了
代码如下:

public boolean isLoop(Node head) {
		Node fastNode = head;
		Node slowNode = head;
		while(fastNode!=null&&fastNode.next!=null) {
			//fastNode.next!=null的目的是避免空指针异常
			fastNode = fastNode.next.next;
			slowNode = slowNode.next;
			if(fastNode == slowNode) {//如果有环,那么快慢指针肯定会相遇,因为速度不一样,就像钟表一样,秒针总会和分针相遇
				return true;
			}
		}
			return false;
	}

2.寻找单链表的中间节点
最简单的方法就是遍历一边链表,然后找到其链表的长度,第二次循环到一半的位置就行了,但是这需要遍历两次链表,开销有点大。
那我们寻找遍历一边就找到中间节点的方法,没错,就是快慢指针。
同样的是快指针走两步,慢指针走一步,我们发现若是链表长度为奇数时,快指针到尾部,慢指针正好在中间。而链表长度为偶数时,快指针到倒数第二个节点时,慢指针指向的和接下来一个节点都是中间节点(偶数的中间值有两个);
代码如下:

public Node SearchMid(Node head) {
		Node slowNode = head;
		Node fastNode = head;
		while(fastNode!=null&&fastNode.next!=null&&fastNode.next.next!=null) {//此判断条件有三个而且都是相互短路,所以不存在空指针异常
			//fastNode!=null用来判断是否是空链表,如果是那么就返回空
			//fastNode.next!=null用来判断偶数节点时,fastNode是否在尾节点
			//fastNode.next.next!=null用来判断奇数节点时,fastNode是否在倒数第二个节点
			//以上都是循环终止条件,此时slowNode已经在中间节点的位置了
			fastNode = fastNode.next.next;
			slowNode = slowNode.next;
		}
		return slowNode;
	}

3.找出单链表中的倒数第k个元素
1.同样,简单暴力的方法是遍历一边链表找到其长度n,然后循环到n-k即可,这也需要遍历两遍
2.优化一下,我们这样,每次在从当前的节点开始遍历k个节点,看看是否到了尾结点,如果到了尾结点,那么当前节点就是倒数第k个节点,代码如下:

public Node findElem(int k) {
		if(k<1) return null;
		Node cur = head;
		Node nextNode = null;
		while(cur!=null) {
			nextNode = cur;
			for(int i = 1; i<=k; i++) {
				nextNode = nextNode.next;
			}
			if(nextNode==null) {
				break;
			}
			cur = cur.next;
			
		}
		return cur;
	}

3.继续优化一下,我们发现方法二有个缺点也就是有内循环那么时间复杂度就是O(nk)基本上是n²级别,所以我们又采用快慢指针的方式
当然这里的快慢指针只是表明两个指针的相对位置,而不表示指针的移动速度,我们这样处理,快指针先移动到第k个位置,然后快慢指针一起移动。如果快指针到了尾结点,那么慢指针指向的就是倒数第k个节点了,代码如下:

public Node findElem1(int k) {
		if(k<1) return null;
		Node cur = head;
		Node nextNode = head;
		for(int i = 1; i<k&&nextNode!=null; i++) {//同样这里的nextNode!=null也是为了避免空指针异常,因为我们不知道链表的长度,有可能k大于链表的长度了
			nextNode = nextNode.next;
		}
		if(nextNode == null){
			throw new Exception("k不合法");
		}
		while(nextNode.next!=null) {//接下来就是两个指针一起移动了
			cur = cur.next;
			nextNode = nextNode.next;
		}
		return cur;
	}

从尾到头的输出单链表

显然,单链表只能顺序遍历,如何反向输出了,答案呼之欲出,没错,就是使用栈。既然用到栈,那我们就可以用更加简便的方法——递归来解决这个问题
递归终止条件:递归到空节点了
递去:参数条件就是cur.next;
归来:归来我们就直接输出该节点就行了
代码如下:
public void printListReversely(Node head) {
		if(head!=null) {//递归临界条件
			printListReversely(head.next);//递去
			System.out.println(head.val);//归来
		}
	}

当然我们也可以使用栈来解决,没遍历一个节点,将它的值入栈,遍历结束之后,依次出栈就行了。
代码如下:

public void printListReversely(Node head) {
		Stack<Integer> stack = new Stack<>();
		Node cur = head;
		while(cur!=null) {
			stack.push(cur.val);
			cur = cur.next;
		}
		while(!stack.empty()) {
			System.out.println(stack.pop());
		}
	}

删除指定节点,头结点未知的情况下

如果头结点知道的话,很简单我们遍历到指定节点位置的前面,然后删除即可。然而头结点未知,那我们必然找不到该节点的前驱。所以不能用一般的方法删除,我们发现如果我们将两个节点的值调换一下,那么是否可以简单的认为两个节点调换了呢。对的,我们就是用这个思路。既然前驱找不到,那么我们自己创建前驱即可。如何创建呢?我们将指定节点的值和下一个节点进行交换,那么我们可以认为要删除的节点“跑到”后面去了,我们所指向的节点即是要删除的节点的前驱。当然从以上可以看出,如果要删除的尾结点,我们做不到。因为我们没有办法找一个前驱节点。
代码如下:
public boolean deleteNode(Node cur) {
		if(cur==null||cur.next==null) {
			return false;
			//cur.next==null表示如果是尾节点,则无法删除,因为前驱节点不知道。
		}
		//如果是非尾结点,那么可以用交换值的方法,也就是将要删除节点的值与下一个节点值交换,这样删除下一个节点就是相当于删除该节点。
		cur.val = cur.next.val;//这里我没有交换,只要把后面节点的值保存下来即可,至于要删除节点的值就不用管了。
		cur.next = cur.next.next;
		return true;
	}

判断两条链表是否相交

我们知道单链表只有一个指针域,也就意味着如果两条链表相交,那么从交点开始之后的所有节点都公用,那么我们可以遍历到尾节点,如果两条链表相交,那么必然指向的尾结点是同一个。
代码如下:
public boolean isIntersect(Node h1,Node h2) {
		if(h1==null||h2==null) {
			return false;  //任何一条链表为空,那么必然不想交
		}
		Node p = h1;
		Node q = h2;
		while(p.next!=null) {
			p = p.next;
		}
		while(q.next!=null) {
			q = q.next;
		}
		return p==q;

从链表中删除重复元素

1.一个简单方法,我们可以利用set集合的性质,也就是不重复,每add一个节点,看看是否加入成功,成功说明没重复,不成功说明重复,删除即可
代码如下:

public void deleteDuplecate(Node head) {
		Set<Node> set = new HashSet<Node>();
		Node preNode = null;
		Node cur = head;
		while(cur!=null) {
			if(set.add(cur)) {
				preNode = cur;
			}else {
				preNode.next = cur.next;
			}
			cur = cur.next;
		}
	}

2.方法一虽然简便,但是利用set集合有额外的空间开销,我们也可以通过时间换空间的方法来删除重复的元素,怎么做呢,双重循环,外循环我们每遍历一个节点时,内循环从当前节点的下一个节点遍历到尾节点,并且删除重复元素,代码如下:

public void deleteDuplecate1(Node head) {
		Node cur = head;
		Node preNode = null;//指向要删除节点的前驱
		Node nextNode = null;
		while(cur!=null) {
			preNode = cur;
			nextNode = cur.next;
			while(nextNode!=null) {
				if(cur.val==nextNode.val) {
					preNode.next = nextNode.next;
					
				}else {
					preNode = preNode.next;
				}
				nextNode = nextNode.next;
			}
			cur = cur.next;
		}
	}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值