java基础巩固-宇宙第一AiYWM:为了维持生计,四大基础之数算总结_Part_1数组整起(两个有序数组合并、两个(K个)有序链表合并)

这篇博客介绍了数组合并、排序以及链表合并的常见操作,包括使用Java Stream流进行数组合并和排序,以及如何高效地合并两个有序链表和K个有序链表。文章探讨了不同操作的时间复杂度,并提供了具体的代码示例,如双指针法和优先级队列在链表合并中的应用。
摘要由CSDN通过智能技术生成

基础和技术就像无敌的深渊,小伙子,你要不断的学哟~~…
先来看看数组常用的操作的时间复杂度:
在这里插入图片描述

特此鸣谢在leetcode上分享答案的各位大神,让我能够对自己的笔记有如下补充:

  1. 数组合并(将两个数组进行合并)
public ... xxxXXX(int[] nums1, int[] nums2) {
        List<Integer> list = new ArrayList<>();

        //xxx.stream(nums1)就代表把xxx调用stream方法把数组nums1转换成为一个流,stream对象。转成流对象之后就可以调用流对象中的方法们进行链式编程。
        //流对象中有很多的方法,比如filter
        
        //下面两个流计算的作用就是将nums1和nums2进行合并
        list.addAll(Arrays.stream(nums1).boxed().collect(Collectors.toList()));
        list.addAll(Arrays.stream(nums2).boxed().collect(Collectors.toList()));

        //对合并好的大新数组进行排序
        Collections.sort(list);
        ......
  • Stream流式计算:(大数据以后玩的是存储+计算,咱们学的DB是用来存储的,计算交给Stream流计算)
    • java.util.stream:Interface Stream:函数式接口(也就是接口内部只能有一个抽象方法):比如咱们见过的Runnable接口、 Executor接口。源码中会用@FunctionalInterface在接口上面注。函数式接口咱们就可以用Lambda表达式去写,从而简化编程过程
      在这里插入图片描述
//时间复杂度为O(nlog(n))
class Solution {
    public double findMedianSortedArrays(int[] nums1, int[] nums2) {
        List<Integer> list = new ArrayList<>();

        //xxx.stream(nums1)就代表把xxx转换成为一个流,stream对象。转成流对象之后就可以调用流对象中的方法们进行链式编程。
        //流对象中的方法,比如filter
        
        //下面两个流计算的作用就是将nums1和nums2进行合并
        list.addAll(Arrays.stream(nums1).boxed().collect(Collectors.toList()));
        list.addAll(Arrays.stream(nums2).boxed().collect(Collectors.toList()));

        //对合并好的大新数组进行排序
        Collections.sort(list);
        if (list.size() == 0){
            return 0;
        }
        if(list.size()%2>0){
            return list.get(((list.size()+1)/2)-1);
        }else {
            int a = list.size()/2;
            return  (double)(list.get(a) +list.get(a-1))/2;
            }
    }
}
  1. 链表合并

第一种情况是将两个有序链表合并成为一个新的有序链表。
Notes:一般而言就是咱们要分别得到题目所给的两个链表或者说多个链表中的最小节点,然后一个一个按小到大的顺序拼接到dummy的屁股后面最重要的就是找到小节点接到某个东东的屁股后面,这个东东你可以找dummy也可以迭代)

  • 注意点,我们是靠指针在链表中的节点上不断游走的,所以最小节点是相对的,变化的。因为你list1这次把这个最小节点扫描到之后,拼接到dummy屁股后面了,那第二次再扫描时有可能是list1的第二个节点或者说list2的第一个节点…给的两个链表都是有序的,链表本身按小到大的顺序排好了,要是人家给的两个已知链表没有顺序,那岂不是麻烦大了。你都不知道从所给的链表中拿谁来接到dummy屁股后面,暴力扫描吗?
    在这里插入图片描述
ListNode mergeTwoLists(ListNode list1, ListNode list2){
    //dummy嘛,找一个虚拟的头节点(head,head说,那我是啥,你是实的呗),返回的时候就把dummy返回就行了
    ListNode dummy = new ListNode(0);
    ListNode cur = dummy;
    while(list1 != null && list2 != null){
        //比较list1和list2,将较小的节点接到dummy上,这不就一步一步把题目给的两个链表中所有节点按从小到大的顺序接到dummy中了嘛
        if (list1.val > list2.val){
            cur.next = list2;//把list1和list2中小的节点接到dummy屁股后面
            list2 = list2.next;//指针向前移动一步
        } else{
            cur.next = list1;
            list1 = list1.next;//指针向前移动一步
        }
        cur = cur.next;//指针向前移动一步
    }
    
    //当两个链表中后面有重复的元素的话,光靠上面的while循环是会漏掉重复的元素的,你漏掉了还能叫拼接成功吗?,所以下面这就是把两个链表中谁剩下来了重复元素呀,快来,拼接到屁股后面哦,
    //剩下的,肯定是要接在dummy最后面的相对较大的大元素了呗
    //当然啦,三目运算符整起来也阔以喽
    //cur.next = list1 != null ? list1 : list2;
    //直接往dummy屁股后面接就行
    if(list1 != null){
        cur.next = list1;
    }
    
    //剩下的,肯定是要接在dummy最后面的相对较大的大元素了呗
    //直接往dummy屁股后面接就行
    if(list2 != null){
        cur.next = list2;
    }
    return dummy.next;
}

有以下几个注意点:

  • 链表中的XXX = XXX.next就相当于XXX++,也就是XXX指针向前步进一步
  • return dummy.next意思是,一般链表这里我们会找一个head头指针的替身,也就是一个虚拟的头节点(head说,他是虚拟的头节点,那我是啥?,观众回答,你是实的呗),返回的时候就把dummy返回就行了,返回的时候一般写的就是dummy.next。
    • 用dummy虚拟节点可以节省很多事,简化代码
    • 并且用dummy作为一个占位符,可以避免处理空指针的情况,意思就是假设链表总共有5个节点,题目让咱们删除倒数第5个节点,也就是让咱们删除第一个节点,那么按照咱们一般的算法思路的逻辑,应该先找到倒数第6个节点,但是第一个节点前面已经没有节点了,找的话就会报错,所以用一个虚拟的头节点就可以避免出现这种情况
      还有就是有位leetcode大佬的迭代写法:
/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
    	//官话判空
        if(list1 == null){
			return list2;
		} 
        if(list2 == null){
			return list1;
		} 
        if(list1.val <= list2.val) {
            list1.next = mergeTwoLists(list1.next, list2);;//还是将返回的小节点list接到屁股后面呀
            return list1
        } else {
            list2.next = mergeTwoLists(list1, list2.next);;//还是将返回的小节点list接到屁股后面呀
            return list2;
        }
    }
}

第二种情况是将K个有序链表合并成为一个新的有序链表:
思路是差不多的,

dummy.next = 要拼接的那个最小节点
......
再把dummy.next返回就行

现在有了K个链表,怎么找到K个链表中那么多的节点中的最小节点呢

  • 此时用一个牛叉结构:优先级Queue(属于二叉堆的一种)
    • 优先级队列的最大特点就是可以对插入的元素们自动排序,并且可以按照从小到大或者从大到小的顺序取出元素(因为都是从顶上往出挤呢嘛)
    • 把链表节点依次放入一个最小堆中就可以每次获得K个节点中的最小节点。
ListNode mergeKLists(ListNode[] lists){
    //官话判空
    if(lists.length == 0){
        return null;
    }
    //dummy嘛,找一个虚拟的头节点(head,head说,那我是啥,你是实的呗),返回的时候就把dummy返回就行了
    ListNode dummy = new ListNode();
    ListNode cur = dummy;
    //用优先级队列构造一个最小堆出来
    //所有的链表中的节点都会被加入和弹出队列,所以此算法的时间复杂度是O(NlogK),K指链表的条数,N是这些链表的节点总数.
    PriorityQueue<ListNode> priorityQueue = new PriorityQueue<>(//优先队列中的元素个数最多是K(这要解释吗能不能看看题目),所以一次poll或者add方法的时间复杂度是O(logk)
        Comparator.comparingInt(o -> o.val)
        //这其实相当于Comparator.comparingInt((o) -> {return o.val})
    );
    //将k个链表的头节点head加入到最小堆里面
    for(ListNode head : lists){
        if(head != null){
            priorityQueue.offer(head);
        }
    }
    
    while(!priorityQueue.isEmpty()){
        //将lists中较小的节点接到dummy上,这不就一步一步把题目给的K个链表(lists不是代表链表数组嘛)中所有节点按从小到大的顺序接到dummy中了嘛
        ListNode tempNode =  priorityQueue.poll();
        cur.next = tempNode;
        //dummy指针不断前进
        cur = cur.next;
        //优先队列priorityQueue中的元素个数最多是K个,所以一次poll()或者add()方法的时间复杂度为O(logk),所有的链表节点都会被加入和弹出priorityQueue,所以算法整体复杂度为O(Nlogk),k代表链表的条数,N是这些链表的节点总数
        if (tempNode.next != null){
            priorityQueue.offer(tempNode.next);
        }
    }
    return dummy.next;
}
  • 想着是直接用dummy而就不用再ListNode cur = dummy,提交后发现直接用dummy答案会缺少,这是在合并两个链表和合并K个链表里面都试出来的
  • 另一个就是priorityQueue.poll().next、dummy.next = priorityQueue.poll();这种写法会报错,然后找一个中间节点转换一下就成了,所以是不是提示自己别想偷懒哦

## 数组part ##

eg1.给定一个数字val,不能创建新数组,只能在原来数组中去除与val相等的数。

POINT1:但凡输入参数中有数组,先来判空官话

if(nums == null || nums.length == 0){
	return 0;
}

slove1:双指针–首尾指针

int frontToEndPoint = 0;
int backToFrontPoint = nums.length - 1;
//一个从最后一个开始,当后指针与前指针相遇时终止循环。
while(frontToEndPoint < backToFrontPoint){
	//后指针向前扫,扫不到指定元素停下来(说明这个货要被换到前边去)不能再向前移动,扫到了指定元素说明指定元素已经在后面了,所以我就不动你了,我后指针就向前正常走就完事了
	while(frontToEndPoint < backToFrontPoint && val == backToFrontPoint){
		backToFrontPoint--;
	}
	//前指针开始往后扫,扫不到指定元素(人家让挑出来的元素)就前指针向后移,否则(就是扫到了)就原地等待不能向后移动
	while(frontToEndPoint < backToFrontPoint && val != frontToEndPoint){
		frontToEndPoint++;
	}
	//后指针扫到非指定元素(说明你不该呆在后面,指定元素才改呆在后面),前指针扫到指定元素(说明你指定元素不该呆在前面,该呆在后面去),开换呗。
	int temp = nums[frontToEndPoint];
	nums[frontToEndPoint] = temp;
	nums[backToFrontPoint] = nums[frontToEndPoint];
	}
//用一个三目运算符实现,因为我扫完换完之后前指针和后指针相遇了后,说明前指针和后指针所在位置的前面都是非指定元素,即要输出的内容。
return nums[frontToEndPoint] == val ? frontToEndPoint : frontToEndPoint+1;

slove2:双指针–快慢指针

//这样i从一开始就比j多加了个1,所以就形成快慢指针,就实现了把位于前面的指定元素给换到后面去
int j = 0;
for(int i = 0; i < nums.length; i++){//i就是快指针,j就是慢指针
	if(nums[i] != val){
		nums[j] = nums[i];//虽然说这样也可能会让两个都不是指定元素的元素在那里换来换去,不过,肯定不会放过指定元素的,换就换吧。
		j++;
	}
}
return j;//因为j是满指针,到最后j前面的肯定都是非指定元素喽,放心输出

eg2.问题:将所给数组中所有0元素移到数组末尾,并保持数组中其余元素相对位置保持不变。

slove3:双指针–快慢指针

/**
 * 总体思路:快慢指针
 * @param arr
 * @return
 * @author HuHongBo
 */
public int[] MoveZero(int[] arr) {
	int index = 0;//定义慢指针,慢指针有个重要的作用就是,他指的位置要被快指针指的位置的元素替换,
	/**
	 * 官话,有入参为数组时,必须写上
	 */
	while (arr.length == 0 || arr == null) {
		return null;
	}
	
	/**
	 * 
	 */
	for (int i = 0; i < arr.length; i++) {
		/**
		 * 利用快指针去判断数组中哪个元素不为0,哪个元素为0,然后不为0时,慢指针向前走一步。
		 * 慢指针每次指的位置就是(你不是用arr[i] != 0判断了哪个元素不为0嘛)要用不为0的元素去替换的位置
		 */
		
		if (arr[i] != 0) {
			arr[index] = arr[i];//利用后面快指针甄别出的非零元素,按顺序替换前面的元素
			index++;//使慢指针向前一小步
		}
	}
	/**
	 * 下面循环的作用就是,你不是把后面的元素按顺序都复制到前面去了吗,那后面你是不是得,按照此题来说,就给人家都赋值
	 * 为0嘛
	 */
	for (int i = index; i < arr.length; i++) {
		arr[i] = 0;
	}
	return arr;
}

eg3.最大的连续1的个数。

public int NumberOfOne(int[] arr){
	int countTemp = 0;
	int count = 0;
	while (arr == null || arr.length == 0) {
		return 0;
	}
	for (int i = 0; i < arr.length; i++) {
		if (arr[i] == 1) {
			count++;
		}else{
			countTemp = max(count, countTemp);
			count = 0;
		}
	}
	return countTemp;
}

public int max(int count, int countTemp) {
	if (count < countTemp) {
		countTemp = countTemp;
	}else{
		countTemp = count;
	}
	return countTemp;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值