2024年1月

排序算法是一种通过特定的算法因式将一组或多组数据按照既定模式进行重新排序的方法。通过排序,我们可以得到一个新的序列,该序列遵循一定的规则并展现出一定的规律。经过排序处理后的数据可以更方便地进行筛选和计算,从而大大提高了计算效率。因此,掌握排序算法是每个程序员的基本功之一。

今天我们将详细讲解一些与冒泡排序、快速排序和插入排序相关的leetcode真题。

冒泡排序

字如其名,冒泡排序是一种算法,它类似于水中的泡泡逐渐上升,通过逐轮比较和交换,最终使每个元素按照顺序排列。

看一下今天的题目:给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。请注意 ,必须在不复制数组的情况下原地对数组进行操作。进阶:你能尽量减少完成的操作次数吗?

示例 1:
  输入: nums = [0,1,0,3,12]
  输出: [1,3,12,0,0]

首先,这道题属于简单题目,一眼基本就可以看出来如何实现。只要遇到零就往后移动,并且冒泡排序最常见的就是双层for循环即可,然后维护两个变量随时进行数据交换。但是这种都知道的解决方案,我们不去实现。我们来实现一下进阶要求,即尽可能减少操作次数来完成数据的转换。

我们可以考虑一下,因为nums数组中不一定只有一个零,所以在操作数据时,我们将所有零都看作一个整体,只移动第一个零即可,其他的零都不用动。下面是图解:

image

我们在这里写一下相关的Python实现代码。在这段代码中,我们使用了三元表达式的一种变种:(假,真)[条件]

from typing import List
class Solution:
    def moveZeroes(self, nums: List[int]) -> None:
        """
        Do not return anything, modify nums in-place instead.
        """
        i = 0
        j = 0
        while j < len(nums) :
            if nums[j] == 0 :
                i = (j,i)[nums[i] == 0]
            elif  nums[i] == 0 :
                nums[i] = nums[j]
                nums[j] = 0
                i = i + 1
            j = j + 1
solution = Solution()
nums = [0,1,0,3,12]
solution.moveZeroes(nums)
print(nums)

快速排序

快速排序的重要之处在于选择合适的标准数字,通过将大于和小于该数字的元素分成两部分。随后,我们不断重复这个操作。通常情况下,我倾向于使用递归方法,每次分割完后再调用以基准数字为标准的方法,直到只剩下一个元素为止。在今天的例题中,我们将探讨快速排序的应用。

给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。你必须设计并实现时间复杂度为 O(n) 的算法解决此问题。

通常情况下,快速排序的时间复杂度是无法达到O(n)的,而且在最坏情况下可能会达到O(n2)的时间复杂度。因此,为了满足题目要求,我们需要对选择排序进行一些改进。

我们先来看下简单实现:

from typing import List
class Solution:
    def findKthLargest(self, nums: List[int], k: int) -> int:
        return sorted(nums)[len(nums) - k]
        
solution = Solution()
nums = [3,2,3,1,2,4,5,5,6]
k = 4
print(solution.findKthLargest(nums, k))        

然而,这样的实现肯定无法满足O(n)的时间复杂度要求。因此,我们需要进行进一步优化。如果我第一反应是降低时间复杂度,我可能会考虑牺牲空间复杂度,然后逐步进行优化。根据这个,我写出来了递归。

from typing import List

class Solution:
    def findKthLargest(self, nums: List[int], k: int) -> int:
        def quickSelect(nums,k) -> int:
            # 选择一个作为基准
            result = nums[0]
            # 将数组分为三部分,大于、等于、小于
            big ,equal,small = [],[],[]
            # for循环不要排序,一进行排序就会增加时间复杂度。
            for num in nums:
                if num > result:
                    big.append(num)
                elif num < result:
                    small.append(num)
                else :
                    equal.append(num)
            # 说明在big中
            if k <= len(big):
                return quickSelect(big,k)
            if k > len(big) + len(equal):
                return quickSelect(small,k - len(big) - len(equal))
            return result

        return quickSelect(nums,k)

solution = Solution()
nums = [3,2,3,1,2,4,5,5,6]
k = 4
print(solution.findKthLargest(nums, k))

为了更加方便大家理解,我还特意制作了一个简单的图例,以便于更直观地说明。

image

插入排序

插入排序(Insertion Sort)是一种简单直观的排序算法。其基本思想是将待排序序列分为已排序和未排序两部分。每次从未排序部分取出一个元素,将其插入到已排序部分的合适位置,直到所有元素都被插入到已排序部分为止。这种排序方法相对其他复杂的排序算法而言,实现简单、容易理解,适用于小规模数据的排序。

我们来看下正题:

给你一个整数数组 nums,请你将该数组升序排列。

输入:nums = [5,1,1,2,0,0]
输出:[0,0,1,1,2,5]

我们先来简单实现一个插入排序:

from typing import List

class Solution:

    def insertSort(self,index: int,nums: List[int]):
        temp = nums[index]
        while index > 0 and temp < nums[index-1] :
            nums[index] =  nums[index-1]
            index = index - 1
        nums[index] = temp

    def sortArray(self, nums: List[int]) -> List[int]:
        for i in range(1,len(nums)):
            self.insertSort(i,nums)
        return nums
   
insert = Solution();
nums = [5,1,1,2,0,0]
print(insert.sortArray(nums))

为了更好地说明,我简单地绘制了一个图例。请注意,数字的顺序应根据实际情况而定,而不是根据图中的显示顺序来确定。图中主要展示了交换和比较的过程。

image

然而,插入排序明显不是最优的算法,因为它的运行时间超过了限制。主要原因是插入排序的时间复杂度仍然偏高。那么,我来提出一种简单的优化方法,主要是减少比较和交换操作的消耗。我们知道,如果数组的前面部分已经是有序的,那么我们可以首先考虑使用二分查找来减少比较次数。我们来实现一下:

from typing import List

class Solution:

    def findIndex(self,begin:int, end: int,temp: int,nums: List[int]):
        if begin <= end :
            return begin
        mid = (begin + end ) / 2
        if nums[mid] > temp:
            findIndex(begin,mid,temp,nums)
        else :
            findIndex(mid,end,temp,nums)   

    def insertSort2(self,index: int,nums: List[int]):
        temp = nums[index]
        small = self.findIndex(0,index,temp,nums)
        while index > small and temp < nums[index-1] :
            nums[index] =  nums[index-1]
            index = index - 1
        nums[index] = temp

    def sortArray(self, nums: List[int]) -> List[int]:
        for i in range(1,len(nums)):
            self.insertSort2(i,nums)
        return nums
   
insert = Solution();
nums = [5,1,1,2,0,0]
print(insert.sortArray(nums))

本来我觉得这次的任务应该能够顺利通过,但是却没能满足时间限制要求,结果还是超时了。接下来,为了优化交换次数,我需要考虑使用插入排序的高级变体——希尔排序。

希尔排序

希尔排序是一种优化的插入排序算法,它的重点是通过增加间隔来减少元素之间的比较次数。相比于传统的插入排序一次比较一个元素,希尔排序通过间隔的设定,可以一次比较多个元素。为了更好地理解这个过程,我简单地画了一张图。

image

如果采用之前的插入排序算法,将数字0移动到前面将需要进行多次比较和交换操作,这将导致效率较低。而如果使用希尔排序并增加间隔,可以避免对中间数字进行比较和交换操作,从而有效减少了所需的比较和交换次数。

我们来简单实现一下算法:

from typing import List

class Solution:

    def insertSort3(self,index: int,nums: List[int],gap: int):
        temp = nums[index]
        while index-gap >= 0 and temp < nums[index-gap] :
            nums[index] =  nums[index-gap]
            index = index - gap
        nums[index] = temp

    def sortArray(self, nums: List[int]) -> List[int]:
        gap = len(nums) // 2
        while gap > 1 :
            for i in range(gap,len(nums)):
                self.insertSort3(i,nums,gap)
            gap = gap // 2
        return nums
insert = Solution();
nums = [5,1,1,2,0,0]
print(insert.sortArray(nums))

终于通过了这道题目,从代码实现上来看,与插入排序相比,它多了一些变量,但仍然很容易理解。然而,由于数组前面不再是绝对有序的,我们需要放弃使用二分查找。

@

前言

这篇文章,其实在一年之前的时候就已经写好了。当时是在公司内部分享的,作为一个监控框架。当时是想着过一段时间之后,分享到技术论坛上面的,没想到计划赶不上变化,过完国庆被裁了

当时忙着找工作,就一直没有更新了,放在笔记里面吃灰

最近,发现好久没有分享技术文章了,从笔记里面找了一下,就拿来分享了

在项目开发中,会有很多第三方依赖,通过 gradle 引入进来的。比如 androidxDesignVersion、androidxSupportVersion、 rxjava2Version、 okhttpVersion 等第三方库。有时候第三方库改到了或者升级了,我们并不能及时发现,往往需要等到出问题的时候,去排查的时候,才发现是某个依赖版本改动导致的。

这时候其实是有点晚了,如果能够提前暴露,那么我们能够大大地减少风险,因此我们希望能够监控起来。

在这里插入图片描述

基本原理

  1. 代码 merge 到 dev 分支的时候,借助 gitlab ci,促发 gradle task 任务,自动分析 dependency 链表
  2. 对比上一次打包的 dependency 链表,如果发现变更了,会通过 机器人进行通知。并附上最新的 commit,提交作者信息,需要 author 确认一下

执行流程

目前主要对 dev 分支进行监控,以下几种场景会促发 diff 检查

  • MR 合并进 dev 分支的时候
  • 在 dev 分支直接提交代码的时候

img

diff 报告

diff 报告主要包括以下几种信息

  • 作者,当前 commitId 的 author
  • branch 分支名
  • commitId 当前的 commitId, baseCommitId:基准 id
  • 变动依赖,这里最多显示 6 行,超过会截断,具体变动可以见
    详情
  • 提交:如果是 MR 合并进来的,会显示 MR 链接,否则,会显示 commit 链接

不同分支 merge 过来的 diff 报告

检测到  Dependency 变化
分支: 573029_test
作者: 徐俊
commitId: 4844590b     baseCommitId: bed4cb64
变动依赖: 
+\--- project :component-matrix
+     \--- com.google.code.gson:gson:2.8.2 -> 2.8.9
详情: {url}
提交:{url}/merge_requests/4425/diffs

同个分支产生的 merge 报告

检测到 Dependency 变化
分支: 573029_dep_diff
作者: xujun
commitId: 16145365     baseCommitId: 4844590b
变动依赖: 
+\--- project :component-matrix
+     \--- com.squareup.retrofit2:converter-gson:2.4.0 (*)
详情: {url}
提交: {url)/commit/16145365

同个分支提交的 diff 报告

检测到  Dependency 变化
分支: 573029_dep_diff
作者: xujun
commitId: 19f22516     baseCommitId: 8c90d512
变动依赖: 
+\--- project :component-tcpcache
+     \--- com.google.code.gson:gson:2.8.2 -> 2.8.9
详情: {url}
提交: {url)/commit/16145365

我们主要讲述以下几点

  • 我们需要监控怎样的 Dendenpency 变化
  • 怎样获取 dependency Tree
  • dependency Tree 怎样做 diff
  • 如何找到基准点,进行 diff 计算
  • 怎样结合 CI 进行计算

具体实现原理

我们需要监控怎样的 Dendenpency 变化

众所周知,Android 的 Dependency 是通过 gradle 进行配置的,如果我们在 build.gradle 下面配置了这样,证明了我们依赖 recyclerview 这个库。

dependencies {
    implementation androidx.recyclerview:recyclerview:1.1.0 ”
}

那一行代码会给我们的 Dendenpency 带来怎样的变化呢?

有人说,它是新增了 recyclerview 这个库。

这个说法对嘛?

不全对。

因为 gradle 依赖默认是有传递性的。他还会同时引入 recyclerview 自身所依赖的库。

+--- androidx.recyclerview:recyclerview:1.1.0
|    +--- androidx.annotation:annotation:1.1.0 
|    +--- androidx.core:core:1.1.0 
|    +--- androidx.customview:customview:1.1.0
|    \--- androidx.collection:collection:1.0.0 -> 1.1.0 (*)
  1. 如果项目当中当前没有这些库的,会同时导入这些库。
  2. 如果项目中有这些库了,库的版本比较低,会升级到相应的版本。比如 collection 会从 1.0.0 升级到 1.1.0

然而这些情况就是我们往往所忽略的,
即使有代码 review,有时候也会漏了。即使 review 待了,可能下意识也只以为只引入了这个库,却很难看到它背后的变化

而这些如果带到线上去,有时候会发生一些难以预测的结果,因此,我们需要有专门的手段来监控这些变化。能够监测到整条链路的变化,而不仅仅只是
implementation androidx.recyclerview:recyclerview:1.1.0 ”
这行代码的变化

至于如果依赖的传递性,可以通过
transitive

exclude
等用法做到。 可以看这些文章,这里不再一一展开。

解决 Android Gradle 依赖的各种版本问题

build.gradle管理依赖的版本(传递(transitive)\排除(exclude)\强制(force)\动态版本(+))

怎样获取 dependency Tree

获取 dependency Tree 的话,有多种方式

  1. 通过
    project.configurations
    这种方式获取
  2. 通过
    gradlew :app:dependencies
    task
  3. 通过
    AsciiDependencyReportRenderer
    获取,需要适配不同版本的 gradle 版本

project.configurations
方式

通过这种方式获取的,他是能够获取到所有的 dependencies,但是并不能看到 dependencies 的树形关系。

伪代码如下

        def configuration = project.configurations.getByName("debugCompileClasspath")
        configuration.resolvedConfiguration.lenientConfiguration.allModuleDependencies.each {
            def identifer = it.module.id
            depList.add(identifer)
        }

./gradlew dependencies

./gradlew dependencies 会输出所有 configuration 的 Dependcency Tree。包括 testDebugImplementation、testDebugProvided、testDebugRuntimeOnly 等等

事实上,我们只关心打进 APK 包里面的 dependencies。因此我们可以指定更详细的 configuration 。即

gradlew :app:dependencies --configuration releaseRuntimeClasspath

这样,就只会输出 Release 包 runtimeClasspath 相关的东西。

RuntimeClasspath 跟我们常用的 implementation,关系大概如下

在输出的 dependencies tree 报告中,我们看到的格式一般是这样的

** 这里有几个格式需要说明一下**

  • x.x.x (*), 比如图中的 4.2.2(*), 该依赖已经有了,将不再重复依赖,
  • x.x.x -> x.x.x 该依赖的版本被箭头所指的版本代替
  • x.x.x -> x.x.x(*) 该依赖的版本被箭头所指的版本代替,并且该依赖已经有了,不再重复依赖

AsciiDependencyReportRenderer

AsciiDependencyReportRenderer 这个东东,在不同的 gradle 版本有不同的差异,需要适配一下。

如果要这种方案,建议将某个版本的代码剥离出来,伪代码一般如下,单独集成一个库。

project.afterEvaluate {
   Log.i(TAG, "afterEvaluate")
   val renderer = AsciiDependencyReportRenderer()
   val sb = StringBuilder()
   val f = StreamingStyledTextOutputFactory(sb)
   renderer.setOutput(f.create(javaClass, LogLevel.INFO))
   val projectDetails = ProjectDetails.of(project)
   renderer.startProject(projectDetails)
   // sort all dependencies

   val configuration: org.gradle.api.artifacts.Configuration =
       project.configurations.getByName("releaseRuntimeClasspath")
   renderer.startConfiguration(configuration)
   renderer.render(configuration)
   renderer.completeConfiguration(configuration)
   // finish the whole processing
   renderer.completeProject(projectDetails)
   val textOutput = renderer.textOutput
   textOutput.println()
   Log.i(TAG, "end sb is $sb")

}

方案选择

从上面阐述可知,第一种方案
project.configurations
, 通过这种方式获取的,他是能够获取到所有的 dependencies,但是并不能看到 dependencies 的树形关系。

第二种方案
./gradlew dependencies
的优点是简单,直接采用 gradle 原生 Task,输出特定格式的文本。然后根据规律将所有的 dependency tree 提出出来。

可能有人担心
./gradlew dependencies
的输出格式会变化。

其实还好,看了几个 gradle 版本的输出格式,基本都是一样的。

第三种方

AsciiDependencyReportRenderer
的优点是可定制性高,缺点是麻烦,需要适配不同版本的 gradle。

最终我选择的方案是
方案二

怎样对 dependency Tree 进行 diff 计算

传统 diff 方案

可能很多人想到的方案是使用 Git diff 进行 diff 计算。但是这种方式有局限性。

  • 当有多个修改的时候,key -value 可能无法一一对应。
  • 他的 diff 类型 add、remove、 change 并不能一一对应我们 dependency add、remove、 change 的类型。

这无法达到我们想要的结果。因此,我们需要整合自己的 diff 算法。

自定义的 diff 方案

这里的方案是借鉴了 JakeWharton 大神的方案,在其基础之上进行了改造。

原理大概如下

  1. 分别计算当前,上一次的 dependency tree,用 Set<List
    > 储存,分别表示为 oldPaths,newPaths
  2. 接着根据 oldPaths 和 newPaths 计算出 removedTree, addedTree, changedTree
  3. 最后,根据 removedTree, addedTree 计算出 diff

第一步

对于这里的依赖,我们会使用
Set<List<String>>
的数据结构储存

转换之后的数据结构

这样的好处就是可以看到每一个 dependency 的全路径,如果 dependency 的全路径不一样,那么可以 diff 出来。

第二步
计算 remove 树 和 add 树

有了第一步的基础,其实很简单,直接调用 kotlin 的扩展方法
Set<T>.minus

如何找到一个基准点,进行 diff 计算

其实,这个说到底,就是找到上一个 commit 提交的 diff 文件。

  1. 看是不是 MR,如果是 MR,我们应该找到 MR 合并前的一个 commit
  2. 不是 MR 合并进来的,我们直接找到上一个 commit 即可

因此,我们可以借助 git 命令来处理。对于 merge request,目前主要有几种情况会产生 merge request。

  • 直接 MR 合并进来的,这时候 parent 会产生两个点,我们去 parent[0] 即可
  • 当前本地分支落后远程分支, 且 local 分支有 commit 的时候,pull 或者 push 的时候,会产生一个 merge 节点,这时候 parent 会产生两个点,我们去 parent[1] 即可

原理图如下:

怎样集合 Gialab CI 进行计算

Gialab push 或者 merge 的时候,我们需要感知到,接着执行特定的 task,进行计算。 每个公司的 CI 可能不太一样,具体可以修改一下

gradlew :{appName}:checkDepDiff

总结

dependency diff 监控的原理其实不难,主要是涉及到挺多方面的,有兴趣的可以看一下。
如果觉得对你有所帮助的话,希望可以一键三连。

参考文章

https://wajahatkarim.com/2020/03/gradle-dependency-tree/

https://tomgregory.com/gradle-dependency-tree/

https://github.com/jfrog/gradle-dep-tree

http://muydev.top/2018/08/21/Analyze-Android-Dependency-Tree/

本文由 CNCF 大使 Eric D. Schabell 撰写,预测2024年云原生领域最可能发生的3大变化,并与其对云原生可观测性领域的见解结合。

关注云原生倦怠

毫无疑问,在 2023 年中云原生可观测性领域的头号话题是
倦怠
。这涉及到每一个角色,从 SREs、DevOps、工程师、开发人员到管理企业内云原生工程体验的任何部分。他们都认为这是首要主题。

根据 2023 Cloud Native Observability Report 的研究,以下是其中的一些结果:

  • 工程师和开发人员平均每周花费 10 个小时来分流和了解事件,这相当于他们每周 40 小时工作时间的四分之一。
  • 88% 的人表示,耗在问题上的时间对他们和他们的职业生涯产生了负面影响。
  • 39% 的人承认他们经常感到压力过大。
  • 22%的人表明他们想辞职。

可见围绕云原生解决方案的使用、基础设施的管理和维护等问题,都仍在全球范围内,以及各类组织中继续
扩大对云原生资源的压力、压迫和由此产生的影响

2023 年中这一话题的关注点在主要集中在随叫随到的角色上,它将扩展并深入到企业努力发展云原生足迹的所有领域。而 2024 年,我们将
听到更多与职业倦怠相关的压力,听到更多关于如何解决这一问题的想法

更多的职业变动

当快速浏览一下所有组织的 IT 职位和保留率,就会发现
每年更换雇主的开发人员、工程师、DevOps、SRE 等人数相当多

这并不是说 2024 年会出现大规模裁员。更多的是云原生组织带来的压力、倦怠和承受水平。根据 Sterling 在2022年底发布的研究报告,科技人员的离职率为13.2%,超过了平均值为10.5%的所有其他行业。LinkedIn 的研究也发现,调查中的 “其他来源” 将科技行业的离职率推高到了 18.3%。

这些数字都关于云原生技术人员的职位,代表着
其中充满了挫折、压力和问题
。他们可能在 2024 年撞上最后一堵墙,并认定在当前的组织中无法获得快乐和参与感。可以预测的是,一股职业流动的浪潮——超过 25% 的现有技术人员将铤而走险,试图在新的角色和新的组织中找到成就感,并抓住新的机遇。

关注云原生成本

从 2019 年初首次启动,到 2020 年加入 Linux 基金会,再到未来,FinOps 基金会对所有云原生和使用云的组织都至关重要。在整个 2022 年和 2023 年,企业开始意识到他们需要从云原生服务中获取价值。

因此,FinOps 基金会已成为各类组织中 FinOps 从业人员的中心聚集地。他们通过培训和认证为从业人员提供支持,并即将发布 The FinOps Cost and Usage Specification(FOCUS)v1.0 版本,该版本与这一全新领域的开源社区方法保持一致。

因此,在云原生组织中看到的
FinOps 领域的持续增长有望在 2024 年演变为越来越多组织的永久增值
。首席信息官、首席财务官和首席技术官们也将在 2024 年更加倚重 FinOps 的角色、流程和教育,以管理他们的云原生支出,确保所花费的价值继续对他们的云原生投资产生影响。

展望 2024 年

以上就是对 2024年在云原生和可观测性领域的看法和预测。随着云原生时代的推进,倦怠现象的浮现,给技术人员带来了一系列挑战,而 FinOps 的兴起提供了一种在这场风暴中保持稳定的方法论。在2024年,如何有效应对倦怠、追求长期价值将成为企业的关键课题。
合理运用 FinOps 的精益理念,将助力企业在云原生领域找到工作与价值平衡的最佳实践,使得技术投入成为持续创新的引擎

前言

今天分享一款基于ApexCharts.js封装的、C#开源免费的Blazor图表库:Blazor-ApexCharts。

ApexCharts.js介绍

ApexCharts.js开源地址:
https://github.com/apexcharts/apexcharts.js

ApexCharts 是一个现代 JavaScript 图表库,可让您通过简单的 API 和 100 多个现成可用的示例来构建交互式数据可视化。ApexCharts 拥有您所期待的各种功能,它包含十多种图表类型,可在您的应用程序和仪表盘中提供美观、响应式可视化效果。ApexCharts 是获得 MIT 许可的开源项目,可用于商业和非商业项目。

项目源码下载运行

项目部分截图

项目源码地址



更多项目实用功能和特性欢迎前往项目开源地址查看