Giter Site home page Giter Site logo

algorithm004-05's Introduction

极客大学「算法训练营第四期 - 5 班」作业提交仓库

请大家通过该链接查看讲师课件并进行下载: https://pan.baidu.com/s/1O6HNBmmIzLptVUedLvO18Q 密码:5a9g

《算法四期1-5班社群往期分享汇总》 https://shimo.im/docs/hdx98DVvVwDC6PyC/

仓库目录结构说明

  1. Week_01/ 代表第一周作业提交目录,以此类推。
  2. Week_01/id_089代表学号为 089 的学员第一周的作业提交目录,以此类推。
  3. 每个目录下面均有一个 NOTE.md 文档,你可以将自己当周的学习心得以及做题过程中的思考记录在该文档中(该项不属于作业内容,是可选项)。

作业提交规则

算法题作业的提交

  1. 先将本仓库 fork 到自己的 GitHub 账号下。
  2. fork 后的仓库 clone 到本地,然后在本地新建、修改自己的代码作业文件,注意: 仅允许在和自己学号对应的目录下新建或修改自己的代码作业。作业完成后,将相关代码 push 到自己的 GitHub 远程仓库。
  3. 提交 Pull Request 给本仓库,Pull 作业时,必须备注自己的学号和提交第几周的作业,如089-Week 02,是指学号为089的学员提交的第二周的算法题作业。
  4. 代码文件命名规则:**LeetCode_题目序号_学号,比如学号为 089 的学员完成 LeetCode_33_108 的第 2 题 后,请将代码文件名保存为 LeetCode_2_089.py (假设你使用的是 Python 语言)。
  5. 务必按照Pull Request的备注形式和作业文件的命名进行提交,班主任会严格按照命名形式统计大家的作业。

学习总结的提交

  1. 除代码作业外,我们要求学员每周提交一篇当周的学习感想,直接发一个独立的 issue 即可,issue 标题格式为【学号-周】总结题目】。例如【089-week 03】二叉树的更多理解是指学号089的学员提交的第三周的学习总结。可参考:#1

Review 与点评

  1. 我们要求学员每周至少Review并点评其他5位同学的作业,点评直接回复在代码作业或学习总结下面都可。

注意事项

  1. 作业公布地址: 我们会在置顶的 issue 中公布当周的算法练习题以及其他注意事项。
  2. 如果对 Git 和 GitHub 不太了解,请参考 Git 官方文档 或者极客时间的《玩转 Git 三剑客》视频课程。

algorithm004-05's People

Contributors

allenyn avatar brucejoe1988 avatar cindyyu759 avatar destroyer399 avatar dorianhe avatar gitleer avatar guojiangwei avatar hjzheng avatar ismind avatar jerryfound avatar laojinxing avatar larryrishi avatar lichangke avatar maximusliu avatar melody-li avatar nxyanliang avatar oahgnod avatar qianxingchuan avatar rogerbu avatar rubyliuruby avatar shizeying avatar simonchu80 avatar suilin1254703825 avatar theshaodi avatar tiarhpl avatar weixun1209 avatar zhangxu-git avatar zhongwenyu004 avatar zhou199252 avatar zyfree avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

algorithm004-05's Issues

【640-week 01】第一周学习总结

在这第一周的学习,跟着老师,学习了数组,链表,栈,队列等相关知识,还通过LeetCode上学习了这些数据结构的具体用法。

在这第一周的学习中,我感觉对我的帮助很大。对于我这样一个非科班出身,以前也没有什么积累的新人来说,老师的课程讲的很详细,并且学会了如何利用LeetCode来做题,来提高自己。

总结一下在这一周中学习到的知识点。

1,升维** 用空间换取时间

2,没有解题思路时应该怎么办:

​ 1)暴力能否解决

​ 2)这道题的基本情况是什么样的,能否在几个基本情况中找出最近重复子问题

3,双指针的**,两边夹逼的**

4,栈和队列的思维导图

思维导图

5,编程是将现实中的问题抽象为数学问题,然后用编程来解决。多思考这个数据结构在现实中的应用是什么。

review:

1,535同学的“Priority Queue学习总结”这一部分写的很好,让我对于priority queue有了更深的认识。

2,185同学的代码写的很简洁,如果有注释就更好了。

3,230同学提交了很多道作业,并且代码写的很简洁,还有相应的注释和自己的思考。

4,060同学的代码前有题目的英文说明,如果程序中有注释的话可读性会更好一些。

5,115同学有时间复杂度的分析这一项内容,这一点值得我们去学习。

【240-week 01】第一周总结

第一周调整心态,找到学习这门课程的状态。总结提炼通用算法和思路,可供随时查阅

1.移动数组内某个元素到最后

//1.遍历数组
//2.数值 === target删除该项,并添加到最后。游标不变
//3.数值 !== target 游标++
function moveToEnd(arr, target) {
  let temp = 0;
  for (let i = 0; i < arr.length; i++) {
    if (arr[temp] === target) {
      arr.splice(temp, 1);
      arr[arr.length] = target;
    } else {
      temp++;
    }
  }
}

2.链表转数组

function listToArray(list) {
  let arr = [];
  while (list !== null) {
    arr.push(list.val);
    list = list.next;
  }
  return arr;
}

3.数组转链表

function arrayToList(array) {
  if (!array.length) {
    return null;
  }
  let head = { val: array[0], next: null };
  let pnode = head;
  for (let i = 1; i < array.length; i++) {
    let node = { val: array[i], next: null };
    pnode.next = node;
    pnode = pnode.next;
  }
  return head;
}

关于链表操作的思考

  • 自身对链表的操作不熟
  • 结合自身理解
    1. list -> array
    2. handle array
    3. array -> list

【755-week1 】学习总结

第一周总结

关于insert函数

问题

之前对于python中insert函数的理解一直是例如test_lst.insert(desired_index, inserted_value)inserted_value将会被插入到test_lstdesired_index的位置上。但是一次无意中尝试将desired_index设置成-1却产生了与设想中不一样的效果。如果执行test_lst.insert(-1, -1),结果为test_lst = [1,2,-1,3]-1其实在index为-2的位置上。

基于源码的解析

insert()函数源码如下:

ins1(PyListObject *self, Py_ssize_t where, PyObject *v)
if (where < 0) {
        where += n;
        if (where < 0)
            where = 0;
    }
    if (where > n)
where = n;

其中where即为之前的desired_indexn为list的长度。可见若where < 0 and where + n < 0,被插入的元素将直接被插在list首部。

总结

list.insert(index, element)

  • If index >= 0, the element is inserted at that index.
  • If index < 0 and index + len(list) >= 0, then element is inserted at index + len(list)
  • If index < 0 and index + len(list) < 0, then element is inserted at the index of 0.

关于remove函数

问题

list的remove()函数只会找到被移除首个匹配的元素,为什么

基于源码的分析

相关源码展示如下

list_remove(PyListObject *self, PyObject *value)
/*[clinic end generated code: output=f087e1951a5e30d1 input=2dc2ba5bb2fb1f82]*/
{
    Py_ssize_t i;

    for (i = 0; i < Py_SIZE(self); i++) {
        int cmp = PyObject_RichCompareBool(self->ob_item[i], value, Py_EQ);
        if (cmp > 0) {
            if (list_ass_slice(self, i, i+1,
                               (PyObject *)NULL) == 0)
                Py_RETURN_NONE;
            return NULL;
        }
        else if (cmp < 0)
            return NULL;
    }
    PyErr_SetString(PyExc_ValueError, "list.remove(x): x not in list");
    return NULL;
}

总结

之所以remove函数有着这样的特性,其实就是因为源码是单个loop,一旦有结果就return

关于Queue

问题

在python中队列可以用数据结构list或者linked list来实现。也有一个queue库。其中queue.Queue则是教科书中常见的FIFO队列,也有queue.PriorityQueue。同时在collections库中,有个所谓的deque (double-ended queue)。一个名为heapq的库则实现了优先队列。那么这些库之间到底是什么关系呢。

基于源码的解答

queue库的源码在这里。 于200行,可见queue.Queue继承于deque类。

那么deque的源码在这里。其实是基于双向链表实现的。

queue.PriorityQueue的源码在于214行。继承于Queue类但是对于putget方法都是基于heapq

heapq的库的源码在,是基于最小堆实现的。

这几个库的关系如下图所示
queue_priorityqueue_summary

给同学的评价放在了代码文件夹的NOTE.md文件里了

附带一些代码的英文总结在这里(notebook文件)

【330-week1】Queue/PriorityQueue 源码分析

NOTE

第一题、将以下事例改为新API重新实现

原题

import java.util.LinkedList;

public class DequeTest {
    public static void main(String[] args) {
        LinkedList<String> deque = new LinkedList<>();
        deque.push("a");
        deque.push("b");
        deque.push("c");
        System.out.println(deque);
        String str = deque.peek();
        System.out.println(str);
        System.out.println(deque);
        while (deque.size() > 0) {
            System.out.println(deque.pop());
        }
        System.out.println(deque);
    }
}
运行结果:
[c, b, a]
c
[c, b, a]
c
b
a
[]

新版API实现

import java.util.LinkedList;

public class MergeSortedArray {
    public static void main(String[] args) {
        LinkedList<String> newDeque = new LinkedList<>();
        newDeque.addFirst("a");
        newDeque.addFirst("b");
        newDeque.addFirst("c");
        System.out.println(newDeque);
        String newStr = newDeque.peekFirst();
        System.out.println(newStr);
        System.out.println(newDeque);
        while (newDeque.size() > 0) {
            System.out.println(newDeque.removeFirst());
        }
        System.out.println(newDeque);
    }
}
运行结果:
[c, b, a]
c
[c, b, a]
c
b
a
[]

第二题、分析源码

Queue 源码分析

1、继承父类

Queue是一个接口,由子类来具体实现。
Collection --> Iterable --> Object

2、方法

        引发异常      返回特殊值
插入      add(e)      offer(e)
去掉      remove()    poll()
检查      element()   peek()

PriorityQueue 源码分析

1、继承父类

AbstractQueue (impl Serializable)--> AbstractCollection (impl Queue) --> Collection --> Iterable --> Object

2、分析

1. 底层实现是数组:
    transient Object[] queue; // non-private to simplify nested class access
1. 默认长度为11
   private static final int DEFAULT_INITIAL_CAPACITY = 11;
   public PriorityQueue() { this(DEFAULT_INITIAL_CAPACITY, null); }
3. 扩容策略:size < 64 ? (size + 2) : (size * 2)
   最大size:MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
   如果扩容size > MAX_ARRAY_SIZE, return oldSize + 1 , 如果没有超出MAX_ARRAY_SIZE的话
2. 默认排序按字典表顺序进行排序
3. 可以自定义排序方式, 构造方法传入继承Comparator的实现类即可
   PriorityQueue(int initialCapacity, Comparator<? super E> comparator) 
4. PriorityQueue(Collection<? extends E> c) 此构造方法,如果实现子类为SortedSet、PriorityQueue(其本身), 使用传入实现类的比较器。
   其他Collection 子类则使用默认字典排序, 因为SortedSet、PriorityQueue 自身实现了Comparator
5. 核心**是使用最小堆结构, 在对queue做poll、remove、add、offer等改变性操作时源码逻辑,主要由两个方法控制:siftUp、siftDown
   siftUp (siftUpComparable、siftUpUsingComparator): 参数:k、key
     循环 (向上和自己父结点比较小就交换,大就退出)最后将queue[k] = key
   siftDown(siftDownComparable、siftDownComparable):
     循环 (向下和自己较小的子结点(两个子节点先比较谁小)比较大就交换,小就退出)最后将queue[k] = key

【445+week1】java PriorityQueu 源码解读

java优先级队列的底层数据存储采用数组。定义如下:
transient Object[] queue;
调用add方法添加元素,PriorityQueue采用泛型的方式,此处的E代表声明PriorityQueue时使用的实际数据类型
public boolean add(E e) {
return offer(e);
}
add方法继续调用offer方法添加元素,所以用户可以直接使用offer来添加元素
public boolean add(E e) {
return offer(e);
}

offer方法中会进行条件判断:
1,判断传入是否为nullpointer
2,队列的空间是否够,如果不够调用grow函数扩大队列
3,队列第一次添加元素直接给queue[0]赋值
4,一般情况调用siftUp(i, e);方法,此时i为当前队列尺寸,也可以理解为队列末尾,e为准备条件的元素
public boolean offer(E e) {
if (e == null)
throw new NullPointerException();
modCount++;
int i = size;
if (i >= queue.length)
grow(i + 1);
size = i + 1;
if (i == 0)
queue[0] = e;
else
siftUp(i, e);
return true;
}

在siftUp中会调用比较器,如果用户设置了比较器就采用设置的比较器,如果没设置比较器使用默认比较方法
private void siftUp(int k, E x) {
if (comparator != null)
siftUpUsingComparator(k, x);
else
siftUpComparable(k, x);
}

典型的折半查找int parent = (k - 1) >>> 1; 该语句每次讲下标除以2 下取整
private void siftUpComparable(int k, E x) {
Comparable<? super E> key = (Comparable<? super E>) x;
while (k > 0) {
int parent = (k - 1) >>> 1;
Object e = queue[parent];
if (key.compareTo((E) e) >= 0)
break;
queue[k] = e;
k = parent;
}
queue[k] = key;
}

【490-week 01】学习总结

学习总结

本周主要学习了几个线性数据结构:数组、链表、跳表、堆、栈

数组和链表的对比:

数组 链表
插入、删除 O(n) O(1)
随机访问 O(1) O(n)

本周整体完成的作业比预期的少,只完成了8道题,而且大部分题目都只完成了一遍,没有隔天后再次练习,按照这样的练习强度,学习的效果不会太好。

下周的目标是一周完成20道题以上,并且题目都复习一遍。

Review与点评

120:提交的题目都只使用了一种解法,建议多尝试几种解法,开拓思路,为了便于其他同学点评,建议在代码里面附上Leetcode题目名称和链接。

455:完成的练习题目很多,且很多题目使用了多种解法,使用了C和python语言完成,一个题目建议使用两种以上自己熟悉的语言实现,可以加深对算法的理解。

030:旋转数组题目完成得比较好,尝试了多种解法,其他的题目建议也尝试多种解法,题目提交得比较少,建议多练习。

115:大部分题目都尝试使用多种解法,并仔细对比了每种解法的耗时和内存消耗,建议下一周做更多的练习。

005:题目基本是使用我比较熟悉的PHP实现的,大部分题目的性能都比较高,有一些解法有值得我参考学习的地方。

【545-week 01】重新认识数据结构和算法

概述

由于我是非计算机专业,一直没有接触过算法,总觉得算法是高不可攀的,也一直对算法是畏惧的。不过自己对人工智能有特别兴趣,而且现在面试都会涉及到算法。于是决定啃下算法这块,让自己能更好的去编写代码和解决问题。

经过这一周的学习,主要学习了数据结构:数组(array)、链表(linked list)、跳表(skip list)、栈(stack)、队列(queue)、双向队列(deques)、优先队列(priority queue),算法:迭代、递归、双指针、栈和双向队列;
这一周收获还是不少的,不过还是有很多欠缺。在查看题解的过程中发现有好多解法都是基于数学概念之上,所以有时间还是要补补数学的。

成长

收获

  1. 时间复杂度的分析
  2. 基础知识的理解和应用
  3. 实际问题解决思路(升维,空间换时间)
  4. 五毒神掌
第一遍 第二遍 第三遍 第四遍 第五遍
仔细读题,快速思考解答,思考多种解决,重点背诵和默写 闭卷默写,比较多解法 一天后,重复做,了解不同解法的,并进行专项训练 一周后,进行一遍重复训练 面试前,进行一遍重复训练

欠缺

  1. 做题效率低
  2. 没有思路
  3. 对一些概念没有理解透彻

笔记

时间复杂度

名称 复杂度量级
常量阶 O(1)
对数阶 O(log n)
线性阶 O(n)
线性对数阶 O(n log n)
平方阶 O(n^2)
立方阶 O(n^3)
... ...
k方阶 O(n^k)
指数阶 O(2^n)
阶乘阶 O(n!)

时间复杂度

算法题的举例说明

LeetCode239 滑动窗口最大值

这道题,如果没有学算法我肯定会这样写

class Solution:
    def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
        return [] if not nums else [max(nums[i:k+i]) for i in range(len(nums)-k+1)]

虽然代码就一行,但是执行效率很差,时间复杂度是O(n*k) 这样写肯定是不好的,通过这一周所学的知识,就可以使用双向队列来优化,时间复杂度可以降到O(n)

from collections import deque
class Solution:
    def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
        n = len(nums)
        if n * k == 0:
            return []
        if k == 1:
            return nums
        
        def clean_deque(i):
            if deq and deq[0] == i - k:
                deq.popleft()
                
            while deq and nums[i] > nums[deq[-1]]:
                deq.pop()

        deq = deque()
        max_idx = 0
        for i in range(k):
            clean_deque(i)
            deq.append(i)
            if nums[i] > nums[max_idx]:
                max_idx = i
        output = [nums[max_idx]]

        for i in range(k, n):
            clean_deque(i)          
            deq.append(i)
            output.append(nums[deq[0]])
        return output

最后

通过算法的学习,不仅可以写出高效的代码,而且还能对问题有不同的思考方式,感觉算法是最训练思维的。为了更好的明天,努力、坚持到最后吧,将算法进行到底!

【155-week1 】学习总结

知识点该要

本周的知识点都是一些很基础的数据结构,数组,链表,跳表,栈,队列,双端队列,优先队列。

  • 数组

数组是很老生常谈的数据结构了,基本是大家所学的第一种数据结构。其特点就是支持 O(1) 的随机访问,但是由于其地址和下标的连贯性,所以当插入和删除元素时就需要挪动指定位置后面的所有元素,复杂度就上升到了 O(n)

关于数组的算法题,基本就是双指针法了,左右两个指针向中间夹逼。当然有时候需要对数组做一些额外的操作,比如在 3sum 这道题中就需要先对数组做一个排序。

  • 链表

与数组相反,链表是依靠节点中的指针来做前后节点的关联的,事实上相邻的两个节点在内存中的地址并不一定是相连的,因此当你想在链表中寻找指定的元素时只能从头开始遍历,一个一个比较,复杂度就是 O(n),但是一旦确定了元素的位置,插入和删除操作只需要移动前后两个节点的指针即可,所以复杂度是 O(1)

关于链表的算法题,感觉没什么技巧,但是有时候加上一个临时的头节点或尾节点(标兵)会比较方便处理问题,唯一要注意的就是操作指针的顺序,一旦顺序错乱就容易出现环,需格外注意。

针对有环的链表用快慢指针法来做也是极为巧妙。

  • 跳表

针对链表查询 O(n) 的时间复杂度,跳表做了升级。从一维链表中每隔一个节点就抽出一个节点来组成一个索引,当然如果索引太大,还可以针对索引再做二级索引...

故跳表的查询时间复杂度为 O(logn)

当然,因为有了索引的存在,当要插入和删除节点时还需要同时更新下索引。这就增加了复杂度。

可以把栈想象为一个桶,只能从上面放入和取出数据,(LIFO)后进先出。这种结构比较适合处理「最近相关性」的题目。浏览器的前进后退也都可以基于此实现。

基于数组或者链表都可以实现栈,但是由于栈只在队尾进行操作,所以大都是基于数组来实现,唯一的缺点就是扩从数组容量时的数据的拷贝花销。

  • 队列

与栈不同,队列时讲究公平性的,一端只可以入队,另一端只可以出队,可以把队列想象成为一个管道,数据在管道里面单向流动,(FIFO)先进先出。因为其支持在两端都为 O(1) 复杂度的操作,所以队列大都是基于链表实现的。当然也有基于数组的队列。

  • 双端队列

双端队列在队列的基础上做了完善,在队列两端都支持入队和出队操作。

  • 优先队列

优先队列类似于单向队列,唯一的区别就是其不再是 FIFO,而是基于元素的优先级来排序,优先级较高者先出队,所以就要求放入队列中的元素支持优先级大小的比较。

总结

刷题要多过遍数,同是要多看社区排名靠前的解法,师夷长技以制夷,将别人优秀的**学以致用。另外一个点就是要懂得空间换时间

改写代码 & 源码分析

  • 改写 Deque 代码
Deque<String> deque = new LinkedList<String>();
deque.addFirst("a"); 
deque.addFirst("b");
deque.addFirst("c");

System.out.println(deque);
String str = deque.peek();
System.out.println(str);
System.out.println(deque);

while (deque.size() > 0) {
		System.out.println(deque.pop()); 
}

System.out.println(deque);
  • Deque 源码分析

JDK 中关于 Deque 的实现可以基于两个维度来区分,线程安全与否和底层的数据结构,而且 JDK 的源码都可以做到望文生义,像 ArrayDequeLinkedListLinkedBlockingDeque 是常用的双端队列,看名字你大概也知道其功能了。

ArrayDeque 是基于数组实现的非阻塞的双端队列,因此会存在多线程安全问题。其底层的具体实现思路是维护一个数组和两个指针,头指针一直指向队列中的头元素,尾指针指向最后一个元素的下一个位置,起初两个指针均指向下标为 0 的位置,当向队列头部添加元素时,头部指针位置减一,如果结果小于 0,则置为最后一个位置,然后将元素放置在头指针的位置即可,也就是说从头部添加元素时是从数组的后面向前面添加的;当向队列末端添加元素时,直接将元素放到尾指针的位置,然后尾指针自增 1。当两个指针相遇时就说明需要扩充容量了。

而扩容的机制时直接创建一个新的数组,当原始数组容量小于 64 时,新数组长度为旧数组长度的 2 倍再加 2,否则新数组长度为旧树组长度的 1.5 倍。

而移除元素则相对简单,将对应位置的元素置空即可,然后移动下指针位置。

基于数组 + 双指针法来实现双端队列,也就是相当于将一维数组直接变成了循环数组,精妙至极。

LinkedList 是基于链表实现的非阻塞的双端队列,因此也存在多线程安全问题。感觉这个没什么难的地方,注意下操作元素时的指针顺序就可以了。LinkedBlockingDeque 则是在 LinkedList 的基础上又维护了一个可重入锁,当入队或者出队时必须先获得锁才可以操作,因此也就避免了线程安全问题。

  • PriorityQueue 源码分析

PriorityQueue 则是底层维护一个基于数组实现的小顶堆来实现的优先级队列。因为小顶堆的根结点比每个叶子结点都要大,所以出队时把根结点弹出即可,只是在添加和删除时需要更新小顶堆中的元素顺序,源码的实现方式是下沉法和上浮法。

添加元素时,该元素不断的和其父节点的元素做比较,直到找到一个比当前节点小的父亲节点即可。

而删除则是从根节点向下遍历,找到儿子节点中比较小的节点 X,然后对比节点 X 与最后一个节点 Y 的大小,如果 Y <= X,则直接将节点 Y 放置在根节点位置即可,否则就将节点 X 上浮至父节点,然后继续找节点 X 的最小子节点 X1,直到节点 Y <= Xn,则将节点 Y 放至 Xn 的父节点即可。

注意:既然优先级队列是按照大小来排序的,所以放入队列中的元素必须支持排序,如自然数或者字符串天生自带排序规则,如果是其他自定义的类型,则该类型需实现 Comparable 接口或者初始化队列时传入比较器。

Review 其他同学作业

545 号同学在总结总描述了自己近一周的进步和收获,同时提出了自己遇到的问题和担心,无独有偶,这些问题我也有,做过的题有时候过一天就忘记了,肯定是自己遍数过的不,然后就是解题思路理解不够深刻。同时该同学还在总结中把题目的多种解法都写出来并做了复杂度分析,点赞。坚持下去相信会有不少收获。

585 同学的作业是用 C++ 写的,应该是个大神,提交题目多余两道,是位懂得奋斗的同学,只有些地方的代码格式不是很友好,可进一步 format。

055 号同学的作业基本都是多种解法并存,逻辑清晰,代码格式工整,可读性好。

030 号同学提交了两道作业,不仅提供了代码,还提供了注释和自己的思考,对于看代码的人来说有很大帮助,这点要向他学习。

510 号同学作业很认真,一题多解,且格式工整。总结中阐述了自己的思考以及一些技巧,共勉。

【260_week01】总结

满心期待的算法训练营终于开课了,一直以来都想补习数据结构与算法,但学习起来总是遇难而退,对知识一知半解,也没有很好地老师,尽管网上的知识琳琅满目。我的数据结构仍然学的很差,在考研与工作的选择中,数据结构都是很重要的入门考试题,所以终于按下心思希望能够吃透吧。

第一周,学习了链表,数组,跳表的原理和实现。
数组是一种线性表数据结构,它是用一组连续的内存空间来存储数据。Int a[10];
数组简单易用,访问效率高,但大小固定,声明后就占用整块连续内存,如果数组过大可能造成内存不足,如果数组过小可能不够用。

链表通过指针将内存块串联使用。
data nextdata next data next data next data nextnull
链表有单链表,循环链表,双向链表,双向循环链表
第一个结点叫作头结点,通过它可以遍历得到整条链表。最后一个结点叫作尾节点,指向一个空地址NULL,表示这是链表上最后一个结点.
链表的插入和删除操作,只需要考虑相邻结点的指针改变,所以对应的时间复杂度是O(1).
跳表,其实我不是很理解,似乎是为了在链表中查找数据提高效率所建的。大概就是链表加多级索引的一种结构吧。
每两个结点会抽出一个结点作为上一级索引的结点,第k级索引的结点个数是第k-1级索引的结点个数的1/2,那第k级索引结点的个数就是n/(2^k)。
假设索引有h级,最高级的索引有2个结点。通过上面的公式,我们可以得到h=log2n-1。
觉得覃超老师讲的提速方法:升维**,空间换时间 很好。

栈(stack)——先进后出,删除与加入均在栈顶操作,也是一种线性表。
就跟步枪弹夹一样,先放进去后击发出来。
PUSH:在堆栈的顶部加入一 个元素。
POP:在堆栈顶部移去一个元素, 并将堆栈的大小减一。
队列(queue)——先进先出
队列也是一种特殊的线性表。但与栈相同,添加删除时间复杂度皆为o(1)。
双端队列(deque):两边都可以插入和删除

可能是基础太差了吧,leetcode上的题我刷起来并不容易。有时候看解析都看不明白。希望两个月的学习能更进一步。

【630-week 01】学习总结

不算是严格的计算机专业出身,之前学校里虽然学过算法的课程,但基本随用随扔,课后用的少,总感觉只是掌握了皮毛,所以想借此机会系统的梳理一遍,争取让算法成为自己的强项而非短板。像一些基本的数据结构也多少了解些,但并不能灵活运用,像Queue这种最多会用到LinkedList,PriorityQueue和其他的一些实现类基本不会用到,这也使得在平日工程的开发工程中没有那么得心应手。
听了这周的课,感觉知识熟悉又陌生,算法题也不是不会写,只是思路受限,还是练得太少了。平日下班回来会有点累,这周的课也是今天突击的,以后争取每天至少一个小时用在学习上,利用好这60天。

【560-week 01】学习总结

【总结】
通过老师的视频,学习了五步刷题方法(以前刷题只刷一边,往往很少思考)
开始的想法:学习一些数据结构,算法,更好的运用到工作中
问题:没有足够的练习、没有全面的思考,难以提升编码能力、工作时遇到问题难以结合数据结构进行开发。
改善:多写、多想、尽可能多的考虑题的解法(随着多写会提升)

数据结构蛮重要的,开发了才知道自己写的逗b代码。最终的目的还是训练思维能力,根据实际场景能不能结合数据结构进行可以开发。

菜鸡一个,坚持撸题

【关于做题】
1.暴力(很实在,没啥高深想法,就是硬着头皮求解)
2.合适的结构(比如三数之和,三个for我是没搞明白咋求,直接双指针解法,很愉快)
3.数学和高深思维(太菜、看答案才会)

【230-week1】学习总结

一,本周所学

  • 第一周,学习了数组、链表、跳表,以及栈和队列。

  • 另外,学习了移动零、盛水最多的容器、爬楼梯、3数之和、环形链表、有效的括号、最小栈这些算法题。

  • 通过看视频、动手、思考、写代码、看别人的代码等步骤,逐步学会算法的思维。其中,老师提到的升维和空间换时间**,还有双指针法等都是自己从未了解的,也学到了很多新的知识。

  • 另一方面,授之以渔不如授之以渔,这句话说的很恳切。

  • 以前自己对于算法是存在畏惧和不知如何去写的尴尬处境,可是跟着老师的讲解,再加上自己的实践,慢慢也习惯了,不再那么畏惧和不知如何去写。

  • 另外,不足之处就是没有好好安排自己的时间和进度,导致这周后面都在赶进度,做的不太好,给自己打个70分,其实自己可以做的更好,加油。

二、改写代码 & 源码分析

(1)改写代码

public static void main(String[] args) {
        Deque<String> deque = new LinkedList<>();
        deque.addFirst("a");
        deque.addFirst("b");
        deque.addFirst("c");
        System.out.println(deque);
        String str = deque.peekFirst();
        System.out.println(str);
        System.out.println(deque);
        while (deque.size() > 0) {
            System.out.println(deque.removeFirst());
            System.out.println(deque);
        }
}

(2) 源码分析

  • Queue源码分析:
  1. 它是一个接口,继承了collection接口(collection接口继承了Iterable几口),除了collection带有的操作,queue接口自己还提供额外的方法,这些方法有两种方式存在:(1)如果方法执行出错会抛出异常;(2)方法执行后返回一个特定值true或者false。

  2. 不管队列是何种顺序放置元素,当调用remove和poll方法时,队列的head都会被移除。在FIFO队列中,插入元素是将元素插入在队列的tail。

  3. Offer方法与collection的add方法不同,当数组容积不够放入元素时,offer会返回false。

  4. 当queue is empty时,remove会抛出异常,poll会返回null。

  5. Peek或者element方法,仅仅返回queue的head元素,但不会remove。

  6. 不推荐将null插入到队列中,虽然queue的实现LinkedList没有不允许,但是null是poll方法在对空队列操作时的返回值,表示这个队列是空的,所以不推荐。

  • PriorityQueue源码分析:
  1. PriorityQueue继承了AbstractQueue,而AbstractQueue实现了Queue。优先队列是基于priority heap实现的,队列中的元素按照comparator的顺序来排列的,不允许null值;而且不允许插入non-comparable对象,否则会导致ClassCastException。

  2. Poll、remove、peek、element方法都会得到head元素。

  3. 当元素插入队列时,队列的capacity会自增长。

  4. 这个队列是非线程安全的,对应的线程安全队列实现是PriorityBlockQueue。

  5. Enqueue和dequeue方法,时间复杂度都是O(log(n)),比如offer、poll、remove、add;而remove和contain都是O(n);peek、element、size都是O(1)。

  6. add方法其实调用了offer方法。首先,offer会判空;如果容量不够会进行扩容,扩容两倍或者1.5倍,并复制到新数组中;

  7. peek方法,如果队列的size为0,则返回null。

  8. remove:如果没有该元素,返回false;否则删除并返回true。

  9. poll:如果size为0,返回null;否则就返回结果。

三、Review

  1. 480号同学,在旋转数组这题,做的很好,代码很简洁,思路是将多余的部分放到新的数组,再用这个新数组替换原来数组部分数值,达到题目要求。代码也写了一些注释,值得学习

  2. 430号同学,添加注释的习惯很棒,代码风格也很不错,思路很清晰。

  3. 445号同学,代码基本没有注释,缺乏可读性;还有代码风格也不是很好。但是,能举出多种解法也是难能可贵,这点做的很好。

  4. 580号同学,代码风格清晰,值得细读;可惜缺少一些注释。思路也不错,代码写的还是不错的。如果多做几道题,那就更好了。

  5. 40号同学,可以看出这位同学还是很努力的,而且也写了一些注释,写出来具体的实现逻辑,具有可读性。这点做的很好。

【510-Week 01】学习总结

1:这周主要了解一些**
重复项,重复项,重复项(说三遍重要!!!)

2:解题的时候先用 暴力解法了解题意以及规律

3:然后尝试 左右指针碰撞

4:再试试 递归是否可以实现
递归需要注意2点
4.1最近重复项
4.2 归纳法

5:降维处理 (目前使用不熟悉,刚刚接触这种**,需要多接触类似题目)

6:空间换时间(没有限制的情况 可以借助其他数据结构处理问题)

Review

算法训练营(第4期)第一周作业

要求

  1. 每周从覃超老师布置的题目中,至少完成并提交两道算法题
  2. 围绕每周重点学习的算法知识点,撰写一篇有观点和思考的技术文章或总结,切忌流水账。

作业提交 Deadline

2019年10月20日 23:59 (以当地时间为准)
未按时提交作业,会在个人作业总分钟 -3 分

本周作业概述

本周需要完成学习的视频内容:

  • 第 3 课 | 数组、链表、跳表
  • 第 4 课 | 栈、队列、优先队列、双端队列

以上两课视频后,覃超老师都给出了大家可以练手的算法题。

其中,第 4 课的课后作业(第2节视频后)还包括:

  • 用add first或add last这套新的API改写Deque的代码
  • 分析Queue和Priority Queue的源码

请大家将此部分作业,放在本周学习总结中一并提交

本周算法习题库:

第三课课后习题:

https://leetcode-cn.com/problems/remove-duplicates-from-sorted-array/
https://leetcode-cn.com/problems/rotate-array/
https://leetcode-cn.com/problems/merge-two-sorted-lists/
https://leetcode-cn.com/problems/merge-sorted-array/
https://leetcode-cn.com/problems/two-sum/
https://leetcode-cn.com/problems/move-zeroes/
https://leetcode-cn.com/problems/plus-one/

第四课课后习题:

https://leetcode.com/problems/design-circular-deque
https://leetcode.com/problems/trapping-rain-water/
用add first或add last这套新的API改写Deque的代码
分析Queue和Priority Queue的源码

改写代码和分析源码这两项作业,需要在你的本周学习总结中一并提交。

【700-week1】 第一周学习心得

  1. 我学到了什么知识?
    (1)理论知识及代码实现数据结构方面:
    课程的学习中,学习了解数组、链表、队列、栈等四种数据结构及他们的时间空间复杂度的分析,学习怎么用Python来实现这四种数据结构及相应的接口等。学会怎么去查询相关的源代码文档及相关信息,逼迫自己去看去学习源代码。
    存在的问题:运用Python实现这些数据结构的熟练程度依然不够,尤其在链表的部分,链表的反转,循环链表等常常会发生指针丢失的情况,仍然需要进一步的练习。
    对照自己原先绘制的数据结构脑图,在受限线性表中,越界错误的知识点及静态链表的知识点,仍然没有掌握。
    在学习的过程中,Python的数组array跟numpy的array一直在困扰着我,疑惑我们写算法的时候,能直接调用第三方库numpy的array吗?以及array跟numpy的array存在什么差异,运用的时候选择哪个为优,这是我后续还要探讨的问题。
    (2)算法题知识点总结:
    a. 双指针法,算法题26删除重复数值及283删除零,我认为是两道类似的题,可以纳入到一类中,他们用一个指针帮助记录目标数值的下标,用另一个指针下标来遍历数组,使用额外的空间nums[i]来记录下一个要比较的值,额外空间为O(1)。我在这道题中常犯的错误是:常常不会借助额外的空间nums[i]记录比较数值,单纯的运用下标nums[j+1]与nums[j]对比,进而导致j循环中,无法解决指针溢出的问题
    b. 链表的算法题,常常需要设置一个“哨兵“指针,或前置指针,我在链表的指针中常常迷失自我。在链表题中,一步一步画图,很关键,帮助梳理自己的思路。链表的题,死命的写,死命的重复就对了。在链表中,自己容易出现的错误是:把链表当数组,遍历链表一直要写成数组下标遍历的形式。
    c. 栈的应用可以用来解决类似最近相关问题,类似“剥洋葱“的结构。左右括号匹配的问题,可以用字典结构来解决。
    d. 队列,时间关系,这周队列的学习还没有完全完成。

  2. 算法题实战心得
    (1)需要提升自己写代码能力
    在算法题的实战中,发现除了暴力解法,大部分情况下自己是想不到更优化的解法的。在暴力法求解的过程中,自己编程能力弱的缺点也一直一直在曝露出来。尤其在写遍历循环的时候,很容易出现下标溢出的问题。
    (2)理解后再去背及仿写代码并且不断重复不断重复才能真的学会
    对于算法题,现阶段只能借助题解及老师的讲解,先学习其他人的代码,模仿着写,背诵默写,让自己达到熟悉相关题型的程度。实际写题目的时候,发现背代码,理解不透彻的话,在自己做题的时候,所有的不理解的问题就都会暴露出来了。因此,每一次的重复,都是在发现自己的问题所在,是检验自己是不是真的理解,是不是真的掌握的那类题型。
    (3)画图是有效的学习手段
    题目理解的过程中,需要不断的画图,画图,且在画图的过程中,不要太相信自己,不要轻易的跳步。跳步的话就很容易出现下图的悲催情况…

  3. 总结
    这一周的学习,痛苦着,痛苦着,痛苦着快乐着。
    (1)让我了解到自己的学习边界,清楚自己的基础有多差,进而重新来制定自己的学习目标,希望在学习结束的时候一方面能够让自己的编程能力有长足的进步,一方面让自己在数据结构与算法的学习上达到掌握老师所讲过的题目,能做出大部分的课后作业题的目标,后续还要继续锻炼与提升。
    (2)这一周另一个主要的问题在于低估了算法学习的时间投入需求,没有合理的每天分配足够的时间,前两天的时间投入不够,导致后续的学习只能勉勉强强跟上,算法题的遍数过不到五遍,甚至有的连第一遍都没能完全走完。在第二周的时候,需要再增加时间的投入,计划每天固定出2小时左右进行学习。接下来在小组内进行每日学习打卡吧~
    (3)不要害怕重复,不要害怕重复,不要害怕重复。在算法题中,哪怕最简单的moveZero等题目,我在周三周四的时候觉得自己已经学会了,已经会写了,等到周六正式写作业的时候,又狠狠的打击了自己。
    (4)需要对每一道算法题写总结,归纳相关的题型,相似的解法。

review 同学的代码或学习心得
155号同学学习心得
将学习的知识点进行概括总结,加入自己在学习中遇到的问题,内容简洁明了,值得学习。
040 同学代码
非科班出身,学习算法的时候,运用三种不同的编程语言做题,功力扎实。review了他的Python的代码,觉得他Python代码写的很简洁,尤其是moveZero 那段代码,运用enumerate(),简化代码。
525 同学学习心得
在学习总结中,对快慢指针的运用讲的很清楚,Python实现堆的代码很清晰。
295号同学学习心得
学习心得中分享了自己的做题技巧,值得借鉴。双指针解法的总结也写得很棒,将其归纳为快慢指针跟对撞指针,并分析在不同场景下的两种指针的应用情况。
总结:大家都在努力的学习着,不管是大佬或小白!看大家的学习心得及代码,真的超级有用。小白的我,只会Python,看其他语言的代码..吃力

【525-Week01】第一周学习总结

一、分析算法题:

1.了快慢双指针法,快指针先行,慢指针根据不同的条件向前走。

移动零:判断两个指针指向的位置,如果快指针指向一个非零元素,将值赋值给慢指针指向的位置,之后更新慢指针的位置。其中还要判断快慢指针是否重叠,“否”的话将快指针指向的位置元素置为0。
删除数组中的重复项:也是一个快慢指针,判断快慢指针指向的位置元素是否不同,如果相同慢指针不变,快指针继续往下走,否则先更新慢指针的位置,将慢指针指向的新位置赋值为快指针指向的位置的元素。
三数之和:首先将数组进行一次排序。然后开始进行for循环,每个循环位置记为“i”,使用两个指针一个为left=i+1,另一个从头的位置,即为right=nums.length-1。然后在开启一个内层循环,两个指针从两侧往中间进行靠拢,每次计算nums[i]+nums[left]+nums[right]的值,为s。当s小于0的时候,说明left位置的元素过小,将left位置进行加一更新。如果大于0,说明right的位置元素过大,将right位置进行减一更新。如果相等则将一组nums[i],nums[left],nums[right]加入到结果中,并同时更新left和right的位置。这里需要注意,加入到结果中时,需要去除重复元素。

2.求模运算:

旋转数组:所谓旋转k个元素,其实就是旋转k%n个元素,将原数组后面的k%n个元素放到前面,也就是n-k%n个前面元素之前。先将数组进行翻转,然后在翻转前k%n个,之后将剩余的n-k%n个元素进行翻转。这道题充分的利用了求模运算。

二、数据结构设计

1.设计一个双端队列:

使用双向链表来实现一个双端队列,实现了从头,尾两端插入删除。这里面需要注意的是,插入和删除的时候,需要进行一个前后节点的指针的更新,容易出错。代码具体实现见 LeetCode_641_525.py。

2.分析优先级队列

优先级队列,也就是堆,是一种二叉树数据结构。因为堆是一个完全二叉树,所以可以使用一个数组来进行实现。

python源码分析:

是用数组来实现的以index 0为起点。计算父亲节点的公式为:index-1/2 ,左孩子:index2 + 1,右孩子:index2+2。

接口:

heappush:向堆中加入一个元素。在结尾添加,并进行父亲节点的下浮操作。
heappop:取出堆顶元素。首先将堆尾元素与堆顶元素进行交换,然后删除并返回新的堆尾元素。之后将进行siftup操作。siftup里面还包括了sitdown操作。将孩子节点进行上浮,将父亲节点进行下沉操作。
heapify:将从数组一半的位置开始,进行sifup操作。
heapreplace:删除并返回堆顶元素,并向堆中添加一个元素。这个操作类似于删除,只不过在删除的时候向其中在添加一个元素
学习编程我认为最重要的是自己动手实现,所以我实现了一个堆,代码如下:
import random


class MaxHeap(object):
    def __init__(self):
        self.array = []

    def __len__(self):
        return len(self.array)

    def __getitem__(self, index):
        return self.array[index]

    def is_empty(self):
        return len(self) == 0

    def left(self, index):
        return index * 2 + 1

    def right(self, index):
        return index * 2 + 2

    def parent(self, index):
        if index < 1:
            raise IndexError
        return (index - 1) // 2

    def add(self, e):
        self.array.append(e)
        self.sift_up(len(self)-1)

    def peek(self):
        return self.array[0]

    def sift_down(self, index):
        '''
        下沉
        :param index:
        :return:
        '''
        while self.left(index) < len(self):
            j = self.left(index)
            if j + 1 < len(self) and self.array[j+1] > self.array[j]:
                j += 1
            if self.array[index] > self.array[j]:
                break
            else:
                self.swap(index, j)
                index = j

    def sift_up(self, index):
        '''
        上浮
        :param index:
        :return:
        '''
        while index > 0 and self.array[self.parent(index)] < self.array[index]:
            parent = self.parent(index)
            self.swap(index, parent)
            index = parent

    def swap(self, a, b):
        if a < 0 or a >= len(self) or b < 0 or b >= len(self):
            raise IndexError
        self.array[a], self.array[b] = self.array[b], self.array[a]

    def extract_max(self):
        '''
        删除堆顶元素并返回
        为了保持完全二叉树的特性,删除时将堆顶元素与堆底元素进行交换。
        然后对新的堆顶元素进行下沉操作,删除堆底元素。
        :return:
        '''
        max_value = self.peek()
        self.swap(0, len(self)-1)
        self.array.pop()
        self.sift_down(0)
        return max_value

    def heapify(self, array):
        '''
        从第一个非叶子节点进行,依次往前遍历对每一个节点进行下沉操作。
        :param array:
        :return:
        '''
        if not array: return array
        self.array = array[:]
        index = self.parent(len(self)-1)
        while index >= 0:
            self.sift_down(index)
            index -= 1

    def replace(self, e):
        '''
        替换堆顶元素
        :param e:
        :return:
        '''
        max_value = self.array[0]
        self.array[0] = e
        self.sift_down(0)
        return max_value


if __name__ == '__main__':

    def test(n):

        heap = MaxHeap()
        a = []
        for i in range(n):
            heap.add(random.randint(0, n))
        for i in range(n):
            a.append(heap.extract_max())
        for i in range(1, len(a)):
            assert a[i - 1] >= a[i], '结果错误'
        print('test success')


    def test_heap(n):

        array = []
        for i in range(n):
            array.append(random.randint(0, n))
        heap = MaxHeap()
        heap.heapify(array)
        a = []
        for i in range(n):
            a.append(heap.extract_max())
        for i in range(1, len(a)):
            assert a[i - 1] >= a[i], '结果错误'
        print('test heapify success')

    test(100)
    test_heap(100)

三、作业点评

1.295同学的打码很整洁,并且提供了两种语言的实现,这一点和不错,站在不同的语言的角度去思考。
2.150同学不仅给出了代码实现,还将自己的思路写上去了,这一点总结的非常好,应该向150同学学习,我也应该写一写自己的思路。
3.115同学的代码不光给出了代码实现,还给出了时间复杂度的分析。

【470-Week01】学习总结

本周主要学习数组,链表,栈和队列。使用“五毒神掌”进行刷题。
分析问题时需要考虑时间复杂度和空间复杂度。

数组:
增加 O(n)
删除 O(n)
查找 O(1) * 随机查找

链表
增加 O(1)
删除 O(1)
查找 O(n)

跳表:
增删改查 O(logn)

栈和队列
栈:先进后出 插入删除 O(1)查找 O(n)
队列:先进先出 插入删除 O(1)查找 O(n)

Review:
Code相关:
1,https://github.com/algorithm004-05/algorithm004-05/pull/158/files#r336778784
2,#132 (review)

分享相关:
1,#209 记录的信息很完善,总结的很到位
2,#129 图示说明复杂度问题
3,#221 和自己有同样状态的学友,需要共勉加油

【100-Week 01】学习总结及源码分析

Week01学习总结

关于学习方法的一些思考

算法和数据结构一直是我难以越过的一道坎,之前研究过总是半途而废,买了很多厚厚的书都读不下去,难以坚持。经过覃超老师的讲课,我大致明白了,是我学习方法的问题。

碰见难点我经常死磕,磕不出来就放弃了,并且陷入自我怀疑中,实际上不必这样,看答案也是一种学习,借鉴别人的思路,理解大佬的想法,融进自己的**里,我觉得这是一种事半功倍的学习方法(当然,前提是自己实在捣鼓不出来),就像牛顿说的,站在巨人的肩膀上。我们创新的一切,也都是学习前人的经验,加上自己的想法完成的,人类的发展都是如此,算法题那就更简单了,套路总共也就那么点儿。

这周总的来说,我是收获颇多的,之前一些不敢面对的问题,现在也是从容面对,之前一些死磕没出来的题目,通过借鉴别人的想法,我也是有了自己的理解。

课后作业

1 Deque 改写

Deque<String> deque = new LinkedList<>();
deque.addFirst("a"); 
deque.addFirst("b");
deque.addFirst("c");
System.out.println(deque);

String str = deque.getFirst();
System.out.println(str);
System.out.println(deque);

while (deque.size() > 0) {
  System.out.println(deque.removeFirst()); 
}
System.out.println(deque);

2 Java Queue以及PriorityQueue源码分析

2.1 Queue分析

Queue是Java定义的一个队列接口,其接口继承了Collection接口,Collection定义了对数据进行遍历,为空判断等一些方法,这里不展开细说。仅仅看Queue接口定义的如下接口

Throws Exception return sepical val
insert add(e) offer()
Remove remove() poll()
Examine element() peek()

Java实现了面向接口编程的规范,让接口更加动态,多态方式指定实现类。其实现类大致分类三种类型,一种是基础的实现,一种是支持并发操作的实现,还有一种是阻塞的实现。

  • 支持并发操作的实现类有ConcurrentLinkedQueueConcurrentLinkedDeque其内部使用CAS操作结点,锁粒度很小
  • 支持阻塞的队列是利用Condition接口以及Lock接口实现等待通知机制来实现的生产者-消费者模式

2.2 PriorityQueue源码分析

优先队列保证每次从队列中获取的元素都是队列中权重最大(或者最小)的元素

2.2.1 分析及自己实现

在进行源码分析前,我们先来看看如果让我们实现,优先队列,该如何实现?

如果底层使用数组来实现,并保证其数组元素绝对有序,我们就需要在插入元素时,就需要将其放置在正确的位置上,如下边伪代码

//定义元素权重大者优先获取
//定义元素E实现Comparable<E>接口,如若a.compareTo(b) > 0,定义a的权重更大
void offer (E val) {
  if (size >= capacity) {
    resize(queue);//扩容操作
  }
  int insertIndex = 0;
  for (int i = 0; i < size-1; i++) {
    if (val.compareTo(queue[i]) <= 0) {
      insertIndex = i;
      break;
    }
  }
  for(int i = size; i > insertIndex; i--) {
    queue[i] = queue[i-1];
  }
  queue[insertIndex] = val;
  size++;
}

所以我们插入元素的时间复杂度是O(N)的.删除权重最大的元素的代码可表示为return queue[--size];,其时间复杂度为O(1)

自定义实现的优先队列插入元素时时间复杂度是线性的,是因为我们保证了数组中元素的绝对有序性,当然,这也使得删除元素也大大提高了性能。如果我们可以将其两个操作的效率均摊一下,或者说我们不需要保证其内部数组的绝对有序性,看看是否能优化插入元素操作?

2.2.2 Java8中的实现

我们抛开Java的继承体系不谈,直接看其内部定义的属性

public class PriorityQueue<E> extends AbstractQueue<E>
    implements java.io.Serializable {

    private static final long serialVersionUID = -7720805057305804111L;
		//default capactiy
    private static final int DEFAULT_INITIAL_CAPACITY = 11;
		//内部数组
    transient Object[] queue; // non-private to simplify nested class access

    private int size = 0;
		//比较器,创建对象时可传入,否则使用泛型元素的实现的compareTo方法
    private final Comparator<? super E> comparator;
		//操作次数
    transient int modCount = 0; // non-private to simplify nested class access
    
    // omit code...
}
2.2.2.1 offer方法

其插入元素的方法忽略细节,大致有两步操作,①若是容量不够,进行扩容操作;②进行实际的插入操作

public boolean offer(E e) {
    if (e == null)
      throw new NullPointerException();
    modCount++;
    int i = size;
    if (i >= queue.length)
      grow(i + 1);//内部数组扩容
    size = i + 1;
    if (i == 0)
      queue[0] = e;
    else
      siftUp(i, e);//上浮操作
  	return true;
}

插入操作,在数组下标等于0的时候直接赋值,而之后会执行siftUp操作,其做了什么操作,是不是如此做会大大提高效率?

private void siftUp(int k, E x) {
    if (comparator != null)//是否用自定义的比较器
      siftUpUsingComparator(k, x);
    else
      siftUpComparable(k, x);
}

//此处就拿默认的比较器分析
private void siftUpComparable(int k, E x) {
    Comparable<? super E> key = (Comparable<? super E>) x;
    while (k > 0) {
      int parent = (k - 1) >>> 1;//无符号右移一位,获取父元素下标
      Object e = queue[parent]; //父元素
      //如果当前和父元素比较权重大些的话,退出
      if (key.compareTo((E) e) >= 0) 
        break;
      //否则父元素覆盖之
      queue[k] = e;
      k = parent;
    }
  	//k为上述迭代后的最终位置
    queue[k] = key;
}

我们将上述过程,我们可知,除了下标为0的元素,每个元素的父元素下标为(k - 1) >>> 1,针对数组[1, 3, 4, 7, 8, 6, 5],6和5的父元素是4;8和7的父元素是3;4和3的父元素为1,可建立模型:

heap

由此可知Java中利用二叉堆来实现优先队列。

二叉堆是一种特殊的堆,二叉堆是完全二元树(二叉树)或者是近似完全二元树(二叉树)。二叉堆有两种:最大堆最小堆。最大堆:父结点的键值总是大于或等于任何一个子节点的键值;最小堆:父结点的键值总是小于或等于任何一个子节点的键值。---百度百科

Java中的实现采用最小堆来实现,我们在插入元素的时候,不可避免的会将节点之间的关系打破,为了保证其有序化,我们需要对其进行上浮操作,使得维持堆的定义结构(父结点元素<关系>子节点元素);所谓上浮操作,就是如果插入的节点不符合关系,则与父结点进行交换(Java中是一直拿着当前元素去比较,父元素占用当前元素索引),轮询向上比较,直至满足条件或者到达堆顶为止。

假设针对上述数组,我们进行插入元素2操作,如下为上浮过程:

  1. 其父元素7大于索引为k元素2,对其进行上浮,元素7占用当前k位
  2. 紧接者与父元素的父元素比较,还未维持平衡,继续上浮,元素3占用当前k位
  3. 与堆顶元素比较,已经维持了平衡,当前元素占领当前k位

整个过程都是对数级的,所以时间复杂度为O(logN)的.

2.2.2.2 poll方法

上述的offer方法分析实际上已经为poll方法的分析奠定了基础,总结来说,offer操作为了保证数组的堆结构,进行了上浮操作,那么poll取走堆顶元素后,仍要保证其堆结构的平衡,通常的做法是,从数组末尾取元素放入堆顶,让其进行下沉,其在堆顶落下的过程中,会保证堆的结构平衡,这个过程称为下沉操作

private void siftDownComparable(int k, E x) {
    Comparable<? super E> key = (Comparable<? super E>)x;
    int half = size >>> 1;        // loop while a non-leaf
    while (k < half) {
      int child = (k << 1) + 1; // assume left child is least
      Object c = queue[child];
      int right = child + 1; //right 结点
      if (right < size &&
          ((Comparable<? super E>) c).compareTo((E) queue[right]) > 0)
        c = queue[child = right]; 
      if (key.compareTo((E) c) <= 0)
        break;
      queue[k] = c; //下沉
      k = child;
    }
    queue[k] = key;//找到了下沉元素的最终位置
}

其时间复杂度也是O(logN)

2.2.2.3 peek方法与remove(Object o)方法

因为堆顶元素始终是优先级较高的元素,所以peek的时间复杂度为O(1)

remove(Object o)需要根据元素确定索引确定元素位置,时间复杂度为O(N),之后需要保证堆结构平衡,需要上浮或者是下沉,时间复杂度为O(logN),取其最大的时间复杂度,所以remove(Object o)方法时间复杂度为O(N)

2.2.2.3 总结

关于Java中优先队列的实现,最重要的两个接口就是offer和poll,其内部使用最小堆来实现的,offer方法和poll方法为了保持平衡,会对整个堆进行上浮或者下沉操作。

Review

  • 005同学的PHP溜的一批,我想学这门最伟大的语言
  • 010同学链表题没有做到善用引用,导致时间复杂度很高
  • 060同学一题多解,代码思路都很清晰,但缺少复杂度分析
  • 545同学代码看着很舒服,多种解法,都提供了思路,值得学习
  • 585同学代码很清晰,值得学习

645-week 01

平时加班比较严重,本来是打算周末集中起来完成作业,结果发现作业量比我想象的要多,按照覃超老师的步骤一道题目至少得花费40分钟的时间去举一反三,后面的课程得想想办法。
课程内容,视频课程听起来还是很轻松的,主要是自己动手练习的过程比想象的要枯燥,与各位共勉。

【275 week预习周】结合自己开发经验对数据结构和算法知识的理解

  coding了五年,一直觉得没有很大的提升,总感觉和其他一两年的人也没有什么区别。这可能就是老师说的,还没有成为顶尖水平,随着时间的飞逝,连追求顶尖的心都没有了。
  工作中很多时候都会遇上问题,刚开始百度,后来学会了google,再后来学会了翻看源码,看实现,找原因。慢慢的发现其中的一点点门道。但是源码看的越多,就发现自己的缺陷越来越明显,很多代码看不懂,debug进去还不是很懂,再google之后才知道最终这一行代码代表什么意思。我总结起来很多情况下都是自己的基础不好,对算法和数据结构的不了解导致的。
  在这个框架横行的开发时代,很多时候都是被业务推着走,跑通就好的**一直充斥着我们的工作,慢慢的只知道框架提供的东西,而忘记了框架的本质:基础知识!这其中就包含了很重要的的数据结构和算法。我们都是用封装好的东西,长此以往就会慢慢成了框架使用者,这就导致了相对于其他人,自己没有任何的不可替代性,可能这就是很多程序员迷茫的原因,包括我自己!找不到自己的优势!
 唯有学好基础才是王道,万变不离其宗。纵观那些主流的技术和流行的框架和解决方案,精华部分往往都是一些很厉害的算法。听了老师的预习课程,感觉这门课还是非常的接地气的,让我对以后的code增加了信心,相信学了之后自己能和别人有所区别,解决问题的时候能得心应手些!加油!最美好的明天!最厉害的自己!

【200-Week1】第一周学习总结

关于收获
1,感觉老师强烈建议自己做数据结构与算法的脑图,很有必要,这样才总领整个算法知识体系。
2,通过视频的学习,我知道了前序,中序,后序二叉树的时间复杂度,图的时间复杂度为O(n)。
3,老师在讲解数组,栈,队列等数据结构时候,不仅讲到基本知识的使用,还深入源代码讲解,我觉得这种方式很好。
关于刷题
刷题是最重要的,五毒神掌很有用,其实不止五遍,十遍以上才对基础很差的我有用。多看优秀的代码,尤其要去国际站看世界顶级高手的代码。
做题过程中的体会:
移动零,解法很巧妙,要是我是想不到的,这种解法值得学习。
3数之和,代码很整洁又严谨
双指针的**,两边夹逼的方法,必须掌握
空间换时间,有时候可以考虑
Code Review
390号同学,代码简洁,注释也明了
380号同学,多种解法,很赞
010号同学,用JS来解答,很认真,让我学到很多
040号同学,用多种语言,逻辑清晰
125号同学,时间,空间复杂度都有,不错

【710-week1】第一周学习总结

  1. 知识点总结
    数组: 访问[基于索引]-O(1); 添加新元素至开头-O(n); 添加新元素至结尾-O(n); 查找(通过值而非索引)-O(n); 删除-O(n)
    链表: 添加操作-O(); 删除-O(n); 查找-O(n); 修改-O(n)
    跳表: 插入-O(logn); 删除-O(n)
    栈: 类似水杯, 先进后出; 插入/删除-O(1); 查找-O(n)
    队列: 类似水管, 先进先出; 插入/删除-O(1); 查找-O(n)

难点: 时间复杂度为O(logn)的推算逻辑比较难理解, 需要多复习

2.Review 与点评
280同学 使用了自顶向下的编程方法; 但是,代码注释较少, 对于我这种编程小白,代码逻辑不好理解
755同学 使用了python的内置函数, 值得我学习; 我都是自己写代码,实现一些逻辑, 效率很低
125同学 使用多种方法求解, 编码功力深厚.并且给出了每种方法的时间复杂度和空间复杂度, 很赞
295同学 使用java和python两种语言, 代码也很简洁. 值得学习
570同学 21题, 第8行, i==0的判断是否多余? 因为, i的起始值就是0; 如果不希望为0, 是不是可以直接从1开始

算法训练营(第4期)第一周作业

要求

  1. 每周从覃超老师布置的题目中,至少完成并提交两道算法题
  2. 围绕每周重点学习的算法知识点,撰写一篇有观点和思考的技术文章或总结,切忌流水账。

作业提交 Deadline

2019年10月20日 23:59 (以当地时间为准)
未按时提交作业,会在个人作业总分钟 -3 分

本周作业概述

本周需要完成学习的视频内容:

  • 第 3 课 | 数组、链表、跳表
  • 第 4 课 | 栈、队列、优先队列、双端队列

以上两课视频后,覃超老师都给出了大家可以练手的算法题。

其中,第 4 课的课后作业(第2节视频后)还包括:

  • 用add first或add last这套新的API改写Deque的代码
  • 分析Queue和Priority Queue的源码

请大家将此部分作业,放在本周学习总结中一并提交

本周算法习题库:

第三课课后习题:
https://leetcode-cn.com/problems/remove-duplicates-from-sorted-array/
https://leetcode-cn.com/problems/rotate-array/
https://leetcode-cn.com/problems/merge-two-sorted-lists/
https://leetcode-cn.com/problems/merge-sorted-array/
https://leetcode-cn.com/problems/two-sum/
https://leetcode-cn.com/problems/move-zeroes/
https://leetcode-cn.com/problems/plus-one/

第四课课后习题:
https://leetcode.com/problems/design-circular-deque
https://leetcode.com/problems/trapping-rain-water/
用add first或add last这套新的API改写Deque的代码
分析Queue和Priority Queue的源码

改写代码和分析源码这两项作业,需要在你的本周学习总结中一并提交。

【680-Week01】学习总结

本周在周一的时候对视频就看的差不多了,感谢老师的讲解。最近一直对算法的学习感到很迷茫,总是找不方向,看到同学的作业与本周的总结都很到位。笔记记录的也很清晰。想各位同学学习了!
在本周里主要对数组/链表的底层原理的学习,通过算法题加深了对底层原理的理解。大多数都是看题解模写出来的,对有些题目还是不能很好的理解。需要在通过以后的学习能让我更快的成长吧

【150+week1】第一周学习总结

第一周总结

匆忙的一周,突然感觉到了时间无比宝贵.经历了一周忙碌加班和忙碌中去抽时间学算法,做算法题.睡前养成了去回想算法都用在哪里的习惯.收获甚大.
再来说说学习,第一周算是打开了算法的大门.从最基础的数组和链表学起.自己也重新又仔细看了遍ArrayList和LinkedList. 反复的实现了几遍相关的算法题.虽然没有做到无毒神掌(挤时间能力有待提高).但也是有所收获.从数组和链表的学习中,发现越基础越重要.复杂的算法也好,复杂的问题也好.越是刻意细分到基础的解法上面的,重复的做着基础的事儿. 学的基础越扎实,思路也就能越好的转换,相信遇到复杂问题也会更有思路.

总之,第一周.差强人意.如果给自己评分的话,及格线上一点点吧. 欣慰的是还有提升空间.下周继续努力.

下面是优先队列总结,由于时间仓促,没有截图.没有贴进帖子.
// TODO: 回头将下面总结完善,贴入帖子.

Queue

在jdk中,队列是以接口形式定义的, 不同的类去实现Queue接口,来对队列做不同实现.这也充分的体现出java的面向对象的多态特性.

Queue接口中定义了如下方法:

boolean add(E e);
E element();
boolean offer(E e);
E peek();
E poll();
E remove();

以上方法构成了队列的基础实现.我们常见队列的核心也就是以上的方法,只不过做了不同的实现.

PriorityQueue

PriorityQueue: 优先级队列, 是0个或多个元素的集合.集合中每个元素都有一个权重值.每次都弹出优先级最高(或者最低)的元素.

一般来说, 优先队列使用堆来实现. PriorityQueue也是堆来实现的.

主要属性:

// 默认容量
private static final int DEFAULT_INITIAL_CAPACITY = 11;
// 存储元素的数组
transient Object[] queue; // non-private to simplify nested class access
// 元素个数
private int size = 0;
// 比较器
private final Comparator<? super E> comparator;
// 修改次数
transient int modCount = 0; // non-private to simplify nested class access
  1. 默认容量是11.
  2. queue:元素存储在数组中, 和我们了解到的堆一般使用数组来实现的是一致的.
  3. compatator:比较器,用于对存储在优先队列中的元素进行比较.
  4. modCount: 修改次数, 有这个属性表示PriorityQueue也是fast-fail的.

入队:

public boolean add(E e) {
  return offer(e);
}

public boolean offer(E e) {
  if (e == null)
    throw new NullPointerException();
  modCount++;
  int i = size;
  if (i >= queue.length)
    grow(i + 1);
  size = i + 1;
  if (i == 0)
    queue[0] = e;
  else
    siftUp(i, e);
  return true;
}

priorityQueue提供了两个入队方法,addoffer, 从源码可以看出二者是一致的. add也是调用的offer.并且看源码中,队列是不允许加入null的.

grow方法如果看过arrayList原码的可能都会熟悉,扩容.

扩容机制:

当元素个数低于64时,每次翻倍.

当元素个数大于64时,每次增加原来的一半.

siftUp就是对元素堆化的过程.具体实现大致就是一个自下而上的堆化过程,一直往上跟父节点比较,如果比父节点小,就与腹肌诶单交换位置,知道出现比父节点大位置.由此可见,PriorityQueue是一个小顶堆.

出队:

public boolean remove(Object o) {
  int i = indexOf(o);
  if (i == -1)
    return false;
  else {
    removeAt(i);
    return true;
  }
}

private E removeAt(int i) {
  // assert i >= 0 && i < size;
  modCount++;
  int s = --size;
  if (s == i) // removed last element
    queue[i] = null;
  else {
    E moved = (E) queue[s];
    queue[s] = null;
    siftDown(i, moved);
    if (queue[i] == moved) {
      siftUp(i, moved);
      if (queue[i] != moved)
        return moved;
    }
  }
  return null;
}

public E poll() {
  if (size == 0)
    return null;
  int s = --size;
  modCount++;
  E result = (E) queue[0];
  E x = (E) queue[s];
  queue[s] = null;
  if (s != 0)
    siftDown(0, x);
  return result;
}

出队提供了三个方法. remove,removeAtpoll .出队的逻辑:

  1. 将队首元素弹出
  2. 将队列末元素移动到队列首.
  3. 自上而下堆化,一致往下与最小的子节点比较, 如果比最小的节点大,就交换位置,继续比较.
  4. 如果比最小的子节点小,就不用交换位置了,堆化结束.

取队列元素:

public E peek() {
  return (size == 0) ? null : (E) queue[0];
}

取元素逻辑很简单,如果有元素就返回下标为0的位置元素, 如果没有返回null.

总结:

  • PriorityQueue是通过堆来实现的优先队列,是一个小顶堆
  • PriorityQueue是线程不安全的
  • PriorityQueue不是有序的,只有堆顶存储着最小元素
  • 入队就是堆的掺入元素的实现
  • 出队就是堆的删除元素的实现.

彩蛋:

Queue的addoffer实现是一致的,可以看出add同样也是调用offer().区别就是add在添加失败会抛异常.offer返回null. 为什么PriorityQueue没有抛异常呢?

这是因为PriorityQueue是一个无界队列, 当数组元素满了时,会做扩容,所以是不会添加失败的.

【555-week 01】学习总结

在数组的题中,有一些模板代码需要牢记,比如:将数组中的元素向右移动 1个位置、数组反转等,这些代码是解决其他一些题目的基础部分。

在数组相关的题目如:Leetcode-26删除排序数组中的重复项、LeetCode-11盛最多水的容器,所以在数组相关的题目中,我们可以想到使用双指针方法来解题。

【010-week1】第一周学习总结

满心期待的算法训练营终于开课了,本身在学校里学习了算法和数据结构,结构第一周的上课,还是发现自己很多东西不太懂。

第一课是讲基本的数组链表还有跳表,除了链表用的很少外,本以为是很熟悉的东西。但在遇到算法题时还是很难下手,覃超老师在这里讲到了一点令我眼睛发亮的东西,是双指针,这样改变了我一直以来觉得这类问题需要用多次循环,再加上上周对时间复杂度和空间复杂度的复习,觉得需要追求更好的解法。

在第一课的课后实践中,我也尝试去用双指针解决问题,但总是有几个cases没通过,我以为是我指针指向的问题,再我把一条条指针打印出来检查时,才意识到是复制数组时,浅拷贝的问题,共用了一个地址上的值,改变直接复制了会改变原有的数组。再次觉得计算机的知识需要不断积累,相互交映,才能写出更好的代码。

在第二课中,覃超老师再次帮我理通的了栈和队列。想着普通的数字,不同的特性可以创造不同的解法,心里真的觉得神奇。虽然到现在我对栈的理解还停留在表面的概念上,课后作业的求最大矩形面积还没用用栈写出来,还得花时间在上面,过程还是很痛苦,去理解代码,吃透,希望最后会有好的结果,拿到offer。

加油。

Review 其他同学作业:
055:我们同一组也是用js的。代码清晰简单,并且提供了多种解法,并我只用一种解法用心太多。 很棒
580: 用java写的同学,因为我本身对java不熟悉,这位同学代码易读性很好,部分代码有注释,继续加油!
430: 也是用java写的同学,注释很多。代码略长,或许能参考下覃超老师说的用自顶向下写法?
040: 用了三种不同的语言实现,代码可读性好,把题目也复在上面,知道对应的题目,也有注释,很棒!
280: 代码可读性好且把部分功能单独分成了function,需要向他学习,很棒!加油!

【280-week 01】学习总结

#学习总结

Array 数组

  • 优势:有序,查询快
  • 缺点:非头尾的更新操作慢
  • -适用于查询频率高的场景

Linked List 链表

  • 优势:更新操作快
  • 缺点:查询慢
  • -适用于更新操作频率高的场景

Skip List 跳表

  • 为优化Linked List 链表的查询问题而设计的

Stack & Queue

  • Stack:先入后出;添加、删除时间复杂度皆为 O(1)
  • Queue:先入先出;添加、删除时间复杂度皆为 O(1)

Deque

  • 两端可以进出的 Queue Deque - double ended queue
  • 插入和删除时间复杂度皆为 O(1)

Priority Queue

  • 插入操作:O(1)
  • 取出操作:O(logN) - 按照元素的优先级取出
  • 底层具体实现的数据结构较为多样和复杂:heap(堆)、bst(二叉搜索树)、treap
  • -适用场景:VIP插队类似的

#Review 与点评
430号同学代码精简思路清醒,就是注释太繁琐了
480号同学先写思路然后一题多解很值得学习
040号同学使用不同的语言实现,牛!
125号同学提交的代码十分工整,还分析了时间、空间复杂度,值得学习
060号同学在每道题下不仅有题解,还附上了题目的要求,阅读性很高

【030-week1】学习总结

源码分析:
Queue:普通队列结构,先进先出,只允许从底端加入元素,从顶端移除元素,不允许有遍历行为
PriorityQueue:优先级队列,可以通过构造器修改来指定排序规则https://blog.csdn.net/kobejayandy/article/details/46832797,参考了这篇帖子

学习总结
之前知道数据结构和算法有用,但是又没啥具体参考的方向,好多东西都被封装起来,希望跟着超哥的这个算法课能提升自己的基础能力,更重要的是能够在日常的工作中使用学习到的相关知识,比如以前写循环都是嵌套循环,没有时间复杂度的概念,现在写这种东西先想想有没有别的方法,多想一会也许提升的就不止一点点,最近项目交付,长期加班,项目结束到下个项目之前我会把所有的作业都补齐,现在就先做个最小值吧(Flag!!!!)

【145-week 01】学习总结

1.链表的题目中,主要出现的问题是:头节点丢失、终止条件的判断。
head结点丢失的情况,可以通过增加dummy_node作为head结点的前驱结点。
终止条件:判断是current==nullptr还是current->next==nullptr.
2.对于迭代器失效的问题,之前遇到过数组的迭代器失效,后来查阅了以下,
数组、树 和list迭代器失效的关系,可以参考https://blog.csdn.net/lujiandong1/article/details/49872763。
3.做ringbuffer时候,到底什么时候需要-circle_size % size? 需要重点加强。
4.在遇到需要stack或者队列解决问题的时候,到底在其中是存储的是index还是value?
参考most water。

【165-week1】学习总结

第一周

心得

数组与链表比较基础,原理都懂,但是真正写代码还是不够熟练,需要多些几遍加深对各种操作的熟练程度,并且掌握各种技巧如头尾指针,快慢指针,环形数组,以及对数组遍历边界的控制,链表则需要熟练操作插入与删除节点的操作。跳表是以前没怎么接触过的结构,其中的索引**值得学习。

队列与栈也是比较基础,底层可用多种数据结构实现如数组,链表等。由队列衍生出来的更高级的数据结构如双端队列,优先队列等平时也接触的不多,需要多练习。

刷题过程中,第一遍容易陷进去而花费大量时间,还是需要贯彻5-10分钟没有思路则直接看题解,会比较有效率。

练习步骤

每道题先考虑所有可能的题解,包括暴力法。昨晚后看题解与评论区,包括英文站。

  1. 5-10分钟思考,有思路就写,没思路看题解。
  2. 默写背诵,尝试自己写。
  3. 一周后再默写,面试前复习。

链表和数组

Prepend Append Lookup Insert Delete
数组 O(1) O(1) O(1) O(n) O(n)
链表 O(1) O(1) O(n) O(1) O(1)

跳表SkipList为补足链表查找的缺陷而创造,采用增加多级索引的方式减少查找的复杂度,但是相应插入和删除会牺牲一些时间重建索引,大部分场景可以代替红黑树。

链表工程中应用:LRU Cache、Redis Skip List

算法常见**:空间换时间,升维,如索引、hash等方法。数组操作技巧:头尾指针、快慢指针、环形数组。

倒序一个数组可用头尾指针。


队列和栈

  • Stack:先入后出; 添加、删除皆为 O(1)
  • Queue:先入先出; 添加、删除皆为 O(1)
  • Deque: 头尾都可以进出; 添加、删除皆为 O(1)
  • Priority Queue: 按照元素的优先级取出; 插入O(1),取出O(logN), 可用heap、bst、treap等实现

经典题目三数之和,暴力法,哈希,双指针。
查询 Stack、Queue、Deque、PriorityQueue 的系统接口的方法。

各数据结构复杂度

fb95e98883d213ed97144223626863d3.png


【040-Week 01】对学习本身的一些感想

学习的本质,就是当复读机

记得刚上小学的时候,前几堂课,老师会用“在书上XX位置写XX”这样的方式教我们,这就是我对学习这件事情本身的启蒙;而后,老师不会手把手教如何记笔记,但是依然会安排好每一个知识点的复习和预习;再后来上了中学,老师不安排预习了,但是复习依然是必不可少的环节;再后来上了大学,没有人告诉你学习的步骤了,老师们默认我们都知道,我们确实也应该知道,但是我忘了;最后毕业工作,由于IT行业日新月异,我必须不断更新知识,我感觉到了障碍。

刚毕业的那几年,我发现,每次看过的文档,读过的书,除非工作生活中会经常用到,否则根本我学不会,我不知道为什么,我好像就是忘记了怎么学习,制导在预习周的时候听到覃超老师讲五遍刷题法,并在第一周实际操作了之后,我找回了一点感觉。

最近网上流行一句话,“人类的本质就是复读机”,本来是形容在评论区里刷同一句评论的,我觉得用在学习上很合适。学习就是当好复读机,听课的时候记笔记本身就是复读,学完一遍,再学一遍,再学一遍,一遍一遍复读,最终学习。学习就是这样的单纯且简单,不浮躁,踏踏实实。

程序开发的学习提升,提升的是开发本身

我本身不是科班毕业的程序员,最开始学习计算机和编程的时候,心里面很有爽快的感觉,因为什么都不会,学什么都是在往上走,后来工作了,也能够很快速的吸收知识,不管是框架也好,代码风格也好,新技术也好,我心里面都会觉得是实实在在学到了。但是过了一段时间之后,我便赶紧无法提升了,各种技术知识仿佛都知道,老感觉用的时候拿来看一看就会了。

也是覃超老师的课上,我意识到基本功的重要性。最开始的爽快就是来自于基本功的修炼,后来的无力也是在于基本功的不扎实。基本盘不稳,看了再多的高级技术,都会崩塌。开发这件事,学的就是开发啊,并不是什么kafka,k8s。

我是怎么知道的

我也在想,我怎么就突然知道了。以前不是没看到过,有人告诉你要学好算法,打好基本功等等,有书上写要重复要重复。我现在有感觉,可能是因为我试过吧。以前只是看看,这两周试了一下,就知道了。

【120-week 01】学习总结

第一周学习内容

  • 主要的学习内容包括数组和链表,栈和队列
  • 掌握刷题套路和**,刷题套路就是"五毒神掌",**就是升维,比如说引入外部的变量
  • 掌握数组和链表的时间复杂度和空间复杂度分析
  • 了解并掌握 java 的基本语法,主要涉及数组方面的内容

数组

了解并优化数组的时间复杂度和空间复杂度
数组:查找 O(1) 插入删除 O(n)
比如说应当避免群移操作

链表

了解并优化数组的时间复杂度和空间复杂度
链表:查找 O(n) 插入删除 O(1)
使用 javascript 实现链表的操作
跳表:查找 O(logn) 插入删除 O(logn)

栈和队列

栈:先进后出 插入删除 O(1)查找 O(n)
队列:先进先出 插入删除 O(1)查找 O(n)

解题思路

  • 掌握题目所给定的信息,如有序数组等
  • 恰当的时候引入变量进行升维的操作
  • 循环的每一步要做的事,并且控制何时结束循环
  • 边界情况最后考虑

模型

数组

  1. 双层嵌套循环(经常用于暴力解法)
  2. 相邻元素直接的操作
  3. 双指针操作

Code Review

510号同学,题目的数量和质量比较高,包含多种解法,值得我们学习
55号同学使用了多种解法,代码逻辑清晰
40号同学用多种语言实现了算法,思路比较清晰
430号同学注释很详细,可读性好
535号同学解法多样,值得我们学习

算法训练营 (第4期) 第二周作业

要求

  1. 每周从覃超老师布置的题目中,至少完成并提交两道算法题
  2. 围绕每周重点学习的算法知识点,撰写一篇有观点和思考的技术文章或总结,切忌流水账。

作业提交 Deadline

2019年10月27日 23:59 (以当地时间为准)
未按时提交作业,会在个人作业总分中 -3 分

本周作业概述

本周需要完成学习的视频内容:

  • 第 5 课 | 哈希表、映射、集合
  • 第 6 课 | 数、图、二叉树、二叉搜索树
  • 第 7 课 | 泛型递归、树的递归
  • 第 8 课 | 分治、回溯

以上四课视频后,覃超老师都给出了大家可以练手的算法题。

其中,第 5 课的课后作业(第2节视频后)还包括:

  • 写一个关于HashMap的小总结
    说明:对于不熟悉Java语言的同学,此项作业可选做。

请大家将此部分作业,放在本周学习总结中一并提交

本周算法习题库:

第五课课后习题:

https://leetcode-cn.com/problems/valid-anagram/description/
https://leetcode-cn.com/problems/group-anagrams/
https://leetcode-cn.com/problems/two-sum/description/
写一个关于HashMap的小总结
说明:对于不熟悉Java语言的同学,此项作业可选做。
请大家将HashMap的小总结放在本周学习总结中一并提交

第六课课后习题:

https://leetcode-cn.com/problems/binary-tree-inorder-traversal/
https://leetcode-cn.com/problems/binary-tree-preorder-traversal/
https://leetcode-cn.com/problems/n-ary-tree-postorder-traversal/
https://leetcode-cn.com/problems/n-ary-tree-preorder-traversal/
https://leetcode-cn.com/problems/n-ary-tree-level-order-traversal/

第七课课后习题:

https://leetcode-cn.com/problems/lowest-common-ancestor-of-a-binary-tree/
https://leetcode-cn.com/problems/construct-binary-tree-from-preorder-and-inorder-traversal
https://leetcode-cn.com/problems/combinations/
https://leetcode-cn.com/problems/permutations/
https://leetcode-cn.com/problems/permutations-ii/

第八课课后习题:

https://leetcode-cn.com/problems/majority-element/description/
https://leetcode-cn.com/problems/letter-combinations-of-a-phone-number/
https://leetcode-cn.com/problems/n-queens/

205-Week 01 对于第一周学习的体会和感悟

本周总结
这周自己极客时间刚好有2门功课最后一周:ppt看的不多且不够透彻-至少自己很不满意,只能把课程学习中自己收获与做题中发现的问题做个总结。
1.关于跳表:其实这个概念听过N次,不过它的底层算法一直没有去真正梳理过;仅仅是单纯的使用过,可能就像老师PPT中工程中的应用中 Redis-Skip List那个链接去看过;其实无序的波动的根本还是一个个小波浪,课程中对此的降解让我想到了老师之前学习的时候说过的任何复杂的编程都是通过最简单的东西去实现的,之前用的时候没有想去底层算法的事情,课程中跟着学了才恍然明白是怎么回事。
2.Queue:其实刻意打算强化自己的算法的原因就是出自这周结束的极客时间的消息队列的课程:队列的原理明白,可是各种变化就变化着糊涂了,明白核心**-下不出手,甚至MQ的源码自己都看了,就是发现糊涂;就是觉得这块自己想看不知道怎么看也不知道从哪儿入手去看,虽然Java写肯定基本上是写不出的,不过课程听完我知道从哪儿入手去看去学习了。课后题让我们动手写,这个还是难度挺大;不过根据相关的知识和方式去看还是在逐渐做的,毕竟实践中不少东西是Java的,有时自己看着看着都。。。对我而言最大的收获可能就是知道这块应当怎么去看怎么去理解,期中考试之前把其中的一些思路记下来当作另外一种收获,后期在其它软件上去尝试实现。
3.通过Coding的实践过程中发现自己脑子里想去做的可能和实际代码写的过程中要去实现的还是有差距:不光是代码本身,可能还是有不可回避的类库。类库和接口其实同样重要:虽然本周课程中的第四课主要是讲怎么去查看接口,确实对于PYTHON而言-其丰富的类库同样重要;用类库的时候其实可以去看它的机制-这样就更加能够明白程序资源的消耗。
以上就是本周学习的一点总结和感受:下周自己极客时间的课程低于4门了,会把这周落下的一些再尽力补;就像老师说的3分学习7分练,其实对于基础差的同学可能可以是2:8原则甚至更低的1:9原则去提升自己。

【675-week1】学习总结

第一周总结:
本周主要内包含数组、列表、调表、栈、队列、优先队列
解题总纲:
1、升维
2、空间换时间

本周数组等相关思路:
1、快慢指针;
2、双向夹击

Queue和Priority Queue源码分析
Queue:
Queue是一个接口,定义方法有:
boolean add(E e) 添加一个元素,如果容量充足,返回成功,如果容量不足,则返回IllegalStateException
异常
boolean offer(E e) 容量不足时不会抛出异常。如果是限制容量的情况,则等价add方法
E remove();
E poll();
E element() 返回队列第一个元素,如果为空,则返回异常
E peek() 返回第一个元素,但是为空时,返回null而不是异常

实现类:LinkedList,PriorityQueue、ava.util.concurrent.LinkedBlockingQueue、java.util.concurrent.BlockingQueue
java.util.concurrent.ArrayBlockingQueue、java.util.concurrent.LinkedBlockingQueue、 java.util.concurrent.PriorityBlockingQueue

Priority Queue
Priority Queue集成了AbstractQueue,AbstractQueue实现Queue接口。具体通过优先级对实现队列。

codeReview:
430代码,代码通过双循环的方式,实现功能。在时间复杂度为O(n)存在优化的空间;
480:加一的代码整洁、清晰。学到了很多东西;
115:代码用多种方式实现,还标记了时间复杂度。数组旋转其中没给出时间复杂度的题目是O(n)的复杂度
150:代码简单,还注释了具体实现,方便大家学习
200:代码简洁,如果能有适当得注释就更好了,方便大家理解和维护。

【295-week1】开营直播感受、双指针解法总结、Java Queue 相关代码分析

开学典礼

今年毕业,开营直播来的很及时,里面内容对我有不少启发,如初入职场需要注意的点,算法训练的方法等。

学习观念

  • 刻意练习、唯手熟尔
  • 学习没有捷径
  • 理论和实践结合

个人成长

  • 核心竞争力打造
  • 自律、技术人需要持续成长

双指针解法总结

可根据指针移动的方向将双指针解法分为两类,如果两个指针从同一侧开始遍历,则为快慢指针,如果从两侧开始向中间移动,则为对撞指针。

快慢指针

快慢指针是双指针的一种,快指针在每一步走的步长要比慢指针一步走的步长要多。快指针通常的步速是慢指针的2倍。

应用

快慢指针多用于链表。

判断链表是否有环

  • 快慢指针用来判断是否有环,当快慢指针指向同一结点时,则有环。

  • 寻找环的入口:快慢指针相遇的时候,distance(fast指针) = 2 * distance(slow指针),可以推导出,只要把fast重新指向头结点,两个指针以一样的速度走,相遇的时候,便是环的入口。

移除重复元素

  • 数组完成排序后,我们可以放置两个指针 i 和 j,其中 i 是慢指针,而 j 是快指针。只要 nums[i] = nums[j] 时,我们就增加 j 以跳过重复项。

  • 当不等时,则将 j 指向的值复制到 i 指向的下一个位置。直到 j 到达末尾位置为止。

对撞指针

对撞指针,使用夹逼的**,是指在有序数组中,定义两个左右指针:

  • left = 0
  • right = nums.length - 1

然后从两头向中间进行数组遍历。

应用

对撞数组适用于有序数组,若题目给定有序数组时,应该第一时间想到用对撞指针解题。

几种常见场景:

  • 二分查找

  • 反转数组

  • 滑动窗口

相关习题
https://leetcode-cn.com/problems/remove-duplicates-from-sorted-array
https://leetcode-cn.com/problems/container-with-most-water/
https://leetcode-cn.com/problems/merge-sorted-array/
https://leetcode-cn.com/problems/trapping-rain-water/
https://leetcode-cn.com/problems/move-zeroes/
https://leetcode-cn.com/problems/reverse-string/
https://leetcode-cn.com/problems/3sum/

做题技巧

当一道题思路不清晰时,不妨降低难度,先练习难度较低的习题,然后再反过来思考较难的题,如 LeetCode 622 与 641 题。

如果一上来就做 641 题《设计循环双端队列》的话,可能有些地方会想不太明白;

那么此时先做 622 题《设计双端队列》,比较简单一些,等做完后,思考如何略微更改代码使得满足 641 题的需求,循序渐进,就会降低理解的难度。

改写 Deque 的代码

  • 用 addFirst 或 addLast 这套新的 API 改写 Deque 的代码

改写前:

Deque<String> deque = new LinkedList<String>();

deque.push("a"); 
deque.push("b"); 
deque.push("c"); 
System.out.println(deque); // [c, b, a]

String str = deque.peek(); 
System.out.println(str); // c
System.out.println(deque);

while (deque.size() > 0) {
    System.out.println(deque.pop()); 
} 
System.out.println(deque); // []

经查阅 Deque 和 Stack 方法的对应关系如下:

Stack Method Equivalent Deque Method
push(e) addFirst(e)
pop() removeFirst()
peek() peekFirst()

改写后:

Deque<String> deque = new LinkedList<String>();

deque.addFirst("a"); 
deque.addFirst("b"); 
deque.addFirst("c"); 
System.out.println(deque); // [c, b, a]

String str = deque.peekFirst(); 
System.out.println(str); // c
System.out.println(deque);

while (deque.size() > 0) {
    System.out.println(deque.removeFirst()); 
} 
System.out.println(deque); // []

分析 Queue 和 Priority Queue 的源码

Queue

Queue 继承自 Collection,用来存放等待处理的集合,这种场景一般用于缓冲、并发访问。以下为 Queue 接口定义的方法:

public interface Queue<E> extends Collection<E> {
    //插入(抛出异常)
    boolean add(E e);
    //插入(返回特殊值)
    boolean offer(E e);
    //移除(抛出异常)
    E remove();
    //移除(返回特殊值)
    E poll();
    //检查(抛出异常)
    E element();
    //检查(返回特殊值)
    E peek();
}

实现的功能:

  • 添加
  • 删除
  • 查询

从源码中可以看到 Java 为以上三种功能分别提供了两个具有不同的处理错误的方法,如 add(), remove(), element() 三个方法遇到异常时会抛出异常,而其他三个方法则会返回一个特定的值。

定义简洁,一目了然。

Java Queue 源码地址:
http://fuseyism.com/classpath/doc/java/util/Queue-source.html

PriorityQueue

基本特点:

  • 正常插入 O(1),按优先级出 O(logN)
  • 实现了 Serializable 接口
  • 不允许空值,而且不支持 non-comparable 对象。
  • 可自动扩容
  • 非线程安全

优先级的实现机制:

  • 二叉搜索树

Java 中通过二叉小顶堆实现,可以用一棵完全二叉树表示。
优先级可具体在 Comparator 比较器里定义。

部分源码:

//默认初始化大小  
private static final int DEFAULT_INITIAL_CAPACITY = 11;  
//堆  
private transient Object[] queue;  
//当前大小  
private int size = 0;  
//比较器  
private final Comparator<? super E> comparator;  
//修改次数(增、删、改、查)  
private transient int modCount = 0;  

实现的方法:

//增加  
public boolean add(E e)   
//查看队头元素(不删除)  
public E peek()  
//出队(删除)  
public E poll()  
//删除  
public boolean remove(Object o)  
//判断是否包含某元素  
public boolean contains(Object o)  
//清空队列  
public void clear()  
//扩容  
private void grow(int minCapacity)  
//查找  
private int indexOf(Object o)  

Java PriorityQueue 源码地址:
http://fuseyism.com/classpath/doc/java/util/PriorityQueue-source.html

Code Review

  1. 学号为 125 的同学提交的代码十分工整,还在注释里附上了解题思路及时间、空间复杂度,以后会借鉴此做法;并且在每个功能前都会加上注视,代码一目了然。
  2. 学号为 60 的同学在每道题下不仅有题解,还在文件里附上了题目的要求,这也是一种好的做法,以后打开代码就可以看到题目要求,以后可以借鉴参考。
  3. 学号为 115 的同学在每个题目里实现了多种解法,并且将每种解法实际提交后运行的耗时与内存写在了注释里,并且附上了时空复杂度,这样对比不同解法就更直观,也是后面学习借鉴的点。
  4. 学号为 100 的同学较为详细的写出了自己的思路。
  5. 学号为 510 的同学结合了我以上所说的1,2点,并且每题有多种解法,代码格式工整,值得学习借鉴。

【750-Week 01】学习总结

通过的第一周的学习看下来,虽然视频的中的内容都能听懂,也有一些了解,但一到做题,可以说是题题看不懂,道道没想法啊,尤其是对一些代码是以数学逻辑实现出来的时候,感到很吃力。有些时候甚至题解都要理解半天。虽然说的有些夸张,但即使做出来的题也废了我很大精力。我的内心受到的打击。

作为一个写了两年CRUD的小程序猿,听到算法这个词真的是既敬畏,又闻风丧胆啊。说实话我自己的逻辑思维不是很强,所以很多那些巧妙的想法不是很能看得懂,但是却觉得非常有意思。工作中有时遇到一些需求实现起来很困难很复杂的时候,也会陷入沉思,甚至会沉迷进去,寻找一个合适的实现办法,很多时候还是会想到一个很有意思的方法。在这个过程中,我的脑内活动就像一个个”算法“在帮助我想出合适的方法。回想那个过程,我发现其实”算法“真的没那么可怕,而且还是一个非常有趣的东西。

随着工作时间的增长,遇到的新问题也变得多起来。但不能每次都靠大量的时间和经历去寻找那个方法。所以我还是想学习一些前人的有趣知识来提升自己,为自己积累一些攻克问题的武器。虽然我的起点有点低,通过这一周的看视频学习老师教的一些方式方法,我渐渐的改变了以前对”算法“的看法,也没有我之前所说那么吓人,通过多次的看,写和理解也是渐渐的可以明白了。

这一周的总结,没写什么关于学习内容的东西,写了一些自己开始学习算法的感触。我希望通过这次总给自己一个鼓励,鼓励自己能坚持下去。在未来的学习中我会投入更多的经历在研究老师所讲的知识中,希望未来能有一些好的想法与同学们分享。

ps:不知道有没有和我有同样困惑的同学,希望我们一起坚持下去。

Deque改写:

import java.util.Deque;
import java.util.LinkedList;

public class test

{

public static void main(String[] args) {

    Deque<String> deque = new LinkedList<String>();
    deque.addFirst("a");
    deque.addFirst("b");
    deque.addFirst("c");
    System.out.println(deque); // [c, b, a]

    String str = deque.getFirst();
    System.out.println(str); // c
    System.out.println(deque); // [c, b, a]

    while (deque.size() > 0) {
        System.out.println(deque.removeFirst()); // c b a
    }
    System.out.println(deque); // []
}

}


Review
1,640同学 很好的总结了上周相关学习内容的知识点。而且还把知识点画成了脑图,这样更直观,简洁。以后我也会参考这种方式来总结内容。LeetCode_42_640.cpp 文件,换行有点多了,看起来有点费劲。
2,040同学 用了两种语言实现的题目,而且注释标注的很好,代码也很简洁。
3,185同学 python写的真好,很简洁。
4,495同学 很厉害,做了很多题,代码写的也很工整。
5,005同学,每道题都写了好几种解法。值得我学习一下。

【115-week1】第一周学习总结

1.学习感受

数据结构与算法零基础,没有时间复杂度,空间复杂度的概念。对于数据的处理基本就是循环嵌套循环 ,明明知道这样的代码很烂但是自己却没有能力改进自己的代码,所以我一定要把握好这次的机会,掌握数据结构与算法这门知识。经过这一周的学习,我感受到自己对明显的进步是心里有了时间复杂度,空间复杂度这样的概念,知道对自己的代码进行时间复杂度的分析,知道了自己所写的代码在面对大量数据时是否可以正常运行,会不会导致系统崩溃。一定要做到追求最低的时间复杂度,追求最简洁的代码逻辑。

2.知识总结

数组:查找O(1) 插入删除O(n)
链表:查找O(n) 插入删除O(1)
跳表:查找O(logn) 插入删除O(logn)
栈:先进后出 插入删除O(1)查找O(n)
队列:先进先出 插入删除O(1)查找O(n)

3.算法优化思路总结

  • 找到重复的点
  • 升维
  • 空间换时间

4.解题思路

  • 暴力解法
  • 无法进行暴力解法 看基本情况,找最近,重复子问题
  • 双指针
  • 快慢指针
  • 中间夹逼

5.Review 与点评

055 号同学的作业基本都是多种解法并存,逻辑清晰,代码格式工整,可读性好。
430号同学注释很仔细,阅读起来很舒适,代码也很简洁,可以学习一下。
125号同学提交的代码十分工整,还分析了时间、空间复杂度,值得学习。
510 号同学作业也给出了好多的解题思路,总结中也描述了自己的思考以及一些技巧,共同学习。
060号同学在每道题下附上了题目的要求,题解很易懂,代码读起来很顺畅。

【430-Week 01】学习总结

学习总结

这一周重温了下数组、链表、队列、栈这几个数据结构。最重要的是要做题,从做题中总结技巧、经验。老师说的生维让我印象深刻,比如三数之和,不就是两数之和的升维吗?从宏观方面看代码,更有助于举一反三。

代码review

阅读645同学的代码,学到了合并两个链表引入空节点的方法和简明的循环队列的实现,也建议一些题目尝试更优的解法。
735同学,代码实现很规范。
370同学,代码逻辑很清晰。
685同学,代码很简洁。
705同学,代码格式良好易读。

时间复杂度

数据结构 prepend append lookup insert delete
linkedlist O(1) O(1) O(n) O(1) O(1)
array O(1) O(1) O(1) O(n) O(n)
stack - - O(n) O(1) O(1)
queue - - O(n) O(1) O(1)
跳表 O(1) O(1) O(logn) O(1) O(1)

题目

数据结构 题目 解题关键 完成遍数
数组 11.最大盛水容器 双指针,因为容器越长越好,不用考虑中间遮挡,i从0开始,j从n-1开始,每次循环计算最大面积,循环条件i<j,需要移动端点时,往中间移动高度较小的那个,尽量多实用高度较高的那个,两端收缩 4
数组 283.移动零 快慢指针,快指针用于遍历,快指针记录不为0的元素下一个位置,当i,j<n时循环,先一个while循环找出下一个不为0的元素,然后nums[j++]=nums[i++],将最后一个非零位置值改成这个非零只,同时两个指针都自增,循环结束后j指示第一个应该放零的位置,循环直到n将元素改成0。需要注意的点:1.外循环条件别忘j<n。2.循环内部循环别忘判断数组边界。 3
数组 70.爬楼梯 从后往前状态记忆,数学归纳,dp[0]=1,dp[1]=1,return dp[n]。 3
数组 15.三数之和 排序+双指针,将数组排序用来去重,第一层循环遍历第一个元素,终止条件是i<n-2。因为三数相加为0,最小的数小于等于0,如果nums[i]>0 就跳出循环,另第一个元素去重,当nums[i]等于nums[i-1],则continue。l=i+1, r=n-1分别作为第二三个元素索引,排序更方便检索两个值得和。当l<r,将三叔相加,如果结果是0,加入结果列表,之后分别将i和j自增和自减,随后两个while循环去重,当nums[l]等于nums[l-1],l自增,当nums[r] = nums[r+1],r自减。如果结果小于0,l自增,否则r自减。需要注意的点:1.第一个元素大约0的提前break。2.第一个元素也要去重。3.去重要在获取的结果后进行,否则可能漏掉结果。4.内层循环去重方法需要注意。5.去重循环不要忘了判断数组越界。 3
链表 206.反转链表 三个变量,pre,cur,next,next在反转时缓存;引入pre作为尾可为null,从前往后prepend。需要注意的点:1.循环条件是cur!=null,不是cur.next!=null。2.循环内部不要少了pre = cur。3.返回pre而不是cur。 3
链表 24.两两交换节点 方法1:三个变量:pre,a,b,引入缺失的pre作为头,为空节点,且将该节点同head相连,pre从dummy开始,a、b分别为每次交换时的第一、二个元素,每次交换完两个节点都把尾节点与后面的节点连起来,将pre改成尾节点,可通过pre.next和pre.next.next找到后面两个节点。方法2:与方法1类似,多加交换函数,在交换后多加一个同后面节点连接的操作,注意连接时是head.next=cur,不是pre,pre是交换后的头节点。在外层循环中,先保存头节点cur = pre.next,将pre.next连上交换后的头节点,再更新尾节点pre = cur,因为连接操作,当只剩一个节点时不用多余操作,因为已与前面连接起来。方法3:递归,最简洁,假设后面已经交换完成,前面的元素要做的是第一个节点的next指向交换好的头,第二个元素指向第一个元素,返回第二个元素。递归出口是只有一个元素或0个元素,即head==null或head.next=null。 3
链表 141.环形链表 快慢指针,注意判断条件,p和q都从head开始,因为q要移动两次,索引要判断p,q和q.next。 3
链表 142.环形链表2 快慢指针求交点,从head和交点两个同速指针,相遇点即交点。需要注意的点:1.第一步寻找交点一定要引入新变量,因为进入不了循环和从循环跳出是两回事。2.可能存在交点为Null的情况,提前返回。3.二次寻找交点的时候,由于头节点也可能是交点,先判断ps是否等于q。 3
链表 25.K个一组交换节点 方法1:循环翻转,循环条件pre1=null,同上一题,关键点是翻转后的尾节点要和后面连接起来。每次循环先判断是否还有K个元素,不满足则直接返回dummy.next。满足,则先保存头节点next = pre.next,pre.next = 以pre.next为开头翻转的头节点,因为原头节点已变成尾节点所以更改pre = next。对于k个元素翻转,循环K次后,别忘了head.next = cur连接后面元素。方法2:递归,递归出口是元素个数少于k个,跳出循环不交换,直接返回Head。第一步先找到k个节点之后的第一个节点,对这个节点作为头节点进行递归处理得到反转后头节点h,之后从当前第一个节点开始进行k次反转,反转后的最后一个节点即为head,head指向n,返回头节点即反转前最后一个节点pre。需要注意的点:第一步中指针右移的条件是p != null而不是p.next != null 3
20.合法括号 左括号入栈,右括号匹配出栈,优化点是记录左括号可用数量,左括号没超量才允许放左括号,否则返回false,优化效果不大。使用数组代替map效率会更高。需要注意的点:1.从queue里poll出来的元素可能为null,不能直接和字符比较,要先和null比较。2.左右括号没匹配上直接return false,不要break。 3
155.最小栈 辅助栈记录当前最小值,主栈压入弹出和辅助栈同步。为了不适用多余map,当当前压入栈的值小于等最小值栈顶的值,则压入最小栈。弹出值时,吐过值等于最小栈栈顶的值,把最小栈也弹出。 3
84.直方图最大矩形 入栈index,构造高度递增栈,当前高度小于栈顶元素高度时,对高度大于等于当前元素高度的索引循环出栈,计算栈顶索引高度,宽度为(i-栈顶第二元素索引-1)的栈顶索引为右边界的最大面积,对于剩余的元素,计算宽度为(n-栈顶第二元素索引-1)的矩形面积最大值。为什么栈内存的是右边界?因为当前面高度大于等于时会被弹出压入新值,该索引直到下一个元素索引之前的高度都满足最小高度。需要注意的点:1.栈是递增站。2.计算最大值时,对poll出来的元素,别忘计算高度。 5
双向队列 239.滑动窗口最大值 双向队列,队首为最大元素下标,入队一个元素,相应下标的队首元素出队,队尾元素入队前值小于或等于的元素下标从后面出队。需要注意的点:1.j结果数组长度需要注意,为n-k+1。2.提前准备0-k窗口的初始值,提高效率。3.入队元素不需要判断条件,元素从后面入队。4,窗口最大值为队首不是队尾。4.结果数组下标同数据数组下标的对应要注意,为n-k+1。5.第一个窗口只有一个结果元素,下标为0。6.使用arrayDeque比LinkedList速度快。 3
数组 26.删除重复元素 双指针:一个指针用来循环所有元素,j放不循环元素的下一个位置,当j>=1 && nums[i]==nums[j-1],则跳过这个i,否则nums[j++] = nums[i++]。注意到i一直需要自增,且判断条件可以优化成不重复则放,而且只有一个元素需要额外判断索引,事实上第一个元素一定不重复。因此优化:i和j都从1开始,默认第一个元素已经放好,i改成for循环,当nums[i]!=nums[j-1],则nums[j++] = nums[i]。 4
数组 189. 旋转数组 方法1:环状替换,从0开始,k-1结束,从前往后移动元素,每次移动都将目标位置更新为pre的值,在此之前保存该新位置的值,替换后更新pre为该值,下一个元素坐标为新位置索引。循环直到下一个需移动的索引回到了i的初始值。在每个k都形成闭环的情况下,需要k次循环,但没有闭环的话可能一次就替换完,索引引入count值,每次移动count++,for循环最后count==n的情况下,跳出循环。需要注意的点:1.需要临时变量保存目标元素值。2.需要count计数和判断。3.内层while循环使用do-while。2.翻转数组:先将整个数组翻转,再先将前k%n个元素翻转,再将后面的元素翻转,这里k%n是因为k可能大于n。需要注意的点:1,翻转长度需要%n,k>n时不能直接返回,也要移动。2.第一个翻转终点是k%n-1,不是(k-1)%n。 4
链表 21. 合并两个有序链表 引入一个head节点,循环指针p=head,当l1和l2都不等于null时,p.next更新为其中的最小节点,l1或l2后移,p后移。循环结束后,p.next指向l2和l2中不为null的那个,最后返回head.next。需要注意的的点:1.第一步循环别忘后移p指针。2.循环结束后不需要对多余的节点进行遍历,直接更新p.next即可。 4
数组 88. 合并两个有序数组 l从m+n-1位置开始递减,从后往前在两个数组中找最大值放在l的位置,直到两个数组中有一个用完。如果用完的nums2,那么nums1一定从来没移动过,数组已排序完整,不需要额外操作,否则,则nums1全比剩余nums2大,空出前面的位置把nums2剩余元素复制过去即可。复制的起始索引时0,复制的长度是j+1或l+1。 4
数组 1. 两数之和 hash表,记录当前索引需要被加数,key为被加数,值为索引,如果当前元素值在map的key中存在,就返回map值和当前索引作为元素的数组。将每个数组元素在循环中提前算出能加快速度。注意这道题用排序+双指针不行,因为这道题需要的是索引,不同索引的元素可能相同。 3
数组 66. 加一 因为是加1,考虑进位的时候只考虑9,进位后结果只可能是0。有两种清空:进位,当前位变成0,且可连续进位,最终不需要进位的位加1,这里还需要考虑需要加一位的情况;不进位,当前位加一。所以先用个while循环从后往前,将为9d的位全部变成0,跳出循环结束的当前位索引,如果是-1,说明需要扩展高位,返回一个长度为n+1,首尾为0的数组。否则,当前位加1,因为当前位可能是没有进过位的最低位,到此程序完。需要注意的点:第一个while循环要不断减不是假。2.需要加高位的条件是当前位索引是-1,不是0。3.最后当前位加一包括无进位的情况,不要多加1。 3
队列 641. 设计循环双端队列 数组实现循环队列,可增加计数器记录size,也可在初始化数组时capacity增加1,这样队列为空和队列满将不是同一个条件,另数组下标直接引用赋值结果会让运行速度变快;也可设置队列数组时多余一个长度,用于判断队列空(tail在head相邻的后面),满的条件是(tail-head+capacity+1)%(capacity+1)==capacity-1,这样就不需要额外的长度变量 2
42. 接雨水 构造高度递减栈,每个元素作为右边界,从栈中弹出高度小于当前高度的元素高度作为水坑底,下一栈顶元素作为左边界,(min(左边界,当前高度)-弹出高度)*(当前序号-左边界-1)加入和。当前序号(左边界或计算过的高度)入栈 2

Deque代码改写

Deque<String> deque = new LinkedList<String>();
deque.addFirst("a"); 
deque.addFirst("b");
deque.addFirst("c");
System.out.println(deque);
String str = deque.getFirst();
System.out.println(str);
System.out.println(deque);
while (deque.size() > 0) {
  System.out.println(deque.removeFirst()); 
}
System.out.println(deque);

Queue和Priority Queue源码分析

Queue

参考文章:Java ArrayDeque源码剖析

Queue是队列的接口,其实现接口有Deque。Deque的含义是“double ended queue”,即双端队列,它既可以当作栈使用,也可以当作队列使用。下表列出了DequeQueue相对应的接口:

Queue Method Equivalent Deque Method 说明
add(e) addLast(e) 向队尾插入元素,失败则抛出异常
offer(e) offerLast(e) 向队尾插入元素,失败则返回false
remove() removeFirst() 获取并删除队首元素,失败则抛出异常
poll() pollFirst() 获取并删除队首元素,失败则返回null
element() getFirst() 获取但不删除队首元素,失败则抛出异常
peek() peekFirst() 获取但不删除队首元素,失败则返回null

下表列出了DequeStack对应的接口:

Stack Method Equivalent Deque Method 说明
push(e) addFirst(e) 向栈顶插入元素,失败则抛出异常
offerFirst(e) 向栈顶插入元素,失败则返回false
pop() removeFirst() 获取并删除栈顶元素,失败则抛出异常
pollFirst() 获取并删除栈顶元素,失败则返回null
peek() getFirst() 获取但不删除栈顶元素,失败则抛出异常
peekFirst() 获取但不删除栈顶元素,失败则返回null

上面两个表共定义了Deque的12个接口。添加,删除,取值都有两套接口,它们功能相同,区别是对失败情况的处理不同。一套接口遇到失败就会抛出异常,另一套遇到失败会返回特殊值(falsenull。除非某种实现对容量有限制,大多数情况下,添加操作是不会失败的。虽然Deque的接口有12个之多,但无非就是对容器的两端进行操作,或添加,或删除,或查看

ArrayDequeLinkedListDeque的两个通用实现,官方更推荐使用AarryDeque用作栈和队列。

从名字可以看出ArrayDeque底层通过数组实现,为了满足可以同时在数组两端插入或删除元素的需求,该数组还必须是循环的,即循环数组(circular array),也就是说数组的任何一点都可能被看作起点或者终点。ArrayDeque是非线程安全的(not thread-safe),当多个线程同时使用的时候,需要程序员手动同步;另外,该容器不允许放入null元素。

head指向首端第一个有效元素,tail指向尾端第一个可以插入元素的空位。因为是循环数组,所以head不一定总等于0,tail也不一定总是比head大。

addFirst()

addFirst(E e)的作用是在Deque的首端插入元素,也就是在head的前面插入元素,在空间足够且下标没有越界的情况下,只需要将elements[--head] = e即可。

实际需要考虑:1.空间是否够用,以及2.下标是否越界的问题。上图中,如果head0之后接着调用addFirst(),虽然空余空间还够用,但head-1,下标越界了。下列代码很好的解决了这两个问题。

//addFirst(E e)
public void addFirst(E e) {
    if (e == null)//不允许放入null
        throw new NullPointerException();
    elements[head = (head - 1) & (elements.length - 1)] = e;//2.下标是否越界
    if (head == tail)//1.空间是否够用
        doubleCapacity();//扩容
}

上述代码我们看到,空间问题是在插入之后解决的,因为tail总是指向下一个可插入的空位,也就意味着elements数组至少有一个空位,所以插入元素的时候不用考虑空间问题。

下标越界的处理解决起来非常简单,head = (head - 1) & (elements.length - 1)就可以了,这段代码相当于取余,同时解决了head为负值的情况。因为elements.length必需是2的指数倍,elements - 1就是二进制低位全1,跟head - 1相与之后就起到了取模的作用,如果head - 1为负数(其实只可能是-1),则相当于对其取相对于elements.length的补码。

下面再说说扩容函数doubleCapacity(),其逻辑是申请一个更大的数组(原数组的两倍),然后将原数组复制过去。

复制分两次进行,第一次复制head右边的元素,第二次复制head左边的元素。

//doubleCapacity()
private void doubleCapacity() {
    assert head == tail;
    int p = head;
    int n = elements.length;
    int r = n - p; // head右边元素的个数
    int newCapacity = n << 1;//原空间的2倍
    if (newCapacity < 0)
        throw new IllegalStateException("Sorry, deque too big");
    Object[] a = new Object[newCapacity];
    System.arraycopy(elements, p, a, 0, r);//复制右半部分,对应上图中绿色部分
    System.arraycopy(elements, 0, a, r, p);//复制左半部分,对应上图中灰色部分
    elements = (E[])a;
    head = 0;
    tail = n;
}

addLast()

addLast(E e)的作用是在Deque的尾端插入元素,也就是在tail的位置插入元素,由于tail总是指向下一个可以插入的空位,因此只需要elements[tail] = e;即可。插入完成后再检查空间,如果空间已经用光,则调用doubleCapacity()进行扩容。

public void addLast(E e) {
    if (e == null)//不允许放入null
        throw new NullPointerException();
    elements[tail] = e;//赋值
    if ( (tail = (tail + 1) & (elements.length - 1)) == head)//下标越界处理
        doubleCapacity();//扩容
}

pollFirst()

pollFirst()的作用是删除并返回Deque首端元素,也即是head位置处的元素。如果容器不空,只需要直接返回elements[head]即可,当然还需要处理下标的问题。由于ArrayDeque中不允许放入null,当elements[head] == null时,意味着容器为空。

public E pollFirst() {
    E result = elements[head];
    if (result == null)//null值意味着deque为空
        return null;
    elements[h] = null;//let GC work
    head = (head + 1) & (elements.length - 1);//下标越界处理
    return result;
}

pollLast()

pollLast()的作用是删除并返回Deque尾端元素,也即是tail位置前面的那个元素。

public E pollLast() {
    int t = (tail - 1) & (elements.length - 1);//tail的上一个位置是最后一个元素
    E result = elements[t];
    if (result == null)//null值意味着deque为空
        return null;
    elements[t] = null;//let GC work
    tail = t;
    return result;
}

peekFirst()

peekFirst()的作用是返回但不删除Deque首端元素,也即是head位置处的元素,直接返回elements[head]即可。

public E peekFirst() {
    return elements[head]; // elements[head] is null if deque empty
}

peekLast()

peekLast()的作用是返回但不删除Deque尾端元素,也即是tail位置前面的那个元素。

public E peekLast() {
    return elements[(tail - 1) & (elements.length - 1)];
}

PriorityQueue

参考文章:深入理解Java PriorityQueue

优先队列的作用是能保证每次取出的元素都是队列中权值最小的(Java的优先队列每次取最小元素)。这里牵涉到了大小关系,元素大小的评判可以通过元素本身的自然顺序(natural ordering),也可以通过构造时传入的比较器(Comparator)。

Java中PriorityQueue实现了Queue接口,不允许放入null元素;其通过堆实现,具体说是通过完全二叉树(complete binary tree)实现的小顶堆(任意一个非叶子节点的权值,都不大于其左右子节点的权值),也就意味着可以通过数组来作为PriorityQueue的底层实现。

PriorityQueuepeek()element操作是常数时间,add(), offer(), 无参数的remove()以及poll()方法的时间复杂度都是log(N)

add()和offer()

add(E e)offer(E e)的语义相同,都是向优先队列中插入元素,只是Queue接口规定二者对插入失败时的处理不同,前者在插入失败时抛出异常,后则则会返回false。对于PriorityQueue这两个方法其实没什么差别。

新加入的元素可能会破坏小顶堆的性质,因此需要进行必要的调整。

//offer(E e)
public boolean offer(E e) {
    if (e == null)//不允许放入null元素
        throw new NullPointerException();
    modCount++;
    int i = size;
    if (i >= queue.length)
        grow(i + 1);//自动扩容
    size = i + 1;
    if (i == 0)//队列原来为空,这是插入的第一个元素
        queue[0] = e;
    else
        siftUp(i, e);//调整
    return true;
}

上述代码中,扩容函数grow()类似于ArrayList里的grow()函数,就是再申请一个更大的数组,并将原数组的元素复制过去,这里不再赘述。需要注意的是siftUp(int k, E x)方法,该方法用于插入元素x并维持堆的特性。

//siftUp()
private void siftUp(int k, E x) {
    while (k > 0) {
        int parent = (k - 1) >>> 1;//parentNo = (nodeNo-1)/2
        Object e = queue[parent];
        if (comparator.compare(x, (E) e) >= 0)//调用比较器的比较方法
            break;
        queue[k] = e;
        k = parent;
    }
    queue[k] = x;
}

新加入的元素x可能会破坏小顶堆的性质,因此需要进行调整。调整的过程为:k指定的位置开始,将x逐层与当前点的parent进行比较并交换,直到满足x >= queue[parent]为止。注意这里的比较可以是元素的自然顺序,也可以是依靠比较器的顺序。

element()和peek()

element()peek()的语义完全相同,都是获取但不删除队首元素,也就是队列中权值最小的那个元素,二者唯一的区别是当方法失败时前者抛出异常,后者返回null。根据小顶堆的性质,堆顶那个元素就是全局最小的那个;由于堆用数组表示,根据下标关系,0下标处的那个元素既是堆顶元素。所以直接返回数组0下标处的那个元素即可

代码也就非常简洁:

//peek()
public E peek() {
    if (size == 0)
        return null;
    return (E) queue[0];//0下标处的那个元素就是最小的那个
}

remove()和poll()

remove()poll()方法的语义也完全相同,都是获取并删除队首元素,区别是当方法失败时前者抛出异常,后者返回null。由于删除操作会改变队列的结构,为维护小顶堆的性质,需要进行必要的调整。

代码如下:

public E poll() {
    if (size == 0)
        return null;
    int s = --size;
    modCount++;
    E result = (E) queue[0];//0下标处的那个元素就是最小的那个
    E x = (E) queue[s];
    queue[s] = null;
    if (s != 0)
        siftDown(0, x);//调整
    return result;
}

上述代码首先记录0下标处的元素,并用最后一个元素替换0下标位置的元素,之后调用siftDown()方法对堆进行调整,最后返回原来0下标处的那个元素(也就是最小的那个元素)。重点是siftDown(int k, E x)方法,该方法的作用是k指定的位置开始,将x逐层向下与当前点的左右孩子中较小的那个交换,直到x小于或等于左右孩子中的任何一个为止

//siftDown()
private void siftDown(int k, E x) {
    int half = size >>> 1;
    while (k < half) {
    	//首先找到左右孩子中较小的那个,记录到c里,并用child记录其下标
        int child = (k << 1) + 1;//leftNo = parentNo*2+1
        Object c = queue[child];
        int right = child + 1;
        if (right < size &&
            comparator.compare((E) c, (E) queue[right]) > 0)
            c = queue[child = right];
        if (comparator.compare(x, (E) c) <= 0)
            break;
        queue[k] = c;//然后用c取代原来的值
        k = child;
    }
    queue[k] = x;
}

remove(Object o)

remove(Object o)方法用于删除队列中跟o相等的某一个元素(如果有多个相等,只删除一个),该方法不是Queue接口内的方法,而是Collection接口的方法。由于删除操作会改变队列结构,所以要进行调整;又由于删除元素的位置可能是任意的,所以调整过程比其它函数稍加繁琐。具体来说,remove(Object o)可以分为2种情况:1. 删除的是最后一个元素。直接删除即可,不需要调整。2. 删除的不是最后一个元素,从删除点开始以最后一个元素为参照调用一次siftDown()即可。

具体代码如下:

//remove(Object o)
public boolean remove(Object o) {
	//通过遍历数组的方式找到第一个满足o.equals(queue[i])元素的下标
    int i = indexOf(o);
    if (i == -1)
        return false;
    int s = --size;
    if (s == i) //情况1
        queue[i] = null;
    else {
        E moved = (E) queue[s];
        queue[s] = null;
        siftDown(i, moved);//情况2
        ......
    }
    return true;
}

【455-week 01】学习总结

学习内容

第三课

数组
链表
跳表

第四课


队列

知识点总结

  • 数组
数组用一块连续的内存空间,来存储相同类型的一组数据。
特点:
1.支持随机访问,O(1)
2.插入、删除操作比较低效,平均情况时间复杂度为O(n)
  • 链表
链表类型有:单链表、双向链表、循环链表、双向循环链表
与数组相比,更适合插入、删除操作频繁的场景。
  • 跳表
使用空间换时间的设计思路,通过构建多级索引来提高查询的效率,实现了基于链表的“二分查找”。
是一种动态数据结构,支持快速的插入、删除、查找操作,时间复杂度为O(nlogn)
空间复杂度为O(n).
栈是一种受限的数据结构,只支持入栈和出栈操作。
特点:后进先出
入栈、出栈时间复杂度为O(1)
  • 队列
特点:先进先出
常见队列结构:
1.优先队列
2.双端队列

实践总结

按照老师的方法,在leetcode上刷题上的感受:
需要熟练掌握基本数据结构类型的实现,最好通过大量的练习形成肌肉记忆。
每道题都有很多种解法,但基本分类大致相同,需要总结整理。

  • 暴力法
一般比较容易想到的方法。
通过多层循环实现。
时间复杂度较高。
要做多快速准确地写出实现代码。
  • 双指针
在解决很多数组或链表的问题上非常好用。
一般通过一次遍历即可实现。
降低时间复杂度
  • 利用一些数据结构
例如哈希表。

课后作业

  • 用add first或add last这套新的API改写Deque的代码
  • 分析Queue和Priority Queue的源码

还未完成,之后会参考其他大神的作业完成

【635-week 01】学习总结

第一次参加算法的专项训练营。
之前对数据结构和算法了解不少,在学校中成绩也不错,只是工作后动手少了些,熟练度不够,因此起初来训练营的目的主要是想提升熟练度。
但通过第一周的实际练习,发现自己不仅熟练度不够,而且自己对算法思路的理解不深入,也不全面。
以接雨水为例,自己看完题目后,比较懵。
想了好久,只想到双指针的方法,但如何具体操作双指针,实际撸起代码来,写起单元测试来,结结巴巴、磕磕碰碰,bug频出。
但好在用谭老师提前说过的学习方法,书读百遍其义自见,先记住一个易理解的较优解法,然后理解其他各种解法,进行比较,然后用不同方法重复刷题,加深理解和熟练。
一轮操作下来,的确除了双指针法,暴力、动态规划、栈的方法练了一通,不仅把这周的知识点好好的实操了一遍,加深了对知识点的理解,还增强了自己对代码编写的感觉。
例如,暴力法虽然在执行上不高效,但对提高代码熟练度来说真的很好,而且也能够加深我对时间复杂度和空间复杂度产生的原因,有了最直观的认识,因为这代码是我自己敲出来的……

啰嗦的感受,下面说这周不足的地方。
时间不够、时间不够、还是时间不够。
按照谭老师的方法练习一遍下来,感觉一周的时间太少了,别说4道作业,就是2道作业,按照方法几遍练下来,用的时间都好多。
另外,我喜欢跑步,跑步要时间。同时,工作996,晚上还要带娃,现在加上训练营,真的有些要命了,我感觉自己是超人了……

好了,希望自己能够在这60多天里坚持下来,并且真正的成为超人。

【495-week1】Queue及Priority Queue源码学习笔记

作业一: 用add first或add last这套新的API改写Deque的代码

public class NewLinkedListDemo {

  public static void main(String[] args) {
    // LinkedList没有指定并固定容量的构造方法
    Deque<String> deque = new LinkedList<String>();

    // 如果可以立即执行此操作,而不会违反容量限制,则在此双端队列的前面插入指定的元素;
    // 如果当前没有可用空间,则抛出IllegalStateException
    deque.addFirst("a");
    deque.addFirst("b");
    deque.addFirst("c");
    deque.addLast("d");
    // 这里插入成功会返回true
    boolean result = deque.offerFirst("e");
    System.out.println(deque);

    String str = deque.peek();
    System.out.println(str);
    System.out.println(deque);

    while(deque.size() >= 0) {
      // 这里如果LinkedList为空,会返回null
      System.out.println(deque.pollFirst());
    }
    System.out.println(deque);
  }
}
public class NewLinkedBlockingDequeDemo {

  public static void main(String[] args) {

    // 这里指定容器的固定容量为3
    Deque<String> deque = new LinkedBlockingDeque<String>(3);

    // 如果可以立即执行此操作,而不会违反容量限制,则在此双端队列的前面插入指定的元素;
    deque.addFirst("a");
    deque.addFirst("b");
    deque.addFirst("c");
    // 这里的插入由于容器没有可用空间,则抛出IllegalStateException
    // deque.addLast("d");

    // 这里的插入成功会返回true,失败会返回false,不会抛出异常
    boolean result = deque.offerFirst("e");
    System.out.println(deque);

    String str = deque.peek();
    System.out.println(str);
    System.out.println(deque);

    while(deque.size() >= 0) {
      // 这里如果LinkedList为空,会返回null
      System.out.println(deque.pollFirst());
    }
    // pop方法,由于容器中没有元素,会抛出异常NoSuchElementException
    // deque.pop();
    /*
    TODO: deque.pollFirst(long timeout, TimeUnit unit) 这个方法是怎么用的?
    */
    System.out.println(deque);
  }
}

作业二: 分析Queue和Priority Queue的源码

分析Queue源码

1.概述

Queue是java中一个接口,默认有LinkedList,LinkedBlockingDeque,PriorityQueue等等多个实现类,这个接口中定义了6个需要实现类实现的方法,如下表:

方法定义 Description
boolean add (E e) 如果可以立即将指定的元素插入此队列,而不会违反容量限制,则在成功时返回true,如果当前没有可用空间,则抛出IllegalStateException
E element () 检索但不删除此队列的头,队列为空时,抛出NoSuchElementException
boolean offer (E e) 如果可以立即将指定的元素插入此队列,而不会违反容量限制,则在成功时返回true,否则返回false
E peek () 检索但不删除此队列的头部,如果此队列为空,则返回null
E poll () 检索并删除此队列的头部,如果此队列为空,则返回null
E remove () 检索并删除此队列的头,队列为空时,抛出NoSuchElementException

2.继承关系

在jdk1.8中Queue继承了Collection接口,Collection接口又继承了Iterable,所以Queue也拥有了常规集合的一些行为,尤其是jdk1.8的新特性,Stream流的一系列操作

分析Priority Queue的源码

1.概述

PriorityQueue是一个

  • 基于优先级堆的无界优先级队列。
  • 优先级队列的元素按照其自然顺序进行排序,
  • 或者根据构造队列时提供的 Comparator 进行排序,具体取决于所使用的构造方法。
  • 优先级队列不允许使用 null 元素。依靠自然顺序的优先级队列还不允许插入不可比较的对象(这样做可能导致 ClassCastException)

2.看源码,自己翻译的,可能会有出入

属性及常量

// 默认的容量大小为11
private static final int DEFAULT_INITIAL_CAPACITY = 11;
// 队列中元素的个数
private int size = 0;
// 比较器,用来给队列中的元素排序,默认为元素们的自然顺序
private final Comparator<? super E> comparator;
// 这个是记录元素修改次数, 因为这个队列操作是线程不安全的,在使用迭代器的过程中有其他线程修改则快速响应失败.就是所谓的fast-fail机制
transient int modCount = 0;
// 这个就是用来存元素的喽
transient Object[] queue;
// 队列的最大size,这里减8的官方解释是:虚拟机需要存储数组的头信息
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

方法

// add方法默认调用了offer方法
public boolean add(E e) {
    return offer(e);
}

public boolean offer(E e) {
    // 判空
    if (e == null)
        throw new NullPointerException();
    modCount++;
    int i = size;
    // 这里看队列中的元素个数是否大于等于队列现在的长度,是的话,就扩容了
    if (i >= queue.length)
        grow(i + 1);
    size = i + 1;
    // 如果是插入第一个值,则最简单
    if (i == 0)
        queue[0] = e;
    else
        // 插入值的逻辑
        siftUp(i, e);
    return true;
}

/**
* 检索并删除此队列的头部,如果此队列为空,则返回null,时间复杂度受siftDown影响为O(logn)
*/
@SuppressWarnings("unchecked")
public E poll() {
    if (size == 0)
        return null;
    int s = --size;
    modCount++;
    E result = (E) queue[0];
    E x = (E) queue[s];
    queue[s] = null;
    if (s != 0)
        // 这里执行重排序
        siftDown(0, x);
    return result;
}

/**
* 从队列中删除某个元素,时间复杂度是O(n + logn)?
*/
public boolean remove(Object o) {
    int i = indexOf(o);
    if (i == -1)
        return false;
    else {
        removeAt(i);
        return true;
    }
}

@SuppressWarnings("unchecked")
/**
* 这里就直接把队列最顶上的元素取出来给你看看就好了
*/
public E peek() {
     return (size == 0) ? null : (E) queue[0];
}

/**
* 用来查找某个元素所在索引的方法,时间复杂度O(n)
*/
private int indexOf(Object o) {
     if (o != null) {
         for (int i = 0; i < size; i++)
              if (o.equals(queue[i]))
                  return i;
      }
      return -1;
}

/**
* 扩容逻辑,从源码看,在队列的长度小于64时,当元素个数低于64时,每次翻倍.
* 当元素个数大于64时,每次增加原来的一半.
*/
private void grow(int minCapacity) {
    int oldCapacity = queue.length;
    // Double size if small; else grow by 50%
    int newCapacity = oldCapacity + ((oldCapacity < 64) ?
                                       (oldCapacity + 2) :
                                       (oldCapacity >> 1));
    // overflow-conscious code
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    queue = Arrays.copyOf(queue, newCapacity);
}

private void siftUp(int k, E x) {
     // 如果没有设置比较器,则按元素自然顺序排序
     if (comparator != null)
         siftUpUsingComparator(k, x);
     else
         siftUpComparable(k, x);
}

/**
* 未指定比较器的插入算法
*/
@SuppressWarnings("unchecked")
private void siftUpUsingComparator(int k, E x) {
    while (k > 0) {
        // 无符号右移
        int parent = (k - 1) >>> 1;
        Object e = queue[parent];
        if (comparator.compare(x, (E) e) >= 0)
            break;
        queue[k] = e;
        k = parent;
    }
    queue[k] = x;
}

写在后面:

希望自己能坚持下去

【380-week 01】学习总结

最近每天加班,头都大了。不过还好,周末双休,总算能抽出时间来学习算法的课程。
我很诚实地说,我是没有预习课件的。我是听了极客时间 APP 中覃超老师的理论部分作为预习,幸亏,本周的内容大学里几乎都有接触到,所以我还是能够理解理论知识的部分的,但是算法题确实是让我知道自己和别人的差距。嗯,不多说了,先把学习的内容做个小结,以后有时间再来优化~
本周是第一周,学了数组(Array(ArrayList))、链表(LinkedList)、跳表(Skip List)、栈(Stack)、队列(Queue)、优先队列(Priority Queue)和双端队列(Deque(Double-End Queue)),还有 Stack 等的 Java 源码解析,LeetCode 上相关算法题思路讲解。
· 数组 Array:
	时间复杂度:

prepend O(1) (刚开始个人以为是 O(n),据说是用循环队列或双端队列实现)
append O(1)
lookup O(1)
insert O(n)
delete O(n)
· 链表 LinkedList:
时间复杂度:
prepend O(1)
append O(1)
lookup O(n)
insert O(1)
delete O(1)
· 跳表 Skip List:
时间复杂度:
prepend O(1)
append O(1)
lookup O(logn)
insert O(1)
delete O(1)
空间复杂度: O(n)
· 栈 Stack:
先入后出 FILO
时间复杂度:
insert O(1)
delete O(1)
· 队列 Queue:
先入先出 FIFO
时间复杂度:
insert O(1)
delete O(1)
· 双端队列 Deque:
时间复杂度:
insert O(1)
delete O(1)
· 优先队列 Priority Queue:
时间复杂度:
insert O(1)
取出 O(logn)

印象最深刻的几点:
	1、过遍数。最大误区:刷题只刷一遍。
	2、两个思路:
		1)升维(空间换时间);
		2)找到最小的重复单元(if else, for/while, recursion)。
	3、遇到没有思路的问题不要较劲,直接看题解并思考、学习,然后”五毒神掌“多刷几遍,直到熟练。
	4、查询不熟悉 API 的方法。
	
改写 Deque 代码:
	```
           Deque<String> deque2 = new LinkedList<>();
	/*deque.push("a");
	deque.push("b");
	deque.push("c");*/
	/*deque2.addFirst("a");
	deque2.addFirst("b");
	deque2.addFirst("c");*/
	deque2.addLast("c");
	deque2.addLast("b");
	deque2.addLast("a");

	System.out.println("deque3 : " + deque2); // [c, b, a]

	/*String string2 = deque2.peek();*/
	String string2 = deque2.peekFirst();
	String string3 = deque2.peekLast();
	System.out.println("string1 : " + string2); // c
	System.out.println("string2 : " + string3); // a
	System.out.println("deque4 : " + deque2); // [c, b, a]

	while (deque2.size() > 0) {
		System.out.println(deque2.pop());
		// c
		// b
		// a
	}

	System.out.println("deque5 : " + deque2); // []

	分析 Queue 源码:
		方法:
	
		第一组:
			add(e)
			remove()
			element()
		
		第二组:
			offer(e)
			poll()
			peek()
			
		以上两组分别是添加、删除、查询三种方法。区别是:如果出错,第一组会抛异常,而第二组会返回一个特殊值。目前我还没有深入研究,觉得可能是线程安全和不安全的原因才会出现这两组方法(个人猜测)。
		和 Stack 不同(class Stack<E> extends Vector<E>),Queue 是个接口,而且继承的是 Collection(public interface Queue<E> extends Collection<E>)。

	分析 Priority Queue 源码:
		方法:
			add(e)
			clear()
			comparator()
			contains(o)
			interator()
			offer(e)
			remove(o)
                        ……
			
		优先队列是继承了 实现了 Queue 接口的抽象类 AbstractQueue(public class PriorityQueue<E> extends AbstractQueue<E>、public abstract class AbstractQueue<E> extends AbstractCollection<E> implements Queue<E> )。
		public boolean add(E e) {
			return offer(e);
		}
		add 方法中直接 return 了一个 offer 方法。
		public boolean offer(E e) {
			if (e == null)
				throw new NullPointerException();
			modCount++;
			int i = size;
			if (i >= queue.length)
				grow(i + 1);
			size = i + 1;
			if (i == 0)
				queue[0] = e;
			else
				siftUp(i, e);
			return true;
		}
		先判空,modCount 用处应该和 Stack 相同。
		private void grow(int minCapacity) {
			int oldCapacity = queue.length;
			// Double size if small; else grow by 50%
			int newCapacity = oldCapacity + ((oldCapacity < 64) ?
											 (oldCapacity + 2) :
											 (oldCapacity >> 1));
			// overflow-conscious code
			if (newCapacity - MAX_ARRAY_SIZE > 0)
				newCapacity = hugeCapacity(minCapacity);
			queue = Arrays.copyOf(queue, newCapacity);
		}

		private static int hugeCapacity(int minCapacity) {
			if (minCapacity < 0) // overflow
				throw new OutOfMemoryError();
			return (minCapacity > MAX_ARRAY_SIZE) ?
				Integer.MAX_VALUE :
				MAX_ARRAY_SIZE;
		}
		grow 方法这里有一个越界异常的判断操作。这边的 newCapacity 操作我看网上的解释是:“// 小于64扩大为原来2倍,大于等于64扩大为原来1.5倍”,最后的 queue 也有一个 copy 的操作,所以感觉如果添加过多也会消耗资源。
		siftUp()具体的操作我还要研究下,目前还没整明白(参考:https://www.bbsmax.com/A/gGdXLlQWd4/),但我觉得应该是通过树结构的方式排优先级的(猜测)。
		
		(先写这么多了,还要写作业--哭唧唧~哈哈。以前总觉得我的水平菜,看不懂源码,也就看看同事的代码。但是现在想想,看源码也挺有趣的,网上也能找到很多精彩的解析,而且自己也会不断成长。以后会不断修改我这个学习总结的。)

点评:
750:“题题看不懂,道道没想法”,哈哈,我已经对号入座了--
680:“最近一直对算法的学习感到很迷茫,总是找不方向”,我也是这种感觉,然后就报名训练营了。
205:“3分学习7分练”,这一点我也要记住。
100:好像是个大神,源码分析很棒。
710:“时间复杂度为O(logn)的推算逻辑比较难理解, 需要多复习”。O(logN)的我个人觉得都不是最基础的数据结构,有过“封装”,所以会比基础的更常用,确实要多复习多练习。
635:“时间不够、时间不够、还是时间不够。”有同感,不过还好我不用带娃,你加油--
280:总结写得很清晰,我觉得我写得就有点乱~
115:“明明知道这样的代码很烂但是自己却没有能力改进自己的代码”。很有同感。
495:这个总结也太棒了吧,点赞。

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.