数据结构算法自学笔记 大话思路与伪代码
以下为笔者自学过程中觉得需要记录的东西,并不代表正确且专业的知识,所以如果大家看到有不对的地方,希望大家能高抬贵手多多指出,谢谢!(持续更新)
一、排序
一)插入排序
插入排序对于少量元素的排序是一个比较有效的算法。
白话思路:对于一个未排列的数组,暂且称当前正在排列移动的数字称作当前数。一般当前数从数组的第二位开始,遵循从当前数的当前位置向前开始比对,例如:a[1]需要和下标为1之前的数字一一比较,即与a[0]进行比较,也就是当前数从右向左依次比较。如果比较过程中,发现比当前数大的比较数,怎么将比较数往后移动一位,同时,该比较数之前的数同意向后移动一位,直到一个比当前数小的数字,将当前数插入到该数字之后的位置上。
伪代码:
INSTERTION-SORT(A) //A为传入的参数 是一个数组
for j=1 to A.length
key = A[j] //将当前待排序的元素备份
i = j-1 //将下标定位到当前待排序数的前一位
while(i>0 && A[i]>key) //当前排序的数字依次与下标之前的元素比较 为的是找到比当前排序数大的元素
//然后将它们依次后移一位,直到找到比key小的值,停止,退出循环
A[i+1] = A[i]
i=i-1
A[i+1] = key //将之前备份的元素插入到循环结束处,该数完成比较插入动作
图示:
总结:也就是把当前数抽出,并移动比当前数大的数,最后将当前数插入到循环停止的下标处。
二)归并排序
思路:将一组数据递归分解成足够小的若干组,合并过程中两两比较大小,重新插入到原数组中。归并排序利用到分治法的思想,采取三步骤:分解——将一个数组分解成足够小的若干个小数组;解决——两两数组比较得出有顺序的新数组;合并——将这些排序后的数组,再两两进行比较合并动作,直到最后只有一个数组。
换个说法,分解的意思就是桌上有一副牌,将这副牌从中间分开,分成了两副牌。这两副牌再接着进行相同的操作,即为分解。解决即是A、B两副牌(假定各有两张,且经过前面的操作已分别有序),两副牌都是叠放,牌面朝上。每次将两副牌的最上面一张牌进行比较,较小的一张抽出,抽出牌为新数组,被抽出的一副牌会露出下面的一张新牌,两副牌再进行比较,直到AB两副牌已比较完,得出的新数组即为合并。
分解过程伪代码:
MERGE-SORT(A,p,r) //A为待排序的数组,p为头下标,r为尾下标
if( p < r) //为了保证A数组当前p、r头尾下标中间还有数据存在
q = (p + r)/2 //分割数组,取中间下标
MERGE-SORT(A,p,q) //递归调用自身,对上半段元素继续进行相同操作
MERGE-SORT(A,q+1,r) //递归调用自身,对下半段元素继续进行相同操作
MERGE(A,p,q,r) //当分割到足够小时,进行比较合并操作
解决、合并过程伪代码:
MERGE(A,p,q,r)
n1 = q - p + 1 //计算出目前所需要排序的前半段元素数
n2 = r -q //计算出目前所需要排序的后半段元素数
let L[0,n1] and R[0,n2] be new arrays //建立两个新数组 用于分别存放前半段和后半段元素
for i = 0 to (n1-1)
L[i] = A[p + i]
for j = 0 to (n2-1)
R[j] = A[q + 1 + j]
L[n1] = R[n2] = ∞ //L,R数组的最后一位都存了一个“哨兵位”,目的是为了避免每次抽掉一已比较数字后都
//要检验一遍数组是否到底,提高了效率,只要指向了哨兵位,说明该数组已检验完
i = j = 0
for k = p to r //循环(r-p+1)次,为的是比较L,R数组(r-p+1)次,然后合并覆盖到数组A中
if(L[i] <= R[j])
A[k] = L[i]
i = i +1
else
A[k] = R[j]
j = j + 1
// 对于这个“哨兵位”,让我们来做一个假设,L数组的里的数全都比R数组里的数大,所以循环到一半的时候,
//R数组中的数已经被抽光了,轮到了最后一位为一个非常大的“哨兵位”,那么这个时候L中的元素再去和R数
//组比较,都是在和这个非常大的“哨兵位”在比较,结果当然是L数组中的元素都比“哨兵位”小,依次合并覆
//到A数组中,省去了每次都需要检测L、R数组是否到底的步骤,提高效率
补充例题:股票问题(最大子数列)
“一段时间内,股票有涨有降,可以在价格最低的一天买入股票,然后在价格最高的一天卖出股票,以获得最大利润,但是最低价格的一天不一定在最高价格的前面,所以需要求得最高利润的买入股票日和卖出股票日。”
思路:把这段时间的价格换算成价格差来看,即第一天的价格差为第1天和第0天的股票价格差,第2天的价格差为第2天与第1天的股票价格差。此价格差数列必定有正有负,所以得到一组新的数组A。接下来利用分治法的思想:将数组A第一次均分的时候,数组如下被分开成1、2两段(如下图),数组A的任意一个子数组都可能处于任何位置,但总共会有三种情况:
1)整个子数组完全处于1段中;
2)整个子数组完全处于2段中;
3)子数组在1、2都有份,跨越mid。
1)、2)的情况利用递归继续分割,所以现在单独来讨论下第三种情况。
当单独讨论3)的情况时,他的子数组有一个限制,就是子数组必定经过mid。所以就会有这么个情况:存在A[i ~ mid] 和 A[mid+1 ~ j ]两个子数组,由一个完整的跨越mid的子数组分割而来,所以若干个类似于此的i点和j点,这就变成了左右分别求最大和值最后在合并的计算了,合并即可求出这种情况下的最大子数列。
接下来是这部分伪代码:
FIND-MAX-CROSSING-SUBARRAY(A,low,mid,high)
left-sum = -∞ //(记录位)建立该比较数的理由在于,从右(mid)向左(low)寻找过程中,sum不断在与左边
//的数相加,如果加数为负数,则sum变小,left-sum不记录该值;如果加数是正数,而相加后
//sum超过了left-sum,则记录下该值和当前下标,表示为目前寻找到的最大和。
sum = 0 //初始值为0 所以第一次比较必定符合判断条件,大于left-sum
for i = mid downto low
sum = sum + A[i]
if sum > left_sum //说明当前遍历的元素符合找到最大和的条件,更新left-sum,并记录下当前元素下标
left_sum = sum
max_left = i
right_sum = -∞ //右边与左边道理相同
sum = 0
for j = mid+1 to high
sum = sum + A[j]
if sum > right_sum
right_sum = sum
max_right = j //记录下right-sum和当前元素的下标
return (max_left,max_right,left_sum + right_sum) //返回左边、右边达到最大和值要求的下标,合并后
//的最大值(最大子数列的和)
现在来实现求最大子数组问题的分支算法伪代码:
FIND-MAXIMUM-SUBARRAY(A,low,high)
if high == low //判断当前子数列是否只有一个元素了
return (low,high,A[low])
else //在保证当前子数列内至少有两个元素的情况下
mid = (low + high)/2
//将当前子数列递归分割 前半段再次进行判断
(left_low,left_high,left_sum)=FIND-MAXIMUM-SUBARRAY(A,low,mid)
//判断当前子数列的后半段
(right_low,right_high,right_sum)=FIND-MAXIMUM-SUBARRAY(A,mid+1,high)
//判断当前子数列的第3)种情况 计算出最大子数列
(cross_low,cross_high,cross_sum)=FIND-MAXIMUM-SUBARRAY(A,low,mid,high)
if left_sum >= cross_sum and left_sum >= right_sum
return (left_low,left_high,left_sum)
else if right_sum >= left_sum and right_sum >= cross_sum
return (right_low,right_high,right_sum)
else return (cross_low,cross_high,cross_sum)
(未完待续)