收集整理了一份《2024年最新Python全套学习资料》免费送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Python知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来
如果你需要这些资料,可以添加V无偿获取:hxbc188 (备注666)
正文
- 组内插入排序,构成有序序列
- 下标差值=2/2=1,将剩余的元素插入排序
希尔排序代码实现
可以看看前面的插入排序,希尔排序
只是一个有步长的直接插入排序。
public void sort(int[] nums){
//下标差
int step=nums.length;
while (step>0){
//这个是可选的,也可以是3
step=step/2;
//分组进行插入排序
for (int i=0;i<step;i++){
//分组内的元素,从第二个开始
for (int j=i+step;j<nums.length;j+=step){
//要插入的元素
int value=nums[j];
int k;
for (k=j-step;k>=0;k-=step){
if (nums[k]>value){
//移动组内元素
nums[k+step]=nums[k];
}else {
break;
}
}
//插入元素
nums[k+step]=value;
}
}
}
}
希尔排序性能分析
- 稳定度分析
希尔排序是直接插入排序的变形,但是和直接插入排序不同,它进行了分组,所以不同组的相同元素的相对位置可能会发生改变,所以它是不稳定的。
- 时间复杂度分析
希尔排序的时间复杂度跟增量序列的选择有关,范围为 O(n^(1.3-2)) 在此之前的排序算法时间复杂度基本都是 O(n²),希尔排序是突破这个时间复杂度的第一批算法之一。
算法名称 | 时间复杂度 | 空间复杂度 | 是否稳定 |
---|---|---|---|
希尔排序 | O(n^(1.3-2)) | O(1) | 不稳定 |
归并排序
归并排序原理
归并排序是建立在归并操作上的一种有效的排序算法,归并,就是合并的意思,在数据结构上的定义就是把把若干个有序序列合并成一个新的有序序列
。
归并排序是分治法的典型应用,分治什么意思呢?就是把一个大的问题分解成若干个小的问题来解决。
归并排序的步骤,是把一个数组切分成两个,接着递归,一直到单个元素,然后再合并,单个元素合并成小数组,小数组合并成大数组。
动图如下(来源参考[4]):
我们以数组[2,5,6,1,7,3,8,4]
为例,来看一下归并排序的过程:
拆分就不用多讲了,我们看看怎么合并
。
归并排序代码实现
这里使用递归来实现归并排序:
- 递归终止条件
递归起始位置小于终止位置
- 递归返回结果
直接对传入的数组序列进行排序,所以无返回值
- 递归逻辑
将当前数组分成两组,分别分解两组,最后归并
代码如下:
public class MergeSort {
public void sortArray(int[] nums) {
sort(nums, 0, nums.length - 1);
}
/\*\*
\* 归并排序
\*
\* @param nums
\* @param left
\* @param right
\*/
public void sort(int[] nums, int left, int right) {
//递归结束条件
if (left >= right) {
return;
}
int mid = left + (right - left) / 2;
//递归当前序列左半部分
sort(nums, left, mid);
//递归当前序列右半部分
sort(nums, mid + 1, right);
//归并结果
merge(nums, left, mid, right);
}
/\*\*
\* 归并
\*
\* @param arr
\* @param left
\* @param mid
\* @param right
\*/
public void merge(int[] arr, int left, int mid, int right) {
int[] tempArr = new int[right - left + 1];
//左边首位下标和右边首位下标
int l = left, r = mid + 1;
int index = 0;
//把较小的数先移到新数组中
while (l <= mid && r <= right) {
if (arr[l] <= arr[r]) {
tempArr[index++] = arr[l++];
} else {
tempArr[index++] = arr[r++];
}
}
//把左边数组剩余的数移入数组
while (l <= mid) {
tempArr[index++] = arr[l++];
}
//把右边剩余的数移入数组
while (r <= right) {
tempArr[index++] = arr[r++];
}
//将新数组的值赋给原数组
for (int i = 0; i < tempArr.length; i++) {
arr[i+left] = tempArr[i];
}
}
}
归并排序性能分析
- 时间复杂度
一趟归并,我们需要把遍历待排序序列遍历一遍,时间复杂度O(n)。
而归并排序的过程,需要把数组不断二分,这个时间复杂度是O(logn)。
所以归并排序的时间复杂度是O(nlogn)。
- 空间复杂度
使用了一个临时数组来存储合并的元素,空间复杂度O(n)。
- 稳定性
归并排序是一种稳定的排序方法。
算法名称 | 最好时间复杂度 | 最坏时间复杂度 | 平均时间复杂度 | 空间复杂度 | 是否稳定 |
---|---|---|---|---|---|
归并排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(n) | 稳定 |
快速排序
快速排序原理
快速排序是面试最高频的排序算法。
快速排序和上面的归并排序一样,都是基于分治思想的,大概过程:
- 选出一个基准数,基准值一般取序列最左边的元素
- 重新排序序列,比基准值小的放在基准值左边,比基准值大的放在基准值右边,这就是所谓的分区
快速排序动图如下:
我们来看一个完整的快速排序图示:
快速排序代码实现
单边扫描快速排序
选择一个数作为基准数pivot,同时设定一个标记 mark 代表左边序列最右侧的下标位置,接下来遍历数组,如果元素大于基准值,无操作,继续遍历,如果元素小于基准值,则把 mark + 1 ,再将 mark 所在位置的元素和遍历到的元素交换位置,mark 这个位置存储的是比基准值小的数据,当遍历结束后,将基准值与 mark 所在元素交换位置。
public class QuickSort0 {
public void sort(int[] nums) {
quickSort(nums, 0, nums.length - 1);
}
public void quickSort(int[] nums, int left, int right) {
//结束条件
if (left >= right) {
return;
}
//分区
int partitonIndex = partion(nums, left, right);
//递归左分区
quickSort(nums, left, partitonIndex - 1);
//递归右分区
quickSort(nums, partitonIndex + 1, right);
}
//分区
public int partion(int[] nums, int left, int right) {
//基准值
int pivot = nums[left];
//mark标记初始下标
int mark = left;
for (int i = left + 1; i <= right; i++) {
if (nums[i] < pivot) {
//小于基准值,则mark+1,并交换位置
mark++;
int temp = nums[mark];
nums[mark] = nums[i];
nums[i] = temp;
}
}
//基准值与mark对应元素调换位置
nums[left] = nums[mark];
nums[mark] = pivot;
return mark;
}
}
双边扫描快速排序
还有另外一种双边扫描的做法。
选择一个数作为基准值,然后从数组左右两边进行扫描,先从左往右找到一个大于基准值的元素,将它填入到right指针位置,然后转到从右往左扫描,找到一个小于基准值的元素,将他填入到left指针位置。
public class QuickSort1 {
public int[] sort(int[] nums) {
quickSort(nums, 0, nums.length - 1);
return nums;
}
public void quickSort(int[] nums, int low, int high) {
if (low < high) {
int index = partition(nums, low, high);
quickSort(nums, low, index - 1);
quickSort(nums, index + 1, high);
}
}
public int partition(int[] nums, int left, int right) {
//基准值
int pivot = nums[left];
while (left < right) {
//从右往左扫描
while (left < right && nums[right] >= pivot) {
right--;
}
//找到第一个比pivot小的元素
if (left < right) nums[left] = nums[right];
//从左往右扫描
while (left < right && nums[left] <= pivot) {
left++;
}
//找到第一个比pivot大的元素
if (left < right) nums[right] = nums[left];
}
//基准数放到合适的位置
nums[left] = pivot;
return left;
}
}
快速排序性能分析
- 时间复杂度
快速排序的时间复杂度和归并排序一样,都是O(nlogn),但是这是最优的情况,也就是每次都能把数组切分到两个差不多大小的子数组。
如果出现极端情况,例如一个有序的序列[5,4,3,2,1] ,选基准值为5,那么需要切分n-1次才能完成整个快速排序的过程,这种情况时间复杂度就退化到了O(n²)。
- 空间复杂度
快速排序是一种原地排序的算法,空间复杂度是O(1)。
- 稳定性
快排的比较和交换是跳跃进行的,所以快排是一种不稳定的排序算法。
算法名称 | 最好时间复杂度 | 最坏时间复杂度 | 平均时间复杂度 | 空间复杂度 | 是否稳定 |
---|---|---|---|---|---|
快速排序 | O(nlogn) | O(n²) | O(nlogn) | O(1) | 不稳定 |
堆排序
堆排序原理
还记得我们前面的简单选择排序吗?堆排序是简单选择排序的一种升级版。
在学习堆排序之前,什么是堆呢?
完全二叉树是堆的一个比较经典的堆实现。
我们先来了解一下什么是完全二叉树。
简答说,如果节点不满,那它不满的部分只能在最后一层的右侧。
我们来看几个示例。
相信看了这几个示例,就清楚什么是完全二叉树
,什么是非完全二叉树
。
那堆
又是什么呢?
- 必须是完全二叉树
- 任一节点的值必须是其子树的最大值或最小值
- 最大值时,称为“最大堆”,也称大顶堆;
- 最小值时,称为“最小堆”,也称小顶堆。
因为堆是完全二叉树,所以堆可以用数组存储。
按层来将元素存储到数组对应位置,从下标1开始存储,可以省略一些计算。
好了,我们现在对堆已经有一些了解了,我们来看一下堆排序是什么样的呢?[2]
- 建立一个大顶堆
- 将堆顶元素(最大值)插入数组末尾
- 让新的最大元素上浮到堆顶
- 重复过程,直到排序完成
动图如下(来源参考[1]):
关于建堆,有两种方式,一种是上浮,一种是下沉。
上浮是什么呢?就是把子节点一层层上浮到合适的位置。
下沉是什么呢?就是把堆顶元素一层层下沉到合适的位置。
上面的动图,使用的就是下沉的方式。
堆排序代码实现
public class HeapSort {
public void sort(int[] nums) {
int len = nums.length;
//建堆
buildHeap(nums, len);
for (int i = len - 1; i > 0; i--) {
//将堆顶元素和堆末元素调换
swap(nums, 0, i);
//数组计数长度减1,隐藏堆尾元素
len--;
//将堆顶元素下沉,使最大的元素浮到堆顶来
sink(nums, 0, len);
}
}
/\*\*
\* 建堆
\*
\* @param nums
\* @param len
\*/
public void buildHeap(int[] nums, int len) {
for (int i = len / 2; i >= 1; i--) {
//下沉
sink(nums, i, len);
}
}
/\*\*
\* 下沉操作
\*
\* @param nums
\* @param index
\* @param end
\*/
public void sink(int[] nums, int index, int end) {
//左子节点下标
int leftChild = 2 \* index + 1;
//右子节点下标
int rightChild = 2 \* index + 2;
//要调整的节点下标
int current = index;
//下沉左子树
if (leftChild < end && nums[leftChild] > nums[current]) {
current = leftChild;
}
//下沉右子树
if (rightChild < end && nums[rightChild] > nums[current]) {
current = rightChild;
}
//如果下标不相等,证明调换过了
if (current!=index){
//交换值
swap(nums,index,current);
//继续下沉
sink(nums,current,end);
}
}
public void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}
堆排序性能分析
- 时间复杂度
堆排的时间复杂度和快排的时间复杂度一样,都是O(nlogn)。
- 空间复杂度
堆排没有引入新的空间,原地排序,空间复杂度O(1)。
- 稳定性
堆顶的元素不断下沉,交换,会改变相同元素的相对位置,所以堆排是不稳定的。
算法名称 | 时间复杂度 | 空间复杂度 | 是否稳定 |
---|---|---|---|
堆排序 | O(nlogn) | O(1) | 不稳定 |
计数排序
文章开始我们说了,排序分为比较类和非比较类,计数排序是一种非比较类的排序方法。
计数排序是一种线性时间复杂度的排序,利用空间来换时间,我们来看看计数排序是怎么实现的吧。
计数排序原理
计数排序的大致过程[4]:
- 找出待排序的数组中最大和最小的元素
- 统计数组中每个值为i的元素出现的次数,存入数组arr的第i项;
- 对所有的计数累加(从arr中的第一个元素开始,每一项和前一项相加);
- 反向填充目标数组:将每个元素i放在新数组的第arr(i)项,每放一个元素就将arr(i)减去1。
我们看一下动图演示(来自参考[4]):
我们拿一个数组来看一下完整过程:[6,8,5,1,2,2,3]
- 首先,找到数组中最大的数,也就是8,创建一个最大下标为8的空数组arr
- 遍历数据,将数据的出现次数填入arr对应的下标位置中
- 然后输出数组元素的下标值,元素的值是几,就输出几次
计数排序代码实现
public class CountSort {
public void sort(int[] nums) {
//查找最大值
int max = findMax(nums);
//创建统计次数新数组
int[] countNums = new int[max + 1];
//将nums元素出现次数存入对应下标
for (int i = 0; i < nums.length; i++) {
int num = nums[i];
countNums[num]++;
nums[i] = 0;
}
//排序
int index = 0;
for (int i = 0; i < countNums.length; i++) {
while (countNums[i] > 0) {
nums[index++] = i;
countNums[i]--;
}
}
}
public int findMax(int[] nums) {
int max = nums[0];
for (int i = 0; i < nums.length; i++) {
if (nums[i] > max) {
max = nums[i];
}
}
return max;
}
}
OK,乍一看没啥问题,但是仔细想想,其实还是有些毛病的,毛病在哪呢?
- 如果我们要排序的元素有0怎么办呢?例如
[0,0,5,2,1,3,4]
,arr初始都为0,怎么排呢?
这个很难解决,有一种办法,就是计数的时候原数组先加10,[-1,0,2],排序写回去的时候再减,但是如果刚好碰到有-10这个元素就凉凉。
- 如果元素的范围很大呢?例如
[9992,9998,9993,9999]
,那我们申请一个10000个元素的数组吗?
这个可以用偏移量解决,找到最大和最小的元素,计算偏移量,例如[9992,9998,9993,9999]
,偏移量=9999-9992=7,我们只需要建立一个容量为8的数组就可以了。
解决第二个问题的版本如下:
public class CountSort1 {
public void sort(int[] nums) {
//查找最大值
int max = findMax(nums);
//寻找最小值
int min = findMin(nums);
//偏移量
int gap = max - min;
//创建统计次数新数组
int[] countNums = new int[gap + 1];
//将nums元素出现次数存入对应下标
for (int i = 0; i < nums.length; i++) {
int num = nums[i];
countNums[num - min]++;
nums[i] = 0;
}
//排序
int index = 0;
for (int i = 0; i < countNums.length; i++) {
while (countNums[i] > 0) {
nums[index++] = min + i;
countNums[i]--;
}
}
}
public int findMax(int[] nums) {
int max = nums[0];
for (int i = 0; i < nums.length; i++) {
if (nums[i] > max) {
max = nums[i];
}
}
return max;
}
public int findMin(int[] nums) {
int min = nums[0];
for (int i = 0; i < nums.length; i++) {
if (nums[i] < min) {
min = nums[i];
}
}
return min;
}
}
计数排序性能分析
- 时间复杂度
我们整体运算次数是n+n+n+k=3n+k,所以使劲复杂度是O(n+k)。
- 空间复杂度
引入了辅助数组,空间复杂度O(n)。
- 稳定性
我们的实现实际上是不稳定的,但是计数排序是有稳定的实现的,可以查看参考[1]。
同时我们通过实现也发现,计数排序实际上不适合有负数的,元素偏移值过大的数组。
桶排序
桶数组可以看做计数排序的升级版,它把元素分到若干个桶
中,每个桶中的元素再单独排序。
桶排序原理
桶排序大概的过程:
- 设置一个定量的数组当作空桶;
- 遍历输入数据,并且把元素一个一个放到对应的桶里去;
- 对每个不是空的桶进行排序;
- 从不是空的桶里把排好序的数据拼接起来。
桶排序动图如下(动图来源参考[1]):
我们上面说了,计数排序不适合偏移量过大的数组,我们拿一个偏移量非常大的数组[2066,566,166,66,1066,2566,1566]
为例,来看看桶排序的过程。
- 创建6个桶,分别存储0-500,500-1000,1000-1500,1500-2000,2000-2500,2500-3000的元素
- 遍历数组,将元素分别分配到对应的桶中
- 桶中元素排序,这里我们明显只用排序第一个桶
- 将桶中的元素依次取出,取出的元素就是有序的了
桶排序代码实现
桶排序的实现我们要考虑几个问题:
- 桶该如何表示?
- 桶的数量怎么确定?
- 桶内排序用什么方法?
我们来看一下代码:
public class BucketSort {
public void sort(int[] nums) {
int len = nums.length;
int max = nums[0];
int min = nums[0];
//获取最大值和最小值
for (int i = 1; i < len; i++) {
if (nums[i] > max) {
max = nums[i];
}
if (nums[i] < min) {
min = nums[i];
}
}
//计算步长
int gap = max - min;
//使用列表作为桶
List<List<Integer>> buckets = new ArrayList<>();
//初始化桶
for (int i = 0; i < gap; i++) {
buckets.add(new ArrayList<>());
}
//确定桶的存储区间
int section = gap / len - 1;
//数组入桶
for (int i = 0; i < len; i++) {
//判断元素应该入哪个桶
int index = nums[i] / section - 1;
if (index < 0) {
index = 0;
}
//对应的桶添加元素
buckets.get(index).add(nums[i]);
}
//对桶内的元素排序
for (int i = 0; i < buckets.size(); i++) {
//这个底层调用的是 Arrays.sort
// 这个api不同情况下可能使用三种排序:插入排序,快速排序,归并排序,具体看参考[5]
Collections.sort(buckets.get(i));
}
//将桶内的元素写入原数组
int index = 0;
for (List<Integer> bucket : buckets) {
for (Integer num : bucket) {
nums[index] = num;
index++;
}
}
}
}
桶排序性能分析
- 时间复杂度
桶排序最好的情况,就是元素均匀分配到了每个桶,时间复杂度O(n),最坏情况,是所有元素都分配到一个桶中,时间复杂度是O(n²)。平均的时间复杂度和技术排序一样,都是O(n+k)。
- 空间复杂度
桶排序,需要存储n个额外的桶,桶中又要存储k个元素,所以空间复杂度是O(n+k)。
- 稳定性
稳定性得看桶中排序用的什么排序算法,桶中用的稳定排序算法,那么就是稳定的。用的不稳定的排序算法,那么就是不稳定的。
如果你也是看准了Python,想自学Python,在这里为大家准备了丰厚的免费学习大礼包,带大家一起学习,给大家剖析Python兼职、就业行情前景的这些事儿。
一、Python所有方向的学习路线
Python所有方向路线就是把Python常用的技术点做整理,形成各个领域的知识点汇总,它的用处就在于,你可以按照上面的知识点去找对应的学习资源,保证自己学得较为全面。
二、学习软件
工欲善其必先利其器。学习Python常用的开发软件都在这里了,给大家节省了很多时间。
三、全套PDF电子书
书籍的好处就在于权威和体系健全,刚开始学习的时候你可以只看视频或者听某个人讲课,但等你学完之后,你觉得你掌握了,这时候建议还是得去看一下书籍,看权威技术书籍也是每个程序员必经之路。
四、入门学习视频
我们在看视频学习的时候,不能光动眼动脑不动手,比较科学的学习方法是在理解之后运用它们,这时候练手项目就很适合了。
四、实战案例
光学理论是没用的,要学会跟着一起敲,要动手实操,才能将自己的所学运用到实际当中去,这时候可以搞点实战案例来学习。
五、面试资料
我们学习Python必然是为了找到高薪的工作,下面这些面试题是来自阿里、腾讯、字节等一线互联网大厂最新的面试资料,并且有阿里大佬给出了权威的解答,刷完这一套面试资料相信大家都能找到满意的工作。
成为一个Python程序员专家或许需要花费数年时间,但是打下坚实的基础只要几周就可以,如果你按照我提供的学习路线以及资料有意识地去实践,你就有很大可能成功!
最后祝你好运!!!
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
如果你需要这些资料,可以添加V无偿获取:hxbc188 (备注666)
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
977b313558a11d3c13e43.png)
四、入门学习视频
我们在看视频学习的时候,不能光动眼动脑不动手,比较科学的学习方法是在理解之后运用它们,这时候练手项目就很适合了。
四、实战案例
光学理论是没用的,要学会跟着一起敲,要动手实操,才能将自己的所学运用到实际当中去,这时候可以搞点实战案例来学习。
五、面试资料
我们学习Python必然是为了找到高薪的工作,下面这些面试题是来自阿里、腾讯、字节等一线互联网大厂最新的面试资料,并且有阿里大佬给出了权威的解答,刷完这一套面试资料相信大家都能找到满意的工作。
成为一个Python程序员专家或许需要花费数年时间,但是打下坚实的基础只要几周就可以,如果你按照我提供的学习路线以及资料有意识地去实践,你就有很大可能成功!
最后祝你好运!!!
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
如果你需要这些资料,可以添加V无偿获取:hxbc188 (备注666)
[外链图片转存中…(img-ZWz06WX7-1713846522281)]
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!