前言:
学习算法,不论对思考问题的方式还是对编程的思维都会有很大的好处。
而数据结构在算法的这个概念里是非常重要的,学好了数据结构就能写出更好的代码,所以必须花费精力和时间去好好学习。
要想学好数据结构就必须先掌握好基础。本节对数据结构里的两种基本的线性表(顺序表和单链表)的基本内容进行了详细的描述,理解并且熟练运用这些很重要。
正文:
首先来说什么是时间复杂度和空间复杂度?
复杂度:粗鲁衡量算法好坏的刻度尺(工具)
两个维度:快慢 时间复杂度(重点)
使用空间的情况 空间复杂度
时间复杂度:
直接利用允许时间衡量不现实。测试环境多变,不好控制变量
前提:如果指定CPU的情况下,单位时间内运行的基本指令个数是固定的
如果一个算法需要的指令比另一个算法需要的指令个数小,就可以推出县一个算法的时间复杂度更小
前提:算法计算的快慢和输入数据的规模是有关系的
粗略计算算法的快慢
n:数据规模
f(n):n的数据规模情况下,需要的大概基本指令个数
引入大O渐进表示法
O(f(n))
1)只保留最高次项
2)保留的最高次项系数化为1
算法的快慢分为最好情况、平均情况、最坏情况
一般优先关注最坏情况,其次是平均情况,最后是最好情况。
空间复杂度:
O(F(n)) 在n输入规模的情况下,算法需要的最大的空间情况(额外的)
1)开辟数组
2)调用栈(递归方法)
1.顺序表
用Java实现顺序表的 增 、删、查、改
public class MyArrayList {
private int[] array; // 代表的是存在数据的数组
private int size; // 记录顺序表的已有数据个数
public MyArrayList() {
// 1. 申请空间
array = new int[2];
// 2. 初始化数据个数
size = 0;
}
扩容:扩容的空间越小,空间的浪费越小
扩容的空间越大,需要扩容的频率越少
经验值 大概是1.5倍或2倍
// 扩容
public void ensureCapacity(){
if(size<array.length){
return;
}
int newCapacity=array.length*2;
int[] newArray= new int[newCapacity];
for(int i=0;i<size;i++){
newArray[i]=array[i];
}
array=newArray;
}
尾插
//尾插
public void pushBack(int element){
ensureCapacity();
array[size++]=element;
}
头插
//头插
public void pushFront(int element){
ensureCapacity();
for(int i=size-1;i>=0;i--){
array[i+1]=array[i];
}
array[0]=element;
size++;
}
按下标插入指定位置
//插入指定位置
public void insert(int index,int element){
if(index < 0 || index > size){
System.err.println("插入位置错误");
return;
}
ensureCapacity();
for (int i = size - 1; i >= index; i--) {
array[i + 1] = array[i];
}
array[index] = element;
size++;
}
尾删
//尾删
public void popBack(){
if(size<=0){
System.out.println("顺序表为空");
return;
}
array[--size]=0;
}
头删
//头删
public void popFront(){
if(size<=0){
System.out.println("顺序表为空");
return;
}
for(int i=1;i<size;i++){
array[i-1]=array[i];
}
array[--size]=0;
}
按下标删除指定位置元素
//删除指定位置元素
public void earse(int index) {
if (size <= 0) {
System.err.println("顺序表为空");
return;
}
if (index < 0 || index >= size) {
System.err.println("下标错误");
return;
}
for (int i = index + 1; i < size; i++) {
array[i - 1] = array[i];
}
array[--size] = 0;
}
按元素查找
//查找
public int indexOf(int element){
for(int i=0;i<size;i++){
if(array[i]==element){
return i;
}
}
return -1;
}
按下标查找
public int get(int index){
if(index<0 || index>=size){
System.err.println("下标错误");
return -1;
}
return array[index];
}
只删除第一个出现的元素
//删除掉某一元素,如果多次出现,删除第一次出现的
public void remove(int element){
int index=indexOf(element);
if(index!=-1){
earse(index);
}
else{
System.out.println("元素不存在");
}
全部删除
下面三种方法都能实现,只是按复杂度越来越优
多想几种方法,防止思维定势,选取较好的来编程
public void removeAll(int element){
int index;
while((index=indexOf(element))!=-1){
earse(index);
}
}
public void removeAll(int element){
int j=0;
int[] newArray = new int[array.length];
for(int i=0;i<size;i++){
if(array[i]!=element){
newArray[j++]=array[i];
}
}
}
public void removeAll(int element){
int j=0;
for(int i=0;i<size;i++){
if(array[i]!=element){
array[j++]=array[i];
}
size=j;
}
}
修改
//修改
public void set(int index,int element){
if(index<0 || index>=size){
System.err.println("下标错误");
return ;
}
array[index]=element;
}
打印
//打印
public void print() {
System.out.println("打印顺序表: 当前容量: " + array.length);
for (int i = 0; i < size; i++) {
System.out.print(array[i] + " ");
}
System.out.println();
}
下面两个程序很短也写在方法里,很好的体现了Java的封装性
public int size(){
return size;
}
public boolean isEmpty(){
return size==0;
}
最后进行测试,查看结果与预期的结果是否一致
public static void main(String[] args){
MyArrayList list= new MyArrayList();
list.print();
list.pushBack(1);
list.pushBack(2);
list.pushBack(3);
list.print(); // 1 2 3
list.pushFront(10);
list.pushFront(20);
list.pushFront(30);
list.print(); // 30 20 10 1 2 3
list.insert(3, 100);
list.print(); // 30 20 10 100 1 2 3
list.insert(20, 200); // 报错
list.earse(2);
list.earse(2);
list.print(); // 30 20 1 2 3
list.popFront();
list.popFront();
list.popFront();
list.print(); // 2 3
list.popBack();
list.popBack();
list.print(); // 空的
list.popBack(); // 报错
}
}
2.单链表
单链表的增和删
class Node {
int val;
Node next;
Node(int val) {
this.val = val;
this.next = null;
}
public String toString() {
return String.format("Node(%d)", val);
}
}
public class MyLinkedList {
1.头插
head: 原来的第一个结点
val:要插入的值
返回:新的第一个结点
private static Node pushFront(Node head, int val) {
// 1. 结点
Node node = new Node(val);
// 2. 让原来的 head 成为 node 的下一个结点
node.next = head;
// 3. 更新第一个结点的引用
return node;
}
2.尾插
尾插需要分情况讨论:
1.空链表的情况
就是让新的结点成为第一个结点
2.非空链表
如果要插入的数据没有结点,先给他装入一个结点中
Node node = new Node(val);
让新结点的next=null;
找到倒数第一个结点(子问题),找到.next=null的结点
Node last=head;
while(last.next!=null){last=last.next}
last就是最后一个结点
让原来的倒数第一个结点的下一个为新结点
last.next=node;
private static Node pushBack(Node head, int val) {
Node node = new Node(val);
if (head == null) {
return node;
} else {
Node last = head;
while (last.next != null) {
last = last.next;
}
last.next = node;
return head;
}
}
3.头删
head=head.next; 原来的第一个结点会因为没有指向被回收
private static Node popFront(Node head) {
if (head == null) {
System.err.println("空链表无法删除");
return null;
}
// 原来第一个结点,会因为没有引用指向而被回收
return head.next;
}
4.尾删
尾删分情况:
1.只有一个结点
head.nextnull;
return null;
2.有两个以上的结点
1.找到倒数第二个结点
Node lastSecond=head;
LastSecond.next.nextnull
LastSecond=lastSecond.next;
2.让倒数第二个结点的.next为空
private static Node popBack(Node head) {
if (head == null) {
System.err.println("空链表无法删除");
return null;
}
if (head.next == null) {
return null;
} else {
Node lastSecond = head;
while (lastSecond.next.next != null) {
lastSecond = lastSecond.next;
}
lastSecond.next = null;
return head;
}
}
5.打印
这里的print(cur)等于print(cur.toString)
// 打印
private static void print(Node head) {
System.out.println("打印链表:");
for (Node cur = head; cur != null; cur = cur.next) {
System.out.print(cur + " --> ");
}
System.out.println("null");
}
最后进行测试
public static void main(String[] args){
Node head=null;
head=pushFront(head,1);
head=pushFront(head,2);
head=pushFront(head,3);
print(head); //3,2,1
head=pushBack(head,5);
head=pushBack(head,6);
head=pushBack(head,7);
print(head); // 3,2,1,5,6,7
head=popFront(head);//2,1,5,6,7
print(head);
head=popBack(head); //2,1,5,6
print(head);
}
要学会画图来理解单链表。