排序算法资料视频下载链接:https://pan.baidu.com/s/1i28Br5ztdnXbztcgk52Eag
提取码:sytm
C实现的数据结构与算法笔记:https://pan.baidu.com/s/1Wk5SCUQVSQik4em-AjKZ7g
14种模式解决面试算法编程题(PART I)
冒泡排序
1.内存循环中使用的是 if (arr[j] > arr[j + 1]){//交换} 来比较的话:
表示 前面比后面大,则交换,那么大的数往后移,每一轮比较完毕时,
便把“该轮比较中所有进行比较的数中的”最大值 放到了最尾部,也即作为了“已排好序的数列的”头部,最终结果为:小到大排序。
数列中分为两部分,前面一部分的数列为还没有排好序的数字,后面一部分的数列则为已经排好序的数字。
2.内存循环中使用的是 if (arr[j] < arr[j + 1]){//交换} 来比较的话:
表示前面比后面小,则交换,那么小的数往后移,每一轮比较完毕时,
便把“该轮比较中所有进行比较的数中的”最小值 放到了最尾部,也即作为了“已排好序的数列的”头部,最终结果为:大到小排序。
数列中分为两部分,前面一部分的数列为还没有排好序的数字,后面一部分的数列则为已经排好序的数字。
3.每轮比较中:前后两个数字两两比较,最终都会取出该轮比较中所有比较的数中的最大值/最小值,并把该最大值/最小值作为“已排好序的数列的”头部。
4.下面为冒泡排序第一轮比较的演示图:前后两两互相比较,前面比后面大的话,则交换,最终第一轮比较完毕时,便取出了所有数字中的最大值,
并把最大值放到了最末尾,作为“已排好序的数列的”头部。
从数列的头部开始往后移动,进行前后数字的两两比较,如果进行交换的话,即表示最大值/最小值往后移动,
相当于把每轮比较中获取出的最大值/最小值插入到“已排好序的数列的”头部,排序结果为 从小到大/从大到小
从数列的尾部开始往前移动,进行前后数字的两两比较,如果进行交换的话,即表示最大值/最小值往前移动,
相当于把每轮比较中获取出的最大值/最小值插入到“已排好序的数列的”尾部,排序结果为 从大到小/从小到大
当前演示的例子为:
从数列的头部开始往后移动,进行前后数字的两两比较,如果进行交换的话,即表示最大值/最小值往后移动,
相当于把每轮比较中获取出的最大值/最小值插入到“已排好序的数列的”头部,排序结果为 从小到大/从大到小
最重要的是循环条件的判断:
1.外层循环:比较轮数
1.外层循环:for (int i = 0; i < arr.length - 1; i++)
int i:比较轮数
arr.length:数组中元素个数 5
循环结束条件:i < arr.length - 1
2.i < arr.length - 1 的原因:
数组中元素个数n为5的话,那么元素个数n-1,比较轮数i 即为4;
因为每比较完一轮,就少一个需要进行比较的数,就多一个已排序好的数,
所以比较完4轮后,5个元素中4个已排好序,只剩1个元素就无需再比较了。
比较到第n轮,就有n个数已经排序好。
因为i = 0开始++,所以最后一轮比较条件为 3 < 5-1 ,到 i=4结束比较,而0~3一共4次比较轮数。
2.内层循环:每一轮中的比较次数,前后两两比较的次数
1.内层循环:for (int j = 0; j < arr.length - 1 - i; j++)
if (arr[j] > arr[j + 1]) 前面比后面大,则交换,最终结果为:小到大排序
if (arr[j] < arr[j + 1]) 前面比后面小,则交换,最终结果为:大到小排序
2.int j = 0:每轮比较中 都从第一个元素开始比较
3.j < arr.length - 1:
if (arr[j] > arr[j + 1]) :因为最大索引为4,arr[4]代表最后一个元素,arr[j + 1]中 j最大值只能为3,
那么 3 < 5 - 1 即为这一轮中最后两个数arr[3]>arr[4]进行比较
4.j < arr.length - 1 - i:
i表示比较到第几轮,之所以还要减i,因为比较到第n轮,就有n个数已经排序好。
减i的意思即为每轮比较中,都把已排序好的n个数排除不在进行比较。
简单版本:
int arr[] = { 1, 6, 0, -1, 9 };
int temp = 0;//临时变量
//外层循环:排序轮数
for (int i = 0; i < arr.length - 1; i++)
{
//内层循环:每一轮中比较次数
for (int j = 0; j < arr.length - 1 - i; j++)
{
//前面比后面大,则交换,最终结果为:小到大排序
if (arr[j] > arr[j + 1])
//前面比后面小,则交换,最终结果为:大到小排序
if (arr[j] < arr[j + 1])
{
//临时变量 加上 比较的两个数 进行交换
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
最优化版本:
优化一:使用flag来记录是否已经提前排序完毕,如果已经提前排序完毕,那后面即不在继续比较。
例如:数组元素为5,需要比较4轮,但是比较完第一轮后,全部数字都已经有序了,便无需再进行第二轮比较。
优化二:使用flag来记录当前这一轮比较中比较完毕时的最尾边界(最末的索引位置),那么表示最尾边界(最末的索引位置)后面的元素都是有序的了,
那么当进行下一轮比较时,只需要比较到上一轮比较中比较完毕时的最尾边界(最末的索引位置)即可。
例如:有数组中1000个数字,那么前面100个数字无序排列,后面900个数字有序排列,那么第一轮比较完毕后,
flag便记录着“只有进行了交换操作的”最尾边界(最末的索引位置)为100,下一轮比较时只需要比较到索引位置为100的位置即可。
int j, k;
int flag = n;//n:数组长度;flag:记录最后交换的位置,也就是每轮比较完毕时的最尾边界(最末的索引位置)
//1.flag不为0 表示上一轮比较中进行过两两元素交换,所以当前继续进行下一轮比较
//2.flag为0 上一轮比较中没有进行过两两元素交换,即表示后面全部元素为有序,无需再进行下一次比较
while (flag > 0)
{
k = flag; //使用k作为flag的备份,用来记录每轮比较完毕时的最尾边界(最末的索引位置)
//每轮比较前都把flag置为0,作用:只要没进行交换,flag便不会变仍为0,即表示后面全部元素为有序,无需再进行下一次比较
flag = 0;
//1.之所以使用j = 1,因为两两比较中使用的是a[j - 1] < a[j],当前元素和前一个元素比较
//2.之所以使用j < k,因为k记录着的正是“上一轮比较中的比较完毕时的”最尾边界(最末的索引位置),
// 所以这一轮比较中 只需要比较到“上一轮比较中的比较完毕时的”最尾边界(最末的索引位置) 即可。
for (j = 1; j < k; j++)
{
//前面比后面小,则交换,最终结果为:大到小排序
//if (a[j - 1] < a[j])
//前面比后面大,则交换,最终结果为:小到大排序
if (a[j - 1] > a[j])
{
//临时变量 加上 比较的两个数 进行交换
int temp;
temp = a[j - 1];
a[j - 1] = a[j];
a[j] = temp;
//表示进行交换过数据,同时记录当前这一轮比较中的比较完毕时的最尾边界(最末的索引位置)
flag = j;
}
}
}
选择排序
1.选择排序的 第一轮比较的 演示图:
第一轮比较结束时,便是在未排序的数列中的所有数字中,找出最小的数字作为“已排好序的数列的”头部,此时已排好序的数列只有一个数(最小值);
2.每轮排序情况:
1.第一轮比较结束时,便是在未排序的数列中的所有数字中,找出最小的数字/最大的数字作为“已排好序的数列的”头部,
此时已排好序的数列只有一个数(最大值/最小值);
2.每一轮比较中:
并不是前后两个数字两两比较,而是在一轮比较开始时,先把未排序的数列中的第一个数字设置为最大值/最小值,
然后最大值/最小值 和 “未排序的数列中的”每个数字逐一比较;如果最大值/最小值所比较的另外一个数字 大于/小于 最大值/最小值的话,
那么他们两两交换索引位置,只需要记录下所找出的最大值/最小值的下标,两两数字无需立即交换,而是在当前这一轮比较结束之后,
才根据所记录的最大值/最小值的索引位置,然后把所记录的最大值/最小值放到“已排好序的数列中的”末尾。
3.内存循环中使用的是 if (arr[j] > arr[k]){k = j;} 来比较的话:
1.arr[k]负责记录最大值,arr[j]负责自增,arr[j]还负责和最大值arr[k]进行比较,一轮比较结束时,
arr[k]记录着这一轮排序中“未排序的数列中的”的最大值,最终排序结果为从大到小。
2.数列中分为两部分,前面一部分的数列为排好序的数字,后面一部分的数列则为还没有排好序的数字。
3.arr[k]负责记录“每轮比较中未排序的数列中的”的最大值,每轮比较结束后,把最大值arr[k]插入到“已排好序的数列的”尾部,
最终排序结果为从大到小。
4.内存循环中使用的是 if (arr[j] < arr[k]){k = j;} 来比较的话:
1.arr[k]负责记录最小值,arr[j]负责自增,arr[j]还负责和最小值arr[k]进行比较,一轮比较结束时,
arr[k]记录着这一轮排序中“未排序的数列中的”的最小值,最终排序结果为从小到大。
2.数列中分为两部分,前面一部分的数列为排好序的数字,后面一部分的数列则为还没有排好序的数字。
3.arr[k]负责记录“每轮比较中未排序的数列中的”的最小值,每轮比较结束后,把最小值arr[k]插入到“已排好序的数列的”尾部,
最终排序结果为从小到大。
5.下面为选择排序第一轮比较的演示图:
1.先把未排序好的数列中的第一个数字作为最大值/最小值,然后最大值/最小值逐一和“未排序好的数列中的”每个数字进行比较,
只记录最大值/最小值的索引位置;
2.第一轮比较完毕时,找出了“未排序好的数列中的”最大值/最小值,然后把最大值/最小值和“未排序好的数列中的”第一个数字交换,
即作为“已排好序的数列中的”末尾。
6.每轮比较中:都会取出该轮比较中“未排序好的数列中的所有的数字中的”最大值/最小值,并把该最大值/最小值作为“已排好序的数列的”末尾。
1.从数列的头部开始往后移动,把“未排序好的数列中的”第一个数字预先作为最大值/最小值,然后最大值/最小值和后面的每个数字进行比较,
一轮比较结束后,便真正获取出“未排序好的数列中的”最大值/最小值。把最大值/最小值插入到“已排好序的数列中的”末尾,排序结果为 从大到小/从小到大。
2.此时有两种情况:
1.一轮比较结束之后,如果真正获取出来的最大值/最小值 是“预先作为最大值/最小值的未排序好的数列中的”第一个数字的话,
则不需要交换动作,直接作为“已排好序的数列中的”末尾;
2.一轮比较结束之后,如果真正获取出来的最大值/最小值 不是“预先作为最大值/最小值的未排序好的数列中的”第一个数字的话,
则进行交换动作,把最大值/最小值插入到“已排好序的数列中的”末尾。
1.从数列的尾部开始往前移动,把“未排序好的数列中的”最后一个数字预先作为最大值/最小值,然后最大值/最小值和前面的每个数字进行比较,
一轮比较结束后,便真正获取出“未排序好的数列中的”最大值/最小值。把最大值/最小值插入到“已排好序的数列中的”头部,排序结果为 从小到大/从大到小。
2.此时有两种情况:
1.一轮比较结束之后,真正获取出“未排序好的数列中的”最大值/最小值,和“预先作为最大值/最小值的未排序好的数列中的”最后一个数字
是同一个数字的话,则不需要交换动作,直接作为“已排好序的数列中的”头部;
2.一轮比较结束之后,真正获取出“未排序好的数列中的”最大值/最小值,和“预先作为最大值/最小值的未排序好的数列中的”最后一个数字
不是同一个数字的话,则进行交换动作,把真正获取出来的最大值/最小值插入到“已排好序的数列中的”头部。
当前演示的例子为:
从数列的头部开始往后移动,把“未排序好的数列中的”第一个数字预先作为最大值/最小值,然后最大值/最小值和后面的每个数字进行比较,
一轮比较结束后,便真正获取出“未排序好的数列中的”最大值/最小值。排序结果为 从大到小/从小到大。
此时有两种情况:
1.一轮比较结束之后,如果真正获取出来的最大值/最小值 是“预先作为最大值/最小值的未排序好的数列中的”第一个数字的话,
则不需要交换动作,直接作为“已排好序的数列中的”末尾;
2.一轮比较结束之后,如果真正获取出来的最大值/最小值 不是“预先作为最大值/最小值的未排序好的数列中的”第一个数字的话,
则进行交换动作,把最大值/最小值插入到“已排好序的数列中的”末尾。
int[] arr = { 1, 3, 2, 45, 65, 33, 12 };
System.out.println("交换之前:");
for (int num : arr)
{
System.out.print(num + " ");
}
//1.外层循环:比较轮数i = 元素个数n-1;
//2.同时因为i=0开始++的,假如元素个数n=5的话,最后一轮比较条件为 3<5-1 ,到 i=4结束比较,而0~3一共4次比较轮数。
//3.int i 既是 比较轮数,又是 “未排序好的数列中的”第一个数字;
// 作用:每轮比较中都把 “未排序好的数列中的”第一个数字预先作为最大值/最小值
for (int i = 0; i < arr.length - 1; i++)
{
//比较轮数i=k;备份比较轮数
//int i 和 int k:既是 比较轮数,又是 “未排序好的数列中的”第一个数字
//int i 和 int k 的作用:每轮比较中都把 “未排序好的数列中的”第一个数字预先作为最大值/最小值
int k = i;
//int j: “未排序好的数列中的”第二个数字,并且负责自增后移
//j < arr.length:后移到“未排序好的数列中的”最后一个元素
for (int j = k + 1; j < arr.length; j++)
{
//1.arr[k]:把 “未排序好的数列中的”第一个数字预先作为最大值,同时负责记录一轮比较中 “未排序好的数列中的”最大值
//2.arr[j] > arr[k]:负责自增后移的arr[j] 轮番和 “最大值arr[k]” 进行比较,找到有比 “最大值arr[k]”更大的值时,
// 把该值的索引值赋值给k,arr[k]记录着一轮比较中 “未排序好的数列中的”最大值
//if (arr[j] > arr[k])
//{
// k = j; //k:负责记录下“未排序好的数列中的”最大值的索引位置
//}
//1.arr[k]:把 “未排序好的数列中的”第一个数字预先作为最小值,同时负责记录一轮比较中 “未排序好的数列中的”最小值
//2.arr[j] > arr[k]:负责自增后移的arr[j] 轮番和 “最小值arr[k]” 进行比较,找到有比 “最大值arr[k]”更小的值时,
// 把该值的索引值赋值给k,arr[k]记录着一轮比较中 “未排序好的数列中的”最小值
if (arr[j] < arr[k])
{
k = j; //k:负责记录下“未排序好的数列中的”最小值的索引位置
}
}
// 此时有两种情况:
// 1.一轮比较结束之后,如果真正获取出来的最大值/最小值 是“预先作为最大值/最小值的未排序好的数列中的”第一个数字的话,
// 则不需要交换动作,直接作为“已排好序的数列中的”末尾;
// 2.一轮比较结束之后,如果真正获取出来的最大值/最小值 不是“预先作为最大值/最小值的未排序好的数列中的”第一个数字的话,
// 则进行交换动作,把最大值/最小值插入到“已排好序的数列中的”末尾。
if (i != k)
{
// 交换a[i]和a[k]:把“最大值/最小值”a[k] 插入到“已排好序的数列中的”末尾a[i]
int temp = arr[i];
arr[i] = arr[k];
arr[k] = temp;
}
}
System.out.println();
System.out.println("交换后:");
for (int num : arr)
{
System.out.print(num + " ");
}
插入排序
1.前一部分为已排序的有序数列,后一部分为未进行排序的数列。
2.排序规则:
1.第一轮比较中:先把“未排序数列中的”第一个数字 作为 “已排序数列中的”第一个数字
2.后面每轮比较中:
“未排序数列中的”第一个数字 从“已排序数列中的”末尾开始往前 逐一和每个数字比较,
最终在已排序数列中找到有序位置,把“未排序数列中的”第一个数字 插入到 已排序数列中的有序位置上,即最终组成 新的有序数列。
3.最优时间复杂度:
每轮比较中,如果一开始“未排序数列中的”第一个数字 就大于 “已排序数列末尾的”最后一个数字 的话,
那么代表了“未排序数列中的”第一个数字 大于 “已排序数列中的”所有数字,
那么只需要把“未排序数列中的”第一个数字 直接作为“已排序数列的”末尾即可。
4.最坏时间复杂度:
每轮比较中,“未排序数列中的”第一个数字 都需要从“已排序数列中的”末尾开始往前 逐一和每一个数字比较,
最终把“未排序数列中的”第一个数字插入到“已排序数列中的”头部;
public static void main(String[] args)
{
int source[] = new int[] { 53, 27, 36, 15, 69, 42 };
System.out.print("初始关键字:");
printArray(source);
System.out.println("");
InsertSort(source);
System.out.print("\n\n排序后结果:");
printArray(source);
}
public static void InsertSort(int[] arr)
{
int i, j;
int insertNode;//“未进行排序的数列中的”第一个数字
//1.int i=0 和 arr[0]:第一轮比较之前,把“未进行排序的数列中的”第一个数字 预先作为 “有序数列中的”第一个数字
//2.int i=1:第一轮比较中,“未进行排序的数列中的第一个数字”arr[1] 作为要插入的数据
for (i = 1; i < arr.length; i++)
{
//insertNode:每轮比较中,“未排序数列中的”第一个数字 作为要插入的数据,
// 同时相当于“未排序数列中的”第一个数字 备份到 insertNode中
insertNode = arr[i];
//i:“未排序数列中的”第一个数字的索引位置
//j=i-1:“已排序数列末尾的”最后一个数字
j = i - 1;
//1.每轮比较中:“未排序数列中的”第一个数字 从“已排序数列中的”末尾开始往前 逐一和每个数字比较
//2.insertNode:(要插入的元素)“未排序数列中的”第一个数字
//3.insertNode < arr[j]:
// 只要 (要插入的元素)“未排序数列中的”第一个数字 小于 “有序数列中的”arr[j]数字时,
// 就执行arr[j + 1] = arr[j]:把“有序数列中的”arr[j]数字 往后移动一个索引位置插入
while ((j >= 0) && insertNode < arr[j])
{
arr[j + 1] = arr[j];//把“有序数列中的”arr[j]数字 往后移动一个索引位置插入
j--;//每轮比较完毕后,在“已排序数列中的” 比较位置“j” 都从当前索引位置 往前移动一个索引位置
}
//(要插入的元素)“未排序数列中的”第一个数字 大于 (arr[j])第j个元素,
//将insertNote(“未排序数列中的”第一个数字) 插入到 arr[j] 的后面 一个索引位置上
arr[j + 1] = insertNode;
System.out.print("第" + i + "趟排序:");
printArray(arr);
}
}
private static void printArray(int[] source)
{
for (int i = 0; i < source.length; i++)
{
System.out.print("\t" + source[i]);
}
System.out.println();
}
希尔排序:插入算法的升级版
1.无论前面多轮比较中,使用了多少种步长进行分组排序,最后一轮比较中都需要使用步长1进行逐个排序,
因为在按照步长1进行排序之前,是无法知道数列是否都已经有序,所以需要在最后一轮比较中仍需要按照步长1进行排序,
最终保证数列是有序的。
2.实际上希尔排序的步长会使用一种算法计算出步长的值,算法计算出来的的步长值决定了希尔排序的效率快慢。
3.第一轮比较中设置为步长值为 元素个数 除以2,然后后面每轮比较中都在之前的步长值基础上除以2,
只要步长值 大于等于1 都需要继续进行分组排序,最终一轮比较中则需要按照步长1进行排序。
4.每一个分组数列在进行完插入排序之后,有两种情况:
1.第一种情况:分组数列整个变为有序
2.第二种情况:分组数列中部分数据变为有序,部分数据仍为无序
造成这种情况的原因:
1.比如使用的是“if (temp < data[j]){data[j + increment] = data[j];}”(排序结果:从小到大)这种判断方式的话,
表示前面的数比temp还大的话,则把前的数往后移一个位置,temp则往前移动一个位置;
2.而此时刚好temp的前一个数字data[j]比temp小的话,则temp不再往前移动,而是插入到该data[j]的后面一个位置;
3.而实际data[j]的前面还有比temp大的数字,那么该分组数列也仅是部分有序,整个分组数列还没有以从小到大的顺序排列。
public static void shellSortSmallToBig(int[] data)
{
int j = 0;
int temp = 0;
//1.第一轮比较中设置为步长值为 元素个数 除以2,然后后面每轮比较中都在之前的步长值基础上除以2,
// 只要步长值 大于等于1 都需要继续进行分组排序,最终一轮比较中则需要按照步长1进行排序。
//2.int increment = data.length / 2:第一轮比较中设置为步长值为 元素个数 除以2
//3.increment > 0:只要步长值 大于等于1 都需要继续进行分组排序
//4.后面每轮比较中都在之前的步长值基础上除以2:increment /= 2
for (int increment = data.length / 2; increment > 0; increment /= 2)
{
System.out.println("\n步长值:" + increment);
//在同一个步长值的情况下,切换分组数列;当一个分组数列中已排好序了,i自增加一,切换到后面下一个分组数列中进行比较:
// 1.int i = increment:每一个分组数列中的最后一个数字的索引位置
// 2.i++:每次自增就切换到下一个分组数列中
for (int i = increment; i < data.length; i++)
{
System.out.print(" i:" + i);
//1.data[i]:每一个分组数列中的最后一个数字
//2.temp:在同一个分组数列中,进行的每次比较时,都以“同一个分组数列中的”最后一个数字 作为 比较标准
temp = data[i];
//在当前同一个分组数列中进行插入排序:
// 分组数列中的最后一个数字 作为 比较标准(最大值/最小值),每次比较时 逐一和 前面每个数字进行比较,从后往前扫描,
// 把分组数列中的最后一个数字 往前插入到 有序的位置上,则前面的元素往后移。
// 1.j = i - increment:循环初始化值,每一个分组数列中的倒数第二个数字的索引位置,
// 同一个分组数列中的 最后一个数字的索引位置 减去 自增值 就是 前面一个数字的索引位置
// 2.j -= increment:自减“自增值”,每次比较完毕后,
// 都从当前数字的索引位置 移动到 同一个分组数列中的 前一个数字的索引位置上,
// 从分组数列中当前数字的索引位置 减去“自增值” 移动到 前面一个数字的索引位置
for (j = i - increment; j >= 0; j -= increment)
{
System.out.print(", j:"+j + "。");
// 代表“同一个分组数列中的最后一个数字的”前面每一个数字,然后逐一和 最后一个数字(最大值)进行比较,
// 只要前面的值有比最后一个数字还大的话,则把前面的值往后移动一个位置,排序结果则为 从小到大
if (temp < data[j]) //排序结果:从小到大
// 代表“同一个分组数列中的最后一个数字的”前面每一个数字,然后逐一和 最后一个数字(最小值)进行比较,
// 只要前面的值有比最后一个数字还小的话,则把前面的值往后移动一个位置,排序结果则为 从大到小
//if (temp > data[j]) //排序结果:从大到小
{
data[j + increment] = data[j];
}
else
{
System.out.println("break; 插入");
break;
}
}
//因为循环中最终是先执行“j -= increment”再执行判断然后结束循环的,所以此处需要data[j + increment]
data[j + increment] = temp;
System.out.println("--------------");
}
System.out.println();
for (int i = 0; i < data.length; i++)
{
System.out.print(data[i] + " ");
}
}
}
//每一个分组数列在进行完插入排序之后,有两种情况:
// 1.第一种情况:分组数列整个变为有序
// 2.第二种情况:分组数列中部分数据变为有序,部分数据仍为无序
//造成这种情况的原因:
//1.比如使用的是“if (temp < data[j]){data[j + increment] = data[j];}”(排序结果:从小到大)这种判断方式的话,
// 表示前面的数比temp还大的话,则把前的数往后移一个位置,temp则往前移动一个位置;
//2.而此时刚好temp的前一个数字data[j]比temp小的话,则temp不再往前移动,而是插入到该data[j]的后面一个位置;
//3.而实际data[j]的前面还有比temp大的数字,那么该分组数列也仅是部分有序,整个分组数列还没有以从小到大的顺序排列。
public static void main(String[] args)
{
int[] data = new int[] { 26, 53, 67, 48, 57, 13, 48, 32, 60, 50 };
System.out.print("数组长度:"+data.length);
shellSortSmallToBig(data);
// System.out.println(Arrays.toString(data));
}
数组长度:10
步长值:5
i:5, j:0。break; 插入
--------------
i:6, j:1。break; 插入
--------------
i:7, j:2。break; 插入
--------------
i:8, j:3。插入--------------
i:9, j:4。break; 插入
--------------
26 53 67 60 57 13 48 32 48 50
步长值:2
i:2, j:0。插入--------------
i:3, j:1。插入--------------
i:4, j:2。, j:0。break; 插入
--------------
i:5, j:3。break; 插入
--------------
i:6, j:4。, j:2。break; 插入
--------------
i:7, j:5。, j:3。break; 插入
--------------
i:8, j:6。, j:4。break; 插入
--------------
i:9, j:7。, j:5。, j:3。break; 插入
--------------
67 60 57 53 48 50 48 32 26 13
步长值:1
i:1, j:0。break; 插入
--------------
i:2, j:1。break; 插入
--------------
i:3, j:2。break; 插入
--------------
i:4, j:3。break; 插入
--------------
i:5, j:4。, j:3。break; 插入
--------------
i:6, j:5。break; 插入
--------------
i:7, j:6。break; 插入
--------------
i:8, j:7。break; 插入
--------------
i:9, j:8。break; 插入
--------------
67 60 57 53 50 48 48 32 26 13
快速排序
1.定义两个游标:
1.左边游标:负责向右边移动,指向的初始位置为左边第一个元素
2.右边游标:负责向左边移动,指向的初始位置为右边第一个元素
2.定义一个中间值(比较标准):
预先把左边的第一个元素作为中间值,也即左边的第一个元素作为比较的标准
3.右边游标比较的规则:
1.右边游标:负责向左边移动。
2.右边游标指向的元素 负责和“作为比较标准的”元素进行比较。
3.右边游标指向的元素 小于 “作为比较标准的”元素时,
把右边游标指向的元素 放到 左边游标指向的位置上,然后这一轮比较才真正结束。
4.右边游标指向的元素 大于 “作为比较标准的”元素时,不发生任何变化,
右边游标继续往左边移动指向下一个元素,直到所比较的元素发生位置替换时,这一轮比较才真正结束。
即一次比较中并没有发生元素的位置交换的话,则右边游标必须继续往左边移动指向下一个元素,
直到下一次比较中发生了元素的位置交换,那么这一次比较才真正结束。
4.左边游标比较的规则:
1.左边游标:负责向右边移动。
2.左边游标指向的元素 负责和“作为比较标准的”元素进行比较。
3.左边游标指向的元素 小于 “作为比较标准的”元素时,不发生任何变化,
左边游标继续往右边移动指向下一个元素,直到所比较的元素发生位置替换时,这一轮比较才真正结束。
4.左边游标指向的元素 大于 “作为比较标准的”元素时,
把左边游标指向的元素 放到 右边游标指向的位置上,然后这一轮比较才真正结束。
即一次比较中并没有发生元素的位置交换的话,则左边游标必须继续往右边移动指向下一个元素,
直到下一次比较中发生了元素的位置交换,那么这一次比较才真正结束。
5.每个分组的每轮比较结束后的结果:
1.分组中的“作为比较标准的”中间值 最终插入到分组中的中间位置或紧邻中间的位置;
以“位于分组中间位置的作为比较标准的”中间值 作为分割位置,分出左右两边的分组,
而左右两边的分组分别再进行新一轮的快速排序。
2.“位于分组中间位置的作为比较标准的”中间值 大于 左边的元素。
3.“位于分组中间位置的作为比较标准的”中间值 小于 右边的元素。
public class FastSort
{
public static void main(String []args)
{
int[] a = {12,20,5,16,15,1,30,45,23,9};
int start = 0;
int end = a.length-1;
sort(a,start,end);
for(int i = 0; i<a.length; i++)
{
System.out.println(a[i]);
}
}
//分组进行快速排序
public void sort(int[] a,int low,int high)
{
int start = low;//分组中的左边游标
int end = high;//分组中的右边游标
int key = a[low];//作为中间值(比较的标准)
while(end>start)
{
//从后往前比较
//如果没有比关键值小的,比较下一个,直到有比关键值小的交换位置,然后又从前往后比较
while(end>start && a[end]>=key)
end--;
//右边游标的值小于中间值,则把右边游标的值移动到左边游标上
if(a[end]<=key)
{
int temp = a[end];
a[end] = a[start];
a[start] = temp;
}
//从前往后比较
//如果没有比关键值大的,比较下一个,直到有比关键值大的交换位置
while(end>start && a[start]<=key)
start++;
if(a[start]>=key)
{
int temp = a[start];
a[start] = a[end];
a[end] = temp;
}
//此时第一次循环比较结束,关键值的位置已经确定了。
//左边的值都比关键值小,右边的值都比关键值大,但是两边的顺序还有可能是不一样的,进行下面的递归调用
}
//递归
if(start>low)
sort(a,low,start-1);//左边序列。第一个索引位置到关键值索引-1
if(end<high)
sort(a,end+1,high);//右边序列。从关键值索引+1到最后一个
}
}
冒泡排序
下面的冒泡排序的 演示图为比较的第一轮:就能把未排序的数列中找出最大的放到尾部,而每一轮比较中的每次两两比较,都可能需要进行交换
选择排序
无论是最坏时间复杂度还是最有时间复杂度都是n-1*n,即O(n^2),因为即使排序完毕一轮之后,都只知道头部是排序完毕的数列,
而剩余的未排序的数列是不清楚有没有序的,因此即使当未进行排序的数列本身是已经有序的,仍然需要进行选择排序,
所以最终结论便是最坏时间复杂度还是最优时间复杂度都是O(n^2)
插入排序
1.前一部分为已排序的有序数列,后一部分为未进行排序的数列(紫色为已排序的有序数列,蓝色为未进行排序的数列);
2.排序规则是取出后一部分未进行排序的数列中的第一个数 和 已排序的有序数列中的最后一个数字往第一个数字的方向 逐一进行 两两比较,
找到相应位置并插入;
排序的结果是把 未进行排序的数列中的第一个数 插入到 已排序的有序数列中 组成 新的有序数列;
3.最优时间复杂度:只比较元素个数n-1轮,每轮只比较一次,那么为n+1,即O(n);
之所以每轮比较一次,是因为每轮的第一次比较中,首先是“未进行排序的数列中的”第一个数 和 “已排序的有序数列中的”最后一个数字
进行比较,如果此时“未进行排序的数列中的”第一个数 就已经大于 “已排序的有序数列中的”最后一个数字 的话,
就表示 “未进行排序的数列中的”第一个数 大于 “已排序的有序数列中的”所有数字,那么只需要把 “未进行排序的数列中的”第一个数
直接追加到 “已排序的有序数列中的”末尾即可;
最坏时间复杂度:只比较元素个数n-1轮,每轮比较中,那么为n+1,即O(n^2),每轮比较中,“未进行排序的数列中的”第一个数 都需要和
“已排序的有序数列中的”所有数字 进行比较;
希尔排序:插入排序的升级版
无论前面进行了多少种步长进行排序,最终都需要按照步长1进行排序,
因为在按照步长1进行排序之前,是无法知道数列是都已经有序,所以需要在最后都需要按照步长1进行排序,
保证数列最终是有序的
实际上希尔排序的步长也会使用一种算法计算出步长的值,步长的值决定了排序的效率的快慢;
而此处就简单地设置步长为元素个数除以2 ;
只要步长大于等于1都进行排序,最终一轮比较都为按照步长1进行排序;
递归
1.向下是 t(n-1)
向上是结束t(n-1)的函数,执行打印语句
2.每次调用t(n-1) 进入函数栈,递归调用t(n-1),直到执行if n==0 递归结束条件 最终开始释放 函数栈,
开始执行每一个函数栈中的打印语句
3.理解每次调用t(n-1) 创建一个函数栈:
最先调用的函数 最先进栈,也最后释放最后调用结束,当符合if n==0 递归结束条件时,立即开始释放最近进栈的函数栈,
也即 开始结束最近调用的函数,每个函数都执行最后的打印语句再结束函数调用;
快速排序(最推荐使用该排序)
归并排序
把两个单独的数列再组成一个有序的数列
while循环结束后,两个列表中的必定会有其中一个列表中的
最后一个元素值没有机会插入到新的列表中,
因为不知道是哪个列表中的最后一个元素值没有插入到新的
列表中,所以需要把两个列表都尝试进行把最后一个元素
都插入到新的列表中
只有归并排序不是在原数列上进行排序数据,而是创建一个新列表保存排列完毕的数据,
其他的排序方法都是在原有的数列上进行排序
二分法:递归实现
二分法:非递归实现
使用链表实现二叉树
层级遍历:从上到下,从左到右
添加子节点:从上到下,从左到右地 使用队列 添加子节点
先序遍历、中序遍历、后续遍历 的打印的顺序 根据的是 函数栈 的顺序,
即 print 语句 所在 函数进栈/函数出栈时 的位置不同,执行 print 语句的顺序 也即不同,所以打印数据的顺序也便不同。
先序遍历:中 --> 左 --> 右
中序遍历:左 --> 中 --> 右
后续遍历:左 --> 右 --> 中
python range() 函数可创建一个整数列表,一般用在 for 循环中。
函数语法range(start, stop[, step])
参数说明:
start: 计数从 start 开始。默认是从 0 开始。例如range(5)等价于range(0, 5);
stop: 计数到 stop 结束,但不包括 stop。例如:range(0, 5) 是[0, 1, 2, 3, 4]没有5
step:步长,默认为1。例如:range(0, 5) 等价于 range(0, 5, 1)
>>>range(10) # 从 0 开始到 10
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> range(1, 11) # 从 1 开始到 11
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
>>> range(0, 30, 5) # 步长为 5
[0, 5, 10, 15, 20, 25]
>>> range(0, 10, 3) # 步长为 3
[0, 3, 6, 9]
>>> range(0, -10, -1) # 负数
[0, -1, -2, -3, -4, -5, -6, -7, -8, -9]
>>> range(0)
[]
>>> range(1, 0)
[]
>>>x = 'runoob'
>>> for i in range(len(x)) :
... print(x[i])
...
r
u
n
o
o
b
>>>
>>>for j in range(5,0,-1):
... print(j)
...
5
4
3
2
1
队列
链表
链表:不申请连续的内存空间,可以充分利用零碎不连续的内存空间进行数据存储,并且每个元素之间的排列是有序的;
链表每个节点都申请8个字节,前面4字节存储数据,后面4字节存储 “指向下一个节点的” 地址值;
头部增加新节点:先让新节点的next 指向 “(头指针)head指向的”头节点,然后再让 (头指针)head指向 新节点,
那么最终新节点成为了单链表中的头节点
遍历链表:需要先创建一个游标cur,游标cur实际为拷贝了一份的(头指针)head;
然后每次移动游标cur指向下一个节点,即对游标cur进行赋值当前一个节点的next,游标cur便指向下一个节点
往链表尾部添加新节点:先遍历到链表的尾节点,再在尾节点后面增加一个新节点,让尾节点的next指向新节点
往链表里面任意位置插入新节点:让游标cur找到并指向 “新节点要插入的指定节点位置的” 前一个节点,
让新节点的next和游标cur的next一样都共同指向“要插入的指定节点位置的”节点,
最后让游标cur的next指向新节点
删除节点:创建pre游标和cur游标,pre游标指向“cur游标指向的节点的”前一个节点,
最终让 “pre游标指向的节点的” next 指向 “cur游标指向的节点的” next
栈
1.Python的内置数据结构,比如列表、元组、字典;
2.Python的扩展数据结构,比如栈,队列等;
而这些数据组织方式,Python系统里面没有直接定义,需要我们自己去定义实现这些数据的组织方式,
这些数据组织方式称之为Python的扩展数据结构;
时间复杂度
顺序表
1.第一种存储方式:顺序表中的元素都是同一种数据类型,那么每个元素所占的内存大小都相同
2.第二种存储方式:(python采取元素外置的方式)顺序表中存储的元素并不是实际的数据值,而是该数据值的地址值
特点:1.每个元素的数据类型可以是不一样,那么不同类型的元素所占的内存大小也不一样;
不管是任何数据类型的数据的地址值都占4字节;
2.顺序表是开辟出来的一块连续的内存空间,而顺序表中存储的每个元素是“任意数据类型的数据的”地址值;
一体式结构的拓容:1.要对一体式结构所占的内存空间进行拓容时,无法在原地址内存空间上直接拓容,
因为该原地址内存空间紧接的后面的地址内存空间可能已经存有数据;
2.需要重新开辟一个新的更大的内存空间,产生新的首地址,即表头是新的首地址;
然后把原地址内存空间上的全部元素数据拷贝到新的更大的内存空间中;
分离式结构的拓容:1.要对分离式结构所占的内存空间进行拓容时, 表头的地址无需变动,只需要新开辟一个更大的内存空间的元素存储区,
新的元素存储区是新的内存地址;
2.把原地址内存空间上的全部元素数据拷贝到新的元素存储区中
python中的顺序表 同时 采取元素外置 和 分离式结构:
获取元素:
1.获取元素数据的时间复杂度是O(1)一步搞定,直接根据下标/角标/偏移量从顺序表中获取元素数据;
增加元素:
1.尾部加入元素数据的时间复杂度是O(1)一步搞定
2.非尾部插入元素数据,那么时间复杂度是O(n),那么就要把插入的元素后面的每个元素都往后移动一步,
那么n个元素往后移动,就要进行n个步骤
删除元素:
1.pop():删除尾部的最后一个元素的时间复杂度是O(1)一步搞定;
2.非尾部删除元素数据:根据下标/角标/偏移量从顺序表中删除元素数据,那么删除的元素后面的所有元素都需要
往前面移动一个元素位置,那么时间复杂度是O(n);