排序算法
排序
代码实现:Java 和 Python
一、概念
1.1 排序算法的稳定性
稳定性:稳定排序算法会让原本有相等键值的纪录维持相对次序。也就是如果一个排序算法是稳定的,当有两个相等键值的纪录R和S,且在原本的列表中R出现在S之前,在排序过的列表中R也将会是在S之前。
采用Python说明
原有的序列:
(4, 1) (3, 1) (3, 7)(5, 6)
按照元组第一个元素,排序后的情况
(3, 1) (3, 7) (4, 1) (5, 6) (维持次序:稳定) (3, 7) (3, 1) (4, 1) (5, 6) (次序被改变)
1.2 常见排序算法效率比较
二、排序算法
2.1 冒泡排序
代码实现
Python实现:
def bubble_sort(alist): """ 冒泡排序 """ n = len(alist) for j in range(n-1): count = 0 # 引入来优化冒泡排序 for i in range(n-j-1): if alist[i] > alist[i+1]: alist[i], alist[i+1] = alist[i+1], alist[i] count += 1 if count == 0: #没发生交换,说明已经有序 break
Java实现:
public class Bubble { public void bubble_sort(int[] alist){ System.out.print("Bubble Sort:"); for (int j = 0; j < alist.length; j++) { for (int i = 0; i < alist.length-j-1; i++) { if(alist[i] > alist[i+1]){ alist[i] = alist[i] + alist[i+1]; alist[i+1] = alist[i] - alist[i+1]; alist[i] = alist[i] - alist[i+1]; } } } } public static void main(String[] args) { int[] alist = {54,26,93,17,77,31,44,55,20}; for (int i = 0; i < alist.length; i++) { System.out.print(alist[i]); System.out.print(" "); } System.out.println(" "); Bubble sort = new Bubble(); sort.bubble_sort(alist); //打印排序结果 for (int i = 0; i < alist.length; i++) { System.out.print(alist[i]); System.out.print(" "); } } }
时间复杂度:
- 最坏时间复杂度:O(n^2)
- 最优时间复杂度O(1)
- 稳定性:稳定
2.2 选择排序
代码实现
Python:
def select_sort(alist): n = len(alist) for j in range(n-1): min_index = j for i in range(j+1,n): if alist[min_index] > alist[i]: min_index = i alist[j], alist[min_index] = alist[min_index], alist[j]
复杂度分析:外层循环执行n-1次,内层循环执行 n-1, n-2, n-3, … 1次
所以算法复杂度为:O(n^2),最优最坏都是O(n^2)
Java:
public class Select { public void select(int[] alist){ System.out.print("Select Sort:"); int min; for (int j = 0; j < alist.length-1; j++) { min = j; for (int i = j+1; i < alist.length; i++) { if(alist[i] < alist[min]){ min = i; } } //记得加判断条件 或者 用另一个变量来进行交换 if(min!=j){ alist[j] = alist[j] + alist[min]; alist[min] = alist[j] - alist[min]; alist[j] = alist[j] - alist[min]; } } } public static void main(String[] args) { int[] alist = {54,26,93,17,77,31,44,55,20}; for (int i = 0; i < alist.length; i++) { System.out.print(alist[i]); System.out.print(" "); } System.out.println(" "); Select sort = new Select(); sort.select(alist); for (int i = 0; i < alist.length; i++) { System.out.print(alist[i]); System.out.print(" "); } } }
时间复杂度
最优时间复杂度:O(n2)
最坏时间复杂度:O(n2)
稳定性:不稳定
算法不稳定:举例子[3,2,3,1],第一次进行选择排序,下标为0的3和最后一位数交换,此时两个3的顺序变了
2.3 插入排序
代码实现
Python:
def insert_sort(alist): n = len(alist) for j in range(1,n): for i in range(j,0,-1): if alist[i] < alist[i-1]: alist[i-1], alist[i] = alist[i], alist[i-1] else: break
Java:
public class Insert { public void insert(int[] alist){ System.out.println("Insert Sort"); for (int j = 1; j < alist.length; j++) { for (int i = j-1; i >= 0 ; i--) { if(alist[i+1] < alist[i]){ alist[i] = alist[i] + alist[i+1]; alist[i+1] = alist[i] - alist[i+1]; alist[i] = alist[i] - alist[i+1]; } //已经是有序了,不用插入交换,跳出 else{ break; } } } } public static void main(String[] args) { int[] alist = {54,26,93,17,77,31,44,55,20}; for (int i = 0; i < alist.length; i++) { System.out.print(alist[i]); System.out.print(" "); } System.out.println(" "); Insert sort = new Insert(); sort.insert(alist); for (int i = 0; i < alist.length; i++) { System.out.print(alist[i]); System.out.print(" "); } } }
时间复杂度
最优时间复杂度:O(n) (升序排列,序列已经处于升序状态)
最坏时间复杂度:O(n2)
稳定性:稳定
2.4 希尔排序
希尔排序(Shell Sort)是插入排序的一种,也称缩小增量排序,把一个序列按照一定增量分组,对每组使用直接插入排序算法排序;增量逐渐减少,当增量减至1时,整个文件恰好被分成一组,算法终止
举例:
例如,假设有这样一组数组[13 14 94 33 82 25 59 94 65 23 45 27 73 25 39 10],如果我们以步长为5开始进行排序,我们可以通过将这列表放在有5列的表中来更好地描述算法,这样他们就应该看起来是这样(竖着的元素是步长组成):
13 14 94 33 82 25 59 94 65 23 45 27 73 25 39 10
然后我们对每列进行排序:
10 14 73 25 23 13 27 94 33 39 25 59 94 65 82 45
将上述四行数字,依序接在一起时我们得到:[10 14 73 25 23 13 27 94 33 39 25 59 94 65 82 45]。这时10已经移至正确位置了,然后再以3为步长进行排序:
10 14 73 25 23 13 27 94 33 39 25 59 94 65 82 45
排序之后变为:
10 14 13 25 23 33 27 25 59 39 65 73 45 94 82 94
最后以1步长进行排序(此时就是简单的插入排序了)
代码实现
Python:
def shell_sort(alist): n = len(alist) gap = n//2 # python3 while gap > 0: for j in range(gap, n) i = j # 注意判断条件,不能写i>0,否则会越界 while i>= gap and alist[i] < alist[i-gap]: alist[i],alist[i-gap] = alist[i-gap],alist[i] i -= gap gap = gap//2
Java:
public class Shell { public void shell(int[] alist){ System.out.println("Shell Sort:"); int gap = alist.length/2; while(gap>0){ for (int j = gap; j < alist.length; j++) { for (int i = j; i >= gap; i -= gap) { if(alist[i] < alist[i-gap]){ alist[i] = alist[i] + alist[i-gap]; alist[i-gap] = alist[i] - alist[i-gap]; alist[i] = alist[i] - alist[i-gap]; } else{ break; } } } gap /= 2; } } public static void main(String[] args) { int[] alist = {54,26,93,17,77,31,44,55,20}; for (int i = 0; i < alist.length; i++) { System.out.print(alist[i]); System.out.print(" "); } System.out.println(" "); Shell sort = new Shell(); sort.shell(alist); for (int i = 0; i < alist.length; i++) { System.out.print(alist[i]); System.out.print(" "); } } }
时间复杂度
最优时间复杂度:根据步长序列的不同而不同,最好的情况O(n^1.3)
最坏时间复杂度:O(n2)
稳定想:不稳定
最优的情况不如插入排序的好
2.5 快速排序
快速排序(英语:Quicksort),又称划分交换排序(partition-exchange sort),通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
步骤为:
- 从数列中挑出一个元素,称为"基准"(pivot),
- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
- 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。虽然一直递归下去,但是这个算法总会结束,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。
代码实现
两个定界实现(Python):
def quick_sort(alist, start, end): if start >= end; return privot = alist[start] left = start right = end while left < right: while left < right and alist[right] > privot: right -= 1 alist[left] = alist[right] while left < right and alist[left] <= privot: left += 1 alist[right] = alist[left] alist[low] = privot quick_sort(alist, start, low-1) quick_sort(alist, low+1, end)
两个游标实现(Java):
public class Quick { public void quick(int[] alist, int start, int end){ if(start >= end){ return; } int privot = alist[start]; int low = start; int high = end; while(low<high){ while(low<high && alist[high]>privot){ high--; } alist[low] = alist[high]; while(low<high && alist[low]<privot){ low++; } alist[high] = alist[low]; } alist[low] = privot; quick(alist,start,low-1); quick(alist,low+1,end); } public static void main(String[] args) { int[] alist = {54,26,93,17,77,31,44,55,20}; for (int i = 0; i < alist.length; i++) { System.out.print(alist[i]); System.out.print(" "); } System.out.println(" "); Quick sort = new Quick(); System.out.println("Quick Sort:"); sort.quick(alist,0,alist.length-1); for (int i = 0; i < alist.length; i++) { System.out.print(alist[i]); System.out.print(" "); } } }
一个小于区域small实现(Java):
public class Quick { public void quick(int[] alist, int start, int end){ if(start >= end){ return; } int privot = alist[start]; int small = start - 1; for (int i = start; i < end; i++) { if (alist[i] <= privot){ swap(alist, i, ++small); } } swap(alist, start, small); quick(alist, start, small-1); quick(alist, small+1,end); } public void swap(int[] alist, int x, int y){ if(x==y){ return; } alist[x] = alist[x] + alist[y]; alist[y] = alist[x] - alist[y]; alist[x] = alist[x] - alist[y]; } public static void main(String[] args) { int[] alist = {54,26,93,17,77,31,44,55,20}; for (int i = 0; i < alist.length; i++) { System.out.print(alist[i]); System.out.print(" "); } System.out.println(" "); Quick sort = new Quick(); System.out.println("Quick Sort:"); sort.quick(alist,0,alist.length); for (int i = 0; i < alist.length; i++) { System.out.print(alist[i]); System.out.print(" "); } } }
利用荷兰国旗提升快排速度(java):
为什么能提速?
相比经典快排(定一个数字privot的位置,拆分按照该位置的左右),每次找到小于privot定界small和大于privot的定界big,按照值privot来进行划分,可以减少划分次数。
public class Quick { public void quick(int[] alist, int start, int end){ if(start >= end){ return; } int privot = alist[start]; int small = start - 1; int big = end + 1; for (int i = start; i < big; i++) { if (alist[i] < privot){ swap(alist, i, ++small); } else if (alist[i] > privot){ swap(alist, i, --big); i--; } } quick(alist, start, small); quick(alist, big, end); } public void swap(int[] alist, int x, int y){ if(x==y){ return; } alist[x] = alist[x] + alist[y]; alist[y] = alist[x] - alist[y]; alist[x] = alist[x] - alist[y]; } public static void main(String[] args) { int[] alist = {54,26,93,17,77,31,44,55,20}; for (int i = 0; i < alist.length; i++) { System.out.print(alist[i]); System.out.print(" "); } System.out.println(" "); Quick sort = new Quick(); System.out.println("Quick Sort:"); sort.quick(alist,0,alist.length-1); for (int i = 0; i < alist.length; i++) { System.out.print(alist[i]); System.out.print(" "); } } }
如果还要提升效率呢?
随机快排:
只要把privot在区间内获取下标的值即可实现:
int privot = alist(int)(start+Math.random()*(end-start+1));
public class Quick { public void quick(int[] alist, int start, int end){ if(start >= end){ return; } int privot = alist[(int)(start+Math.random()*(end-start+1))]; int small = start - 1; int big = end + 1; for (int i = start; i < big; i++) { if (alist[i] < privot){ swap(alist, i, ++small); } else if (alist[i] > privot){ swap(alist, i, --big); i--; } } quick(alist, start, small); quick(alist, big, end); } public void swap(int[] alist, int x, int y){ if(x==y){ return; } alist[x] = alist[x] + alist[y]; alist[y] = alist[x] - alist[y]; alist[x] = alist[x] - alist[y]; } public static void main(String[] args) { int[] alist = {54,26,93,17,77,31,44,55,20}; for (int i = 0; i < alist.length; i++) { System.out.print(alist[i]); System.out.print(" "); } System.out.println(" "); Quick sort = new Quick(); System.out.println("Quick Sort:"); sort.quick(alist,0,alist.length-1); for (int i = 0; i < alist.length; i++) { System.out.print(alist[i]); System.out.print(" "); } } }
时间复杂度
- 最优时间复杂度:O(nlogn)
- 最坏时间复杂度:O(n^2)
- 稳定性:不稳定
复杂度分析:
- 最优:每次拆分的位置都是中间的位置,2*2*2... = n, 次数的时间复杂度是logn
- 最坏:如果每次都是最右边或者最左边,就会分n次,次数的时间复杂度是n
2.6 归并排序
归并排序是采用分治法的一个非常典型的应用。归并排序的思想就是先递归分解数组,再合并数组。
将数组分解最小之后,然后合并两个有序数组,基本思路是比较两个数组的最前面的数,谁小就先取谁,取了后相应的指针就往后移一位。然后再比较,直至一个数组为空,最后把另一个数组的剩余部分复制过来即可。
代码实现
写两个方法:
- 递归拆分(条件:小于等于1,直接返回传入的表)
- 合并(需要两个游标)
Python实现:
def merge_sort(alist): if len(alist) == 1: return alist mid = len(alist)//2 left = merge_sort(alist[:mid]) right = merge_sort(alist[mid:]) return merge(left,right) def merge(left,right): result = [] L_point = 0 R_point = 0 while L_point < len(left) and R_point < len(right): if left[L_point] <= right[R_point]: result.append(left[L_point]) L_point += 1 else: result.append(right[R_point]) R_point += 1 result += left[L_point:] result += right[R_point:]
每个递归拿一个表来存储一个合并结果
Java实现:
时间复杂度
- 最优时间复杂度:O(nlogn)
- 最坏时间复杂度:O(nlogn)
- 稳定性:稳定(注意合并时候的比较,小于和小于等于可能影响稳定性)
时间复杂度分析:纵向拆分O(logn),横向O(n)
时间复杂度是小的,但是需要一个和原来一样大的数组来存储,空间复杂度高
2.8 堆排序
堆排序引入的好处
简单选择排序,在待排序的n个记录中选择最小的记录需要比较n-1次。(这可以理解:查找第一个数据需要比较这么多次是正常的,否则如何知道它是最小的记录)
可惜的是:选择排序没有把每一趟的比较结果保存下来,在后一趟的比较中,有许多比较在前一趟就已经做过了,但是由于前一趟排序未保存这些比较结果,所以以后一趟排序又重复执行了这些比较操作,因此记录的比较次数较多。
如果可以做到每次在选择到最小记录的同时,根据比较结果对其他记录做出相应的调整,那样排序的总体效率就会非常高了。堆排序(Heap Sort),就是对简单选择排序进行的一种改进。
堆结构
堆结构具有的性质
- 完全二叉树
- 大顶堆:每个结点大于等于它的左右孩子
- 小顶堆:每个结点小于等于它的左右孩子
大顶堆满足的位置关系(限制条件:1<=i<=「n/2」 「」我表示为:取整):
- i(父) >= 2i(左孩子)
- i(父) >= 2i+1(右孩子)
小顶堆满足的位置关系(限制条件:1<=i<=「n/2」 「」我表示为:取整):
- i(父) <= 2i(左孩子)
- i(父) <= 2i+1(右孩子)
如果知道位置i(i>1),可以得到其双亲结点「i/2」;由此可推得,上式n个结点的二叉树,那么其i自然就得约束小于等于「n/2」。注意:以上说明的位置是从1开始的,即根节点是1。如果你是用数组存储一个完全二叉树,那么,根节点应该是0,左孩子是2i+1,右孩子是2i+2。一个结点的父节点是(i-1)/2。我们来看特殊点,0结点,父节点(0-1)/2 结果是0,所以0的父节点是自己,符合。
如何无序序列构建成一个堆
堆结构非常重要的操作
- 堆结构的heapInsert和heapify
- 堆结构的增大和减少
- 如果只是建立堆的过程,时间复杂度为O(N)
- 优先级队列结构,就是堆结构
构建一个大顶堆的时间复杂度分析
一个堆(如大顶堆),新增一个数,复杂度只跟树的高度有关系,因为只要往上的父节点依次比较即可,跟其他结点无关。
i结点加入的调整代价为log(i-1),i+1结点加入的调整代价为logi,所以n个结点的调整代价为:log1+log2+log3+…+log(n-1), 复杂度为O(n),所以建立一个大根堆的复杂度就是O(n)
heapInsert
已经建立好一个堆,新增一个结点,要依次向上进行比较。什么时候停止?知道不比父节点大的时候
heapify (需要传入heapsize表示堆的大小,注意:heapsize不一定是数组大小,可能数组的前半部分是堆,例如堆排序,堆顶放到数组后的这个过程,堆的heapsize减一)
应用:数组的一个值变化了,假如变小了,从这个位置往下进行heapify过程重新调整为大根堆。
方法:
- 假设值变化的结点下标为i,找左右孩子(2i+1,2i+2),获取到最大的那个的下标j(2i+1 or 2i+2)。
- 结点i的值与结点j比较,如果结点i值较大或者等于(break:不用往下沉了),如果小于,交换。
- 交换的结果导致结点j的值变化了,重复1,2,3的操作,直到没有孩子(lchild = index*2+ 1 >= heapsize 时跳出)
代码实现:
public void heapIfy(int[] alist, int location, int heapsize){ for (int j = location*2+1; j < heapsize; j = 2*j +1){ j = ((j + 1) < heapsize && alist[j] < alist[j + 1]) ? j+1 : j; if (alist[location] >= alist[j]) { break; } swap(alist, location, j); location = j; } }
构建初始堆:
for (int i = (alist.length-1-1)/2; i >= 0 ; i--) { heapIfy(alist, i, alist.length); }
堆排序算法
思想(大顶堆举例):将待排序的序列构造成一个大顶堆。此时,整个序列的最大值就是堆顶的根节点。将它移走(把它和堆数组尾部元素交换,此时末尾就是最大值),然后将剩余n-1个序列重新构造成一个堆,这样就会得到n个元素中的次大值。如此反复执行,就能得到一个有序序列。
堆的复杂度分析:
运行时间主要是消耗在初始构建堆和重建堆时的反复筛选上。
构建堆从非终端节点开始构建,将它与其孩子进行比较和若有必要的呼唤,对于每个非终端节点来说,其实最多进行两次比较和互换操作,因此构建堆的时间复杂度:O(n)。
正式排序时,第i次取栈顶记录重建堆需要用O(logi)的时间(完全二叉树的某个节点到根结点距离为「logi」+1),并且需要取n-1此堆顶记录,因此,重建堆的时间复杂度为O(nlogn)
总体来说,堆排序的时间复杂度为O(nlogn),由于堆排序对原始记录的排序状态并不敏感,因此它无论是最好、最坏和平均时间复杂度均为O(nlogn)。性能上要远远好过于冒泡、简单选择、直接插入的O(n^2)的时间复杂度。
空间复杂度上,只有一个用来交换的暂存单元,不过由于记录的比较与交换是跳跃式进行,因此堆排序也是一种不稳定的排序方法。由于初始构建堆所需的比较次数较多,因此,它并不适合排序序列个数较少的情况。
堆排序的Java代码实现:
public class HeapSort { public void heapIfy(int[] alist, int location, int heapsize){ for (int j = location*2+1; j < heapsize; j = 2*j +1){ j = ((j + 1) < heapsize && alist[j] < alist[j + 1]) ? j+1 : j; if (alist[location] >= alist[j]) { break; } swap(alist, location, j); location = j; } } public void swap(int[] alist, int x, int y){ int temp; temp = alist[x]; alist[x] = alist[y]; alist[y] = temp; } public void heapSort(int[] alist){ for (int i = (alist.length-1-1)/2; i >= 0 ; i--) { heapIfy(alist, i, alist.length); } //构建玩大顶堆的结果 System.out.println("构建大顶堆的结果:"); for (int i = 0; i < alist.length; i++) { System.out.print(alist[i]); System.out.print(" "); } System.out.println(" "); for (int i = 1; i < alist.length; i++) { swap(alist, 0, alist.length-i); heapIfy(alist, 0, alist.length-i); System.out.println("交换堆顶,重新调整为大顶堆"); for (int j = 0; j < alist.length; j++) { System.out.print(alist[j]); System.out.print(" "); } System.out.println(" "); } } public static void main(String[] args) { int[] alist = {54,26,93,17,77,31,44,55,20}; for (int i = 0; i < alist.length; i++) { System.out.print(alist[i]); System.out.print(" "); } System.out.println(" "); HeapSort sort = new HeapSort(); sort.heapSort(alist); System.out.println("Heap Sort 最终结果:"); for (int i = 0; i < alist.length; i++) { System.out.print(alist[i]); System.out.print(" "); } } }
掌握了堆好处案例:
案例:一个流不断吐出数,随时能找到中位数:
解决方法:
1. 笨方法
一个大数组存进去,要中位数的时候,要先排序,存入是O(1)代价,排序又是O(N*logN),每次来一个数都要排序,如果调用中位数次数很多,代价很大。
2. 采取堆结构
准备两个1000的数组,一个做一个大根堆,另一个叫小根堆。如果新来一个数比大根堆头部小,进入大根堆。做到吐出的数,较小的放到大根堆,较大的放到小根堆。如果大根堆的数目比小根堆的多2,弹出大根堆堆顶,放到小根堆,同理,小根堆比大根堆多的,丢到大根堆去。
最后结果:N个数,N/2存放到大根堆,N/2存放到小根堆,大根堆堆顶是较小的N/2的最大值,小根堆堆顶是较大的N/2个数字的最小值,这样随时能找到中位数。
优先级队列不是双向链表,而是堆
三、补充
3.1 拓扑排序
AOV网:在一个表示工程的有向图,用顶点表示活动,用弧表示活动之间的优先关系,这样的有向图为顶点表示活动的网,我们称之为AOV网(Activity On Vertex Network)
拓扑序列
设G=(V,E)是一个具有n个顶点的有向图,V中的顶点序列v1,v2,…,vn,满足若从顶点vi到vj有一条路径,则在顶点序列中顶点vi必在顶点vj之前(描述的是拓扑图的前后关系)。则我们称这样的顶点序列尾一个拓扑序列。
拓扑排序
对一个有向图构造拓扑序列的过程
构造时有两个结果:
- 全部节点被输出:说明不存在环的AOV网
- 如果输出顶点数少了,哪怕一个,说明这个网存在环,不是AOV网。
算法思想:从AOV网中选择一个入度为0的顶点输出,然后删去此顶点,并删除此顶点为尾的弧,继续重复此步骤,直到输出全部顶点或者AOV网中不存在入度为0的顶点为止
需要由栈来实现
3.2 基数排序(Radix Sort)
基本思想:
将整数按位数切割成不同的数字,然后按每个位数分别比较
具体做法:
将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。
基数排序图文说明:
3.3 计数排序
计数排序的核心思想:我们对一个整数数组进行排序时,可以创建一个辅助数组,大小为待排序数组最大元素和最小元素的差值(时间复杂度O(n+k)),根据待排序数组元素的值,直接确定位置。
例子:排序的数为 7 4 2 1 5 3 1 5;则比7小的有7个数,所有7应该在排序好的数列的第八位,同理3在第四位,对于重复的数字,1在1位和2位(暂且认为第一个1比第二个1小),5和1一样位于6位和7位。
小学生站队问题
假设有一个班级的小学生去操场上体育课,体育老师要求他们按照身高次序站成一队。每个小学生都知道自己的具体身高是多少厘米(假设每个小学生身高都不一样,否则就会为争执位置打架),但每个小学生都不承认别人比自己高,非得互相询问身高比较才能承认,大多数小学生在经过很多次询问和被询问后才能老老实实站在自己的位置上。场面叽叽喳喳混乱不已,快下课才排好队。
体育老师十分头疼,(于是,以后每次要上体育课,英语老师都会进班级说:体育老师请假了,这节课我来上。)于是想到了一个好办法。他先得到班级最高和最矮学生的身高,假定是111cm到170cm,然后在操场上,每隔半步距离用树粉笔写上一个数字,数字从最矮的身高111开始,112,113,...一直写到170。上课的时候,老师要求同学们根据自己的身高,站到相应的位置,然后大叫一声:向右看齐!所有的小学生都有序站成一队了!