持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第四天,点击查看活动详情
最近在看左神的数据结构与算法,考虑到视频讲的内容和给出的资料的PDF有些出入,不方便去复习,打算出一个左神的数据结构与算法的笔记系列供大家复习,同时也可以加深自己对于这些知识的掌握,该系列以视频的集数为分隔,今天是第四篇:P6|链表
一、排序算法总结:
一个数组中,如果有多个相同的数,在排序结束后是否能保证相对的次序不变,对于基础类型而言其实作用不大,而对于非基本类型,比如对学生这个类型进行排序,排序的第一次标准为年龄,那么所有的学生都按照年龄排序了,然后根据班级排序,如果排序具有稳定性,那么此时按照班级排序后,班级内部的人的年龄也是排序好了的
1、排序算法的稳定性
- 选择排序做不到稳定性
- 冒泡排序相等的时候不做交换可以做到稳定性
- 插入排序相等的时候不做交换也可以做到稳定性
- 归并排序merge的时候左侧和右侧相等的时候拷贝左边的可以做到稳定性
- 1 1 2 3 5 和 1 1 5 65,先拷贝左边的 1,就可以保证稳定性
- 快速排序做不到稳定性
- 5 5 5 5 5 3 现在以 5 作为分界值,就会出现第一个 5 与 3 做交换,就导致了不稳定
- 堆排序做不到稳定性,在进行堆化,插入的时候会破坏稳定性
- 5 4 4 插入 6 会变成 6 5 4 4 此时的两个 4 已经换位置了
- 基数、计数排序不基于比较,可以做到稳定性
2、排序算法的总结
排序算法 | 时间复杂度 | 空间复杂度 | 稳定性 |
---|---|---|---|
选择排序 | O(N²) | O(1) | × |
冒泡排序 | O(N²) | O(1) | √ |
插入排序 | O(N²) | O(1) | √ |
归并排序 | O(N*logN) | O(N) | √ |
快速排序 | O(N*logN) | O(logN) | × |
堆排序 | O(N*logN) | O(1) | × |
3、使用情况
- 一般情况下,使用快速排序。尽管它的时间复杂度指标与其他几个排序一致,但是经过实验去检测,它的常数项偏低,而且耗时比较短。
01 stable sort
可以让它做到稳定性,但是空间复杂度变成了O(N)
- 堆排序的话,因为空间复杂度比较低,因此需要使用空间较少的使用它
- 归并排序,是在使用时需要保证稳定性的时候,使用它,使用
内部缓存法
可以把归并排序的额外空间复杂度变成O(1)
,但是变完之后就不稳定了,原地归并排序可以让空间复杂度变为O(1)
,但是时间复杂度变成了O(n²)
。 - 不难发现,尽管快排和归并排序都有自己的优化方案,但是在进行所谓的优化后,尽管解决了一些现有的问题,但是空间复杂度或者时间复杂度就又变成了另外的算法,所以其实优化的意义不大
4、变式
题目:奇数放在数组左边,偶数放在数组右边,要求原始的相对次序不变
- 这个可以联想到快速排序,因为在做
partition
的时候,≤ 的放左边,> 的放右边,即非0即1,此时其实就是奇数和偶数的关系,但是要保证稳定性就要使用01 stable sort
,也就是说可以,但是很难 hhh
5、排序算法的综合
当样本量比较小的时候,使用插入排序,虽然快排时间复杂度看起来很好,小样本量,插入排序的常数时间更低,瓶颈不明显,这种排序就是利用各种排序算法优势进行一个综合。
Java中的Arrays.sort() 在使用时候,如果是基础类型就会选用快排,而如果是引用类型就会选用归并,为了保证稳定性,因为基本类型的稳定性无意义
二、链表
其实链表的题目使用哈希表、栈等方法能做出来,但是这些方法的问题所在就是:会开辟多的内存空间,在下面的笔记中,我不会贴出这类方法,只是对大致的思路做一个了解就行了
判断一个链表是否为回文的
1、实现思路
- 方法一:可以使用栈结构,利用栈的先进后出的方法,弹栈与原链表做比较,但是消耗空间比较多,如果要更省空间,就可以只放一般的数据即右半部分的数据进去。但是出现了新的问题:如何区分出右边的数据?
- 找到右边的数据,即要找到中点位置,就需要用到快慢指针,快指针一次走两步,慢指针一次只走一步,快指针走完的时候,慢指针就到达了中点
- 如何把慢指针右侧的数据放进栈中即可
- 方法二:在进行遍历的过程中,把中点位置指向空,右边的变量的链表往中间指,然后头尾指针做遍历和比较
2、图解举例
① 借用栈
- 对于链表 1 2 3 2 1
- 首先使用快慢指针(都从头结点出发,快指针一次走两步,慢指针一次走一步,快指针到达链表尾结点,那么慢指针就到链表中点)找到链表的中点
- 然后慢指针向右遍历,依次压栈 3 2 1
- 利用栈先进后出的特性,头指针遍历与栈中元素作比较,直到栈空
② 不使用额外空间的方法
- 对于链表 1 2 3 2 1
- 还是先试用快慢指针找到链表的中点,然后让快指针指向中点的下一个,而慢指针就指向空,即中点指向空
- 然后快指针依次遍历后面的链表,让链表反转
- 最后左边是头结点,右边是快指针,往中间一起遍历,比较值,如果遍历到最后指向空,那就是回文链表,如果中间有值不一样,就是非回文链表
3、代码实现
public static boolean isPalindrome3(Node head) {
if (head == null || head.next == null) {
return true;
}
Node n1 = head;
Node n2 = head;
// 找链表的中点
while (n2.next != null && n2.next.next != null) {
n1 = n1.next;
n2 = n2.next.next;
}
// 快指针指向中点的下一个
n2 = n1.next;
// 中点的链表指向null,作为等会循环结束的条件
n1.next = null;
Node n3 = null;
// 右侧的链表反转
while (n2 != null) {
n3 = n2.next;
n2.next = n1;
n1 = n2;
n2 = n3;
}
n3 = n1;
n2 = head;
// 从链表的两侧做遍历
boolean res = true;
while (n1 != null && n2 != null) {
if (n1.value != n2.value) {
res = false;
break;
}
n1 = n1.next;
n2 = n2.next;
}
n1 = n3.next;
n3.next = null;
while (n1 != null) {
n2 = n1.next;
n1.next = n3;
n3 = n1;
n1 = n2;
}
return res;
}
把单向链表按照某个值划分为左边小、中间相等、右边大的形式
1、实现思路
- 可以使用数组结构,创建一个 Node[] 数组,把所有的结点放进来,然后做 partition,然后把所有的结点串起来
- 使用六个指针,分别是:小于这个数的指针头、小于这个数的指针尾、等于这个数的指针头、等于这个数的指针尾、大于这个数的指针头、大于这个数的指针尾
- 对所有的数进行遍历,依次加进三种情况的指针里面
- 最后把所有的指针串起来
- 要注意:串起来的时候,一定要做边界判断!因为很有可能有空指针
2、图解举例
- 链表 0 2 3 2 1 假设这个数为 2
- 创建六个指针:小于这个数的指针头、小于这个数的指针尾、等于这个数的指针头、等于这个数的指针尾、大于这个数的指针头、大于这个数的指针尾
- 然后遍历链表:
- 0 放在小于这个数的指针头和指针尾
- 2 放在等于这个数的指针头和指针尾
- 3 放在大于这个数的指针头和指针尾
- 2 放在等于这书的指针尾,并调整指向
- 1 放在小于这个数的指针尾,也调整指向
- 最后把六个指针串起来,要做边界判断,比如没有小于2的数,那么小于该数的两个指针都是空指针,串起来的时候就会有问题,因此要做边界判断
3、代码实现
public static Node listPartition2(Node head, int pivot) {
Node sH = null; // less head
Node sT = null; // less tail
Node eH = null; // equal head
Node eT = null; // equal tail
Node bH = null; // more head
Node bT = null; // more tail
Node next = null; // 遍历指针
{
next = head.next;
head.next = null;
if (head.value < pivot) {
if (sH == null) {
sH = head;
sT = head;
} else {
sT.next = head;
sT = head;
}
} else if (head.value == pivot) {
if (eH == null) {
eH = head;
eT = head;
} else {
eT.next = head;
eT = head;
}
} else {
if (bH == null) {
bH = head;
bT = head;
} else {
bT.next = head;
bT = head;
}
}
head = next;
}
if (sT != null) {
sT.next = eH;
eT = eT == null ? sT : eT;
}
if (eT != null) {
eT.next = bH;
}
return sH != null ? sH : eH != null ? eH : bH;
}
复制含有随机指针结点的链表
一种特殊的单链表节点类描述如下:
Class Node{
int value;
Node next;
Node rand;
Node(int val){
value = val
}
}
rand指针是单链表节点结构中新增的指针,rand 可能指向链表中的任何一个结点,也有可能指向 null。 给定一个由 Node 结点类型组成的无环单链表的头结点 head,请实现一个函数完成这个链表的复制,并返回复制新链表的头结点
1、实现思路
- 可以使用哈希表,key 为该单链表节点,value 为创建的值相同的链表结点。
- 依次去遍历单链表,首先根据遍历的当前结点,去拿到复制的结点
- 然后把复制的那个结点的下一个指向当前结点的下一个去哈希表里面取的那个值
- 随机的指向当前结点指向的随机的去哈希表里面去拿的
- 语言组织起来可能有点乱,可以看下面的图解举例。
- 另一种实现方法:在每个结点的后面插入一个复制的结点
- 依次去遍历单链表,此时的单链表由于加入了复制的结点,导致长度扩为两倍,此时我们需要遍历的只是复制前的链表,因此链表一次走两步
- 找到当前链表的next和ramdom指向,让当前结点的下一个结点,也就是那个复制结点的next和random分别指向原结点的next和random的下一个
- 具体可以看下面的图解
2、图解举例
方法一
方法二
3、代码实现
public static Node copyListWithRand2(Node head) {
if (head == null) {
return null;
}
Node cur = head;
Node next = null;
// copy node and link to every node
while (cur != null) {
next = cur.next;
cur.next = new Node(cur.value);
cur.next.next = next;
cur = next;
}
cur = head;
Node curCopy = null;
// set copy node rand
while (cur != null) {
next = cur.next.next;
curCopy = cur.next;
curCopy.rand = cur.rand != null ? cur.rand.next : null;
cur = next;
}
Node res = head.next;
cur = head;
// split
while (cur != null) {
next = cur.next.next;
curCopy = cur.next;
cur.next = next;
curCopy.next = next != null ? next.next : null;
cur = next;
}
return res;
}