链表数据重复去重问题,牛群问题
链表去重排序
农场里有一群牛,每头牛都有一个独特的编号,编号由一个整数表示,整数范围是[0, 200]。牛群中的牛用单链表表示,链表已经按照非降序排列。 因为一些事故,导致一头牛可能多次出现在链表中。给你一个链表的头 head,删除链表中所有重复的编号,只留下所有牛的不重复编号。返回已排序的链表。 示例1 输入: {1,2,2,3,3,4,5,5} 复制 返回值: {1,2,3,4,5}
这里我们自定义了一个链表对象。
static public class ListNode {
int val;
ListNode next = null;
public ListNode(int val) {
this.val = val;
}
}
}
添加数据
int [] arrays={1,2,2,3,3,4,5,5};
ListNode head=new ListNode(arrays[0]);
ListNode next=head;
for (int i=1;i<arrays.length;i++){
ListNode node=new ListNode(arrays[i]);
next.next=node;
next=node;
}
OK,链表数据是添加成功了。那么我们如何遍历这个链表呢?可以看到,上面的代码里面我们最后一个节点的next 一定是空的。所以我们这里通过while循环, 判断当前对象的next是否为空,为空了就表示是链表的最后一个节点了。
ListNode showParent=head;
while (showParent!=null){
System.out.println(showParent.val);
showParent=showParent.next;
}
有序链表去重问题
我们假设链表数据本身就是已经排序好了的数据。例如:
{1,2,2,3,3,4,5,5}
所以我们的判断思路是非常简单的。
- 循环到1,我们判断1的var 是否和2 的var 相同。这明显不相同,我们将 1的next 赋值给循环对象,进入到2的循环。
- 循环到第一个2,我们判断第一个2的var 是否和第2个2的var 是否相同,明显是相同的,那么我们就将第一个2的next 换成第2个2的next 也就是3,进入下一次循环。
- 还是循环到第一个2,现在第一个2的var 还是2,因为上一次循环把第一个2的next 换成了3,所以这次不相同,进入3的循环。
以此类推。
static public ListNode deleteDuplicates(ListNode head) {
ListNode cur = head;
while (cur != null && cur.next != null) {
if (cur.val == cur.next.val) {
cur.next = cur.next.next;
} else {
cur = cur.next;
}
}
return head;
}
可以看到,时间复杂度是O(n),空间复杂度是O(1)。
有序列表中删除重复元素
我们假设链表数据本身就是已经排序好了的数据。例如:
{1,2,2,3,3,4,5,5}
但是我们想要的是 {1,4} 。2,3,5 出现了重复数据,可能数据本身就不安全了。
要求:空间复杂度 O(n),时间复杂度 O(n)
进阶:空间复杂度 O(1),时间复杂度 O(n)
这里又一个概念很重要,那就是我们需要把没有重复的节点拼接成一个链表。
双重循环
我们假设一种概念,比如说 数据是:{1,1,2,2,3,3,4,5,5} 结合我们的需求,我们只能输出4。那么我们如何拿到头节点呢?为了保证我们新链表有一个永远不重复的头结点,同时便于计算, 我们可以向链表头部添加一个新的节点,比如说-1,那么这个时候的数据便成了 {-1,1,1,2,2,3,3,4,5,5} ,那么我们新链表的头结点就是我们新增的这个-1了。
- 遍历链表,每次比较相邻两个节点,如果遇到两个相邻的节点相同,则开新的内循环将这一段所有的相同都遍历过去。
- 在外层循环中链接跳出内循环的节点。
- 返回的时候去掉添加的表头。
static public ListNode deleteDuplicates(ListNode head) {
ListNode res=new ListNode(-1);
res.next=head;
ListNode cur=res;
while (cur.next!=null&&cur.next.next!=null) {
if (cur.next.val == cur.next.next.val) {
int temp=cur.next.val;
while (cur.next!=null&& cur.next.val==temp){
//
cur.next=cur.next.next;
}
} else {
cur = cur.next;
}
}
return res.next;
}
整体来说,每个节点只会遍历一次,所以时间复杂度是O(n),空间复杂度是O(1)。
hash表计数模式
hash表是一种根据关键key 直接访问值的一种数据结构。故可以通过hash表来统计频率、快速检验某个元素是否出现过等。
- 遍历一次链表,记录每个节点出现的次数。
- 添加一个不重复的表头。
- 再次遍历链表,判断链表中的次数,只有为1的留下,其他的都舍弃。
- 返回的时候,去掉添加到表头。
static public ListNode deleteDuplicates(ListNode head) {
Map<Integer, Integer> map = new HashMap<>();
ListNode cur = head;
while (cur != null) {
if (map.containsKey(cur.val)) {
map.put(cur.val, map.get(cur.val) + 1);
} else {
map.put(cur.val, 1);
}
cur = cur.next;
}
ListNode res = new ListNode(-1);
res.next = head;
cur = res;
while (cur!=null){
if (cur.next!=null){
if (map.get(cur.next.val)!=1){
cur.next=cur.next.next;
}else {
cur=cur.next;
}
}else {
cur= null;
}
}
return res.next;
}
时间复杂度是O(n),因为两次遍历。hash表每次计数,每次查询都是O(1)。 空间复杂度是O(n),因为基于hashMap 开辟了空间,最多是n.
通过双指针变量计数
思路还是:
- 先定义一个绝对不重复的头结点。
- 对真正的头结点和头结点的下一个节点的值进行对比,如果是相同的,统计数就加1,同时将循环节点的next节点换成next的next节点,重新进入循环,如果说不相同且统计数是0,那么就把当前节点设置到自定义节点的next里面,如果说大于0,就设置循环节点的next。
- 最终返回的时候,移除自定义的头结点。
实现代码:
static public ListNode deleteDuplicates(ListNode head) {
ListNode res = new ListNode(-1);
res.next = head;
ListNode cur = res;
ListNode pre = res;
int count=0;
while (cur!=null&&cur.next!=null){
if (cur.val==cur.next.val){
count++;
// 跳过next 节点。所以比对的还是当前的cur 和他的下一个节点。
cur.next=cur.next.next;
}else {
if (count>0){
// 因为我们删除所有重复的,所以赋值 cur.next.
pre.next=cur.next;
count=0;
}else {
pre=cur;
}
cur=cur.next;
}
}
if (count>0){
// 设置最后的节点为null。如果count 等于0,那么因为最后的节点的next 就是空就不需要设置。
pre.next= null;
}
return res.next;
}