上一篇 : 数据结构(二)
上节在数据结构(二)说了几个关于链表比较有思考性的算法,关于链表在原先周会学习的时候讲了链表的常见算法,所以接下继续把链表的几个常见的算法跟着写一下,因为我感觉除了数组外,就是链表了,所以讲几种链表常见的算法有利于自己写代码时思维发散,提高自己的编码能力,也可以提高代码的整体性能,减少时间损耗。
主要有两类:链表,和快速排序的思维方式
目录
- 链表
- 单链表反转(在前一节讲过)
- 链表的中环检测
- 两个有序链表的合并
- 删除链表倒数第N个节点
- 求链表的中间节点
- 快速排序
- 快排
- 归并
链表
链表的中环检测
意思:给一个链表,若其中包含环,请找出该链表的环的入口结点,否则,输出null。
核心思路:
** 其实 这个意思就好比A和B两个人同时在操场跑圈,A的速度比B快那么几步,这样在绕着操场跑的时候,总有一个时间A会赶上B,然后A和B重合了,快慢指针就是这个意思.**
这个用java写有两个思路:一. 快慢指针法(这个可以是一个通用的思路),另外一个就是用map自定义一个下标,然后存储这个下标作为标记判断是否有死循环,
解决思路1:快慢指针法
这是最常见的方法。思路就是有两个指针P1和P2,同时从头结点开始往下遍历链表中的所有节点。
P1是慢指针,一次遍历一个节点。
P2是快指针,一次遍历两个节点。
如果链表中没有环,P2和P1会先后遍历完所有的节点。
如果链表中有环,P2和P1则会先后进入环中,一直循环,并一定会在在某一次遍历中相遇。
因此,只要发现P2和P1相遇了,就可以判定链表中存在环。
解决思路1:利用map自定义下标索引,然后遍历链表存储,这个就是给每个节点定义一个唯一的标识,就是利用顺序1,2,3,4,5这样区别的,把索引当做key,节点当做value,遍历时如果利用key取出值了,就证明已经死循环了.
上代码:
先声明一个实体类存储链表
class TreeNode{
public int val;
public TreeNode next;
public TreeNode() {}
public TreeNode(int val) {
this.val = val;
}
public TreeNode(int val, TreeNode next) {
this.val = val;
this.next = next;
}
@Override
public String toString() {
return "TreeNode{" +
"val=" + val +
", next=" + next +
'}';
}
public static void main(String[] args) {
TreeNode node6 = new TreeNode(6);
TreeNode node5 = new TreeNode(5,node6);
// TreeNode node5 = new TreeNode(5);
TreeNode node4 = new TreeNode(4,node5);
TreeNode node3 = new TreeNode(3,node4);
TreeNode node2 = new TreeNode(2,node3);
TreeNode node1 = new TreeNode(1,node2);
}
}
链表中环
/**
* 链表中环检测
* 就是比如: 1->2->3->4->5->6->3
* 这就死循环了
* @param headNode
* @return
*/
public boolean LingkedZhongHuan(TreeNode headNode) {
if (headNode == null) {
return false;
}
TreeNode p = headNode;
TreeNode q = headNode.next;
// 快指针未能遍历完所有节点
while (q != null && q.next != null) {
p = p.next; // 遍历一个节点
q = q.next.next; // 遍历两个个节点
// 已到链表末尾
if (q == null) {
return false;
} else if (p == q) {
// 快慢指针相遇,存在环
return true;
}
}
return false;
}
基于上面这个中环监测再补充一个
关于在学递归的时候,递归需要注意的几个条件, 首先递归本质的条件: 终止条件和问题分解就不说了
需要注意的三点:
- 警惕栈溢出
- 防止重复条用
- 中环监测, 防止数据库的脏数据从而引起了死循环
//定义一个map用于存储查询结果
private final static Map<Integer,Integer> hashKey = new HashMap();
//定义一个深度值, 超过这个深度直接抛异常,避免引起栈溢出
private static int depth = 0;
public int f(int n){
++depth;
if (depth > 1000){ throw new OutputLengthException("长度过长!!");}
if (n == 1) {return 1;}
if (n == 2) {return 2;}
//每次递归前, 先判断一下map里有没有值,如果先前搜索过就直接取出来用
if (hashKey.containsKey(n)){return hashKey.get(n);}
int ret = f(n-1)+f(n-2);
hashKey.put(n,ret);
return ret;
}
下面这个是中环监测 , 虽然上面有那个代码, 但是感觉不太严谨 , 随即又有自己的想法,当然可以结合自己要求参照用
//防止中环
public boolean zhonghuan(TreeNode headNode){
if (headNode == null){
return false;
}
TreeNode q = headNode.next;
TreeNode s = headNode;
while(q != null){
if (q.next == null){
return false;
}
if (s == q){
return true;
}
q = q.next.next;
s = s.next;
}
return false;
}
两个有序链表的合并
其实就是把两个链表合并成一个链表,在合并的时候比较两个链表的对应值然后排序
1->5->3->7
2->6->4->8
1->2->5->6->3->4->7->8
可以反过来想 :假如是一个链表然后分而治之,然后排序,但这个分而治之只是分一次然后各个排序
/**
* 两个链表有序合并
* @param l1
* @param l2
* @return
*/
public TreeNode mergeTwoLists(TreeNode l1, TreeNode l2) {
if (l1 == null) return l2;
if (l2 == null) return l1;
TreeNode head = null;
if (l1.val <= l2.val){
head = l1;
head.next = mergeTwoLists(l1.next, l2);
} else {
head = l2;
head.next = mergeTwoLists(l1, l2.next);
}
return head;
}
删除链表倒数第N个节点
就是一个链表删除倒数第n个节点并返回这个链表
例如: 1->2->3->4->5->6 n=2
删除 5 ,结果:1->2->3->4->6
核心思路:
其实也是用的快慢指针的思想: 一个链表节点分别赋值个两个暂存的对象–就是A和B
然后要删除倒数n个节点, 先让A走n步,然后在遍历A的同时也让B遍历,这样当A遍历完的时候,B恰好遍历到那个倒数第n个节点,然后直接返回.
当然中间也是要有许多判断的,避免链表是空的或者遍历后是空的
上代码
/**
*链表删除倒数第n个节点并返回这个链表
* 例如: 1->2->3->4->5->6 n=2
* 删除 5 ,结果:1->2->3->4->6
* @param head
* @return
*/
public TreeNode middleRemoveNodeWithN(TreeNode head ,int n){
TreeNode p=head;
TreeNode q=head;
for(int i=0;i<n;i++){
p=p.next;
}
if(p==null){
head=head.next;
return head;
}
while(p.next!=null){
p=p.next;
q=q.next;
}
q.next=q.next.next;
return head;
}
求链表的中间节点
可以这样理解,就是求链表删除中间节点并返回这个节点
例如:
奇数: 1->2->3->4->5 删除3 并返回3
偶数: 1->2->3->4->5->6 删除4 并返回4
核心::
其实也是用的快慢指针的思想,A和B,遍历A的同时也遍历B,但是让A比B快一步,就是让A每次遍历的步数都是B的两倍
这里用两种方式实现: 1.普通的遍历循环 ,2递归 但是思路是一样的
上代码:
/**
*链表删除中间节点并返回这个节点
* 1->2->3->4->5 删除3 并返回3
* 1->2->3->4->5->6 删除4 并返回4
* @param head
* @return
*/
public TreeNode middleRemoveNode(TreeNode head){
if (head == null || head.next == null) return head;
TreeNode slow = head;
TreeNode fast = head;
while (fast != null && fast.next != null){
slow = slow.next;
fast = fast.next.next;
}
return fast == null ? slow:slow.next;
}
/**
*链表删除中间节点并返回这个节点- 递归
* @param head
* @return
*/
public TreeNode middleRemoveNodeByRecuser(TreeNode head){
if (head == null || head.next == null || head.next.next == null) return head;
TreeNode fast = head.next.next;
if (fast.next == null || fast.next.next == null) return head.next;
TreeNode middleNode = middleRemoveNodeByRecuser(head.next);
return middleNode;
}
快速排序
归并排序
归并排序算法是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。
代码:
package com.fufu.algorithm.sort;
import java.util.Arrays;
public class MergeSort {
public static int[] sort(int [] a) {
if (a.length <= 1) {
return a;
}
int num = a.length >> 1;
int[] left = Arrays.copyOfRange(a, 0, num);
int[] right = Arrays.copyOfRange(a, num, a.length);
return mergeTwoArray(sort(left), sort(right));
}
public static int[] mergeTwoArray(int[] a, int[] b) {
int i = 0, j = 0, k = 0;
int[] result = new int[a.length + b.length]; // 申请额外空间保存归并之后数据
while (i < a.length && j < b.length) { //选取两个序列中的较小值放入新数组
if (a[i] <= b[j]) {
result[k++] = a[i++];
} else {
result[k++] = b[j++];
}
}
while (i < a.length) { //序列a中多余的元素移入新数组
result[k++] = a[i++];
}
while (j < b.length) {//序列b中多余的元素移入新数组
result[k++] = b[j++];
}
return result;
}
public static void main(String[] args) {
int[] b = {3, 1, 5, 4};
System.out.println(Arrays.toString(sort(b)));
}
}
快速排序
俗称挖坑法, 就是先拿一个基数,然后两边开始遍历,左边比右边大了填坑填到右边去,反之填到左边. 但是要注意必须是先遍历左边,如果先遍历右边,在填坑的时候,到最后就会发现坑填不了,因为排序大小不对了.
上代码:
/**
* 快速排序(挖坑法递归)
* @param arr 待排序数组
* @param low 左边界
* @param high 右边界
*/
public static void sort(int arr[], int low, int high) {
if (arr == null || arr.length <= 0) {
return;
}
if (low >= high) {
return;
}
int left = low;
int right = high;
int temp = arr[left]; //挖坑1:保存基准的值
while (left < right) {
while (left < right && arr[right] >= temp) {
right--;
}
arr[left] = arr[right]; //坑2:从后向前找到比基准小的元素,插入到基准位置坑1中
while (left < right && arr[left] <= temp) {
left ++;
}
arr[right] = arr[left]; //坑3:从前往后找到比基准大的元素,放到刚才挖的坑2中
}
arr[left] = temp; //基准值填补到坑3中,准备分治递归快排
System.out.println("Sorting: " + Arrays.toString(arr));
sort(arr, low, left-1);
sort(arr, left + 1, high);
}
总结一下上述的算法其实核心都是有一个公式,就是每个代码它的实现的核心就是那一行或者两行,其他都是辅助的代码,而那个核心的代码其实就是一个数学公式的表达式.
所以说明, 数学思维方式很重要,这个有时间要好好巩固一下.