在React Native鸿蒙跨平台开发中实现 二叉搜索树,如何实现一些基本的遍历方法,如中序遍历,中序遍历按顺序访问左子树、根节点、右子树

在 React Native 中生成一个二叉搜索树(Binary Search Tree,BST)涉及到几个步骤,包括定义二叉搜索树的节点结构、实现插入和遍历等基本操作。二叉搜索树是一种常见的数据结构,其中每个节点都包含一个键(key)以及指向其子节点的链接,且每个节点的键值都满足左子树节点的键值小于父节点,右子树节点的键值大于父节点。

步骤 1: 定义二叉搜索树的节点

首先,我们需要定义一个节点类或结构,用于存储每个节点的值以及指向其左右子节点的引用。

javascript 复制代码
class TreeNode {
  constructor(value) {
    this.value = value;
    this.left = null;
    this.right = null;
  }
}

步骤 2: 实现二叉搜索树类

接下来,我们定义一个二叉搜索树类,该类包含插入节点的方法。

javascript 复制代码
class BinarySearchTree {
  constructor() {
    this.root = null;
  }

  // 插入节点的方法
  insert(value) {
    const newNode = new TreeNode(value);
    if (this.root === null) {
      this.root = newNode;
    } else {
      this.insertNode(this.root, newNode);
    }
  }

  // 递归插入节点的方法
  insertNode(node, newNode) {
    if (newNode.value < node.value) {
      if (node.left === null) {
        node.left = newNode;
      } else {
        this.insertNode(node.left, newNode);
      }
    } else {
      if (node.right === null) {
        node.right = newNode;
      } else {
        this.insertNode(node.right, newNode);
      }
    }
  }
}

步骤 3: 遍历二叉搜索树(可选)

为了验证树的结构是否正确,我们可以实现一些基本的遍历方法,如中序遍历。中序遍历按顺序访问左子树、根节点、右子树。

javascript 复制代码
inorderTraversal(node, callback) {
  if (node !== null) {
    this.inorderTraversal(node.left, callback); // 访问左子树
    callback(node.value); // 访问根节点
    this.inorderTraversal(node.right, callback); // 访问右子树
  }
}

步骤 4: 在 React Native 中使用 BST

最后,在 React Native 应用中使用这个 BST。例如,你可以创建一个组件来展示树的插入和遍历:

javascript 复制代码
import React, { useState } from 'react';
import { View, Text, Button } from 'react-native';
import { BinarySearchTree } from './BinarySearchTree'; // 确保路径正确

const BSTExample = () => {
  const [bst, setBst] = useState(new BinarySearchTree());
  const [values, setValues] = useState([]); // 用于存储遍历结果

  const insertValue = (value) => {
    bst.insert(value);
    setValues([]); // 清空当前值以便重新遍历显示新树结构
  };

  const traverseTree = () => {
    const result = [];
    bst.inorderTraversal(bst.root, (value) => result.push(value)); // 使用回调函数收集结果
    setValues(result); // 更新状态以重新渲染列表显示结果
  };
  
  return (
    <View>
      <Button title="Insert 5" onPress={() => insertValue(5)} />
      <Button title="Insert 3" onPress={() => insertValue(3)} />
      <Button title="Insert 8" onPress={() => insertValue(8)} />
      <Button title="Traverse" onPress={traverseTree} />
      <Text>Inorder Traversal Result:</Text>
      {values.map((value, index) => <Text key={index}>{value}</Text>)}
    </View>
  );
};

这样,你就可以在 React Native 应用中创建一个基本的二叉搜索树,并对其进行操作和展示了。


真实场景代码演示效果:

js 复制代码
import React, { useState } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, ScrollView, Dimensions, Image } from 'react-native';

// Simple Icon Component using Unicode symbols
interface IconProps {
  name: string;
  size?: number;
  color?: string;
  style?: object;
}

const Icon: React.FC<IconProps> = ({ 
  name, 
  size = 24, 
  color = '#333333',
  style 
}) => {
  const getIconSymbol = () => {
    switch (name) {
      case 'prev': return '‹';
      case 'next': return '›';
      case 'today': return '◎';
      case 'event': return '•';
      case 'date': return '📅';
      case 'month': return '📅';
      case 'year': return '📅';
      default: return '●';
    }
  };

  return (
    <View style={[{ width: size, height: size, justifyContent: 'center', alignItems: 'center' }, style]}>
      <Text style={{ fontSize: size * 0.8, color, includeFontPadding: false, textAlign: 'center' }}>
        {getIconSymbol()}
      </Text>
    </View>
  );
};

// Calendar Component
interface CalendarProps {
  events?: { date: string; title: string }[];
  onDateSelect?: (date: string) => void;
  selectedDate?: string;
}

const Calendar: React.FC<CalendarProps> = ({ 
  events = [], 
  onDateSelect,
  selectedDate 
}) => {
  const [currentDate, setCurrentDate] = useState(new Date());
  
  // Get days in month
  const getDaysInMonth = (year: number, month: number) => {
    return new Date(year, month + 1, 0).getDate();
  };
  
  // Get first day of month (0 = Sunday, 1 = Monday, etc)
  const getFirstDayOfMonth = (year: number, month: number) => {
    return new Date(year, month, 1).getDay();
  };
  
  // Format date as YYYY-MM-DD
  const formatDate = (date: Date) => {
    const year = date.getFullYear();
    const month = String(date.getMonth() + 1).padStart(2, '0');
    const day = String(date.getDate()).padStart(2, '0');
    return `${year}-${month}-${day}`;
  };
  
  // Check if date has events
  const hasEvents = (dateStr: string) => {
    return events.some(event => event.date === dateStr);
  };
  
  // Navigate to previous month
  const prevMonth = () => {
    setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() - 1, 1));
  };
  
  // Navigate to next month
  const nextMonth = () => {
    setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 1));
  };
  
  // Go to today
  const goToToday = () => {
    const today = new Date();
    setCurrentDate(new Date(today.getFullYear(), today.getMonth(), 1));
    if (onDateSelect) {
      onDateSelect(formatDate(today));
    }
  };
  
  // Render calendar header
  const renderHeader = () => {
    const year = currentDate.getFullYear();
    const month = currentDate.toLocaleString('zh-CN', { month: 'long' });
    
    return (
      <View style={styles.calendarHeader}>
        <TouchableOpacity onPress={prevMonth} style={styles.navButton}>
          <Icon name="prev" size={24} color="#4a5568" />
        </TouchableOpacity>
        
        <View style={styles.monthYearContainer}>
          <Text style={styles.monthText}>{month}</Text>
          <Text style={styles.yearText}>{year}年</Text>
        </View>
        
        <TouchableOpacity onPress={nextMonth} style={styles.navButton}>
          <Icon name="next" size={24} color="#4a5568" />
        </TouchableOpacity>
      </View>
    );
  };
  
  // Render weekdays
  const renderWeekdays = () => {
    const weekdays = ['日', '一', '二', '三', '四', '五', '六'];
    
    return (
      <View style={styles.weekdaysContainer}>
        {weekdays.map((day, index) => (
          <View key={index} style={styles.weekdayCell}>
            <Text style={[styles.weekdayText, index === 0 && styles.sundayText]}>{day}</Text>
          </View>
        ))}
      </View>
    );
  };
  
  // Render calendar days
  const renderDays = () => {
    const year = currentDate.getFullYear();
    const month = currentDate.getMonth();
    const daysInMonth = getDaysInMonth(year, month);
    const firstDayOfMonth = getFirstDayOfMonth(year, month);
    
    const today = new Date();
    const isCurrentMonth = today.getFullYear() === year && today.getMonth() === month;
    const todayDate = today.getDate();
    
    const days = [];
    
    // Previous month's days
    const prevMonthDays = getDaysInMonth(year, month - 1);
    for (let i = firstDayOfMonth - 1; i >= 0; i--) {
      const day = prevMonthDays - i;
      days.push(
        <View key={`prev-${day}`} style={styles.dayCell}>
          <Text style={styles.otherMonthDay}>{day}</Text>
        </View>
      );
    }
    
    // Current month's days
    for (let day = 1; day <= daysInMonth; day++) {
      const dateObj = new Date(year, month, day);
      const dateStr = formatDate(dateObj);
      const isToday = isCurrentMonth && day === todayDate;
      const isSelected = selectedDate === dateStr;
      const hasEvent = hasEvents(dateStr);
      
      days.push(
        <TouchableOpacity 
          key={`curr-${day}`} 
          style={[
            styles.dayCell,
            isToday && styles.todayCell,
            isSelected && styles.selectedCell
          ]}
          onPress={() => onDateSelect && onDateSelect(dateStr)}
        >
          <View style={styles.dayNumberContainer}>
            <Text style={[
              styles.dayNumber,
              isToday && styles.todayText,
              isSelected && styles.selectedText
            ]}>
              {day}
            </Text>
            {hasEvent && (
              <View style={styles.eventIndicator}>
                <Icon name="event" size={8} color="#e53e3e" />
              </View>
            )}
          </View>
        </TouchableOpacity>
      );
    }
    
    // Next month's days
    const totalCells = 42; // 6 rows * 7 days
    const remainingCells = totalCells - days.length;
    for (let day = 1; day <= remainingCells; day++) {
      days.push(
        <View key={`next-${day}`} style={styles.dayCell}>
          <Text style={styles.otherMonthDay}>{day}</Text>
        </View>
      );
    }
    
    return (
      <View style={styles.daysContainer}>
        {days}
      </View>
    );
  };
  
  // Render today button
  const renderTodayButton = () => {
    return (
      <TouchableOpacity style={styles.todayButton} onPress={goToToday}>
        <Icon name="today" size={16} color="#4a5568" style={styles.todayIcon} />
        <Text style={styles.todayButtonText}>今天</Text>
      </TouchableOpacity>
    );
  };

  return (
    <View style={styles.calendarContainer}>
      {renderHeader()}
      {renderWeekdays()}
      {renderDays()}
      {renderTodayButton()}
    </View>
  );
};

// Main App Component
const CalendarComponentApp = () => {
  const [selectedDate, setSelectedDate] = useState<string>('');
  const [events] = useState([
    { date: new Date().toISOString().split('T')[0], title: '会议' },
    { date: new Date(Date.now() + 86400000).toISOString().split('T')[0], title: '生日' },
    { date: new Date(Date.now() + 172800000).toISOString().split('T')[0], title: '假期' },
  ]);

  const handleDateSelect = (date: string) => {
    setSelectedDate(date);
  };

  return (
    <ScrollView style={styles.container}>
      <View style={styles.header}>
        <Text style={styles.headerTitle}>二叉搜索树组件</Text>
        <Text style={styles.headerSubtitle}>插入/删除/查找与遍历演示</Text>
      </View>

      <View style={styles.section}>
        <Text style={styles.sectionTitle}>二叉搜索树(可视化 · 森林风格)</Text>
        <View style={styles.bstContainer}>
          <BSTVisualizer />
        </View>
      </View>
      
      <View style={styles.footer}>
        <Text style={styles.footerText}>© 2023 数据结构演示 | 现代化UI组件库</Text>
      </View>
    </ScrollView>
  );
};

const { width } = Dimensions.get('window');

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#f7fafc',
  },
  header: {
    backgroundColor: '#ffffff',
    paddingVertical: 30,
    paddingHorizontal: 20,
    marginBottom: 10,
    borderBottomWidth: 1,
    borderBottomColor: '#e2e8f0',
  },
  headerTitle: {
    fontSize: 28,
    fontWeight: '700',
    color: '#2d3748',
    textAlign: 'center',
    marginBottom: 5,
  },
  headerSubtitle: {
    fontSize: 16,
    color: '#718096',
    textAlign: 'center',
  },
  section: {
    marginBottom: 25,
  },
  sectionTitle: {
    fontSize: 20,
    fontWeight: '700',
    color: '#2d3748',
    paddingHorizontal: 20,
    paddingBottom: 15,
  },
  calendarWrapper: {
    backgroundColor: '#ffffff',
    marginHorizontal: 15,
    borderRadius: 15,
    padding: 15,
    elevation: 4,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 8,
  },
  selectedDateCard: {
    backgroundColor: '#ebf8ff',
    marginHorizontal: 15,
    borderRadius: 12,
    padding: 20,
    borderWidth: 1,
    borderColor: '#bee3f8',
  },
  selectedDateText: {
    fontSize: 18,
    fontWeight: '700',
    color: '#2b6cb0',
    textAlign: 'center',
    marginBottom: 5,
  },
  selectedDateDesc: {
    fontSize: 14,
    color: '#4a5568',
    textAlign: 'center',
  },
  demosContainer: {
    backgroundColor: '#ffffff',
    marginHorizontal: 15,
    borderRadius: 15,
    padding: 20,
    elevation: 3,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.08,
    shadowRadius: 4,
  },
  demoItem: {
    flexDirection: 'row',
    alignItems: 'center',
    marginBottom: 20,
  },
  demoItemLast: {
    marginBottom: 0,
  },
  demoIcon: {
    marginRight: 15,
  },
  demoTitle: {
    fontSize: 16,
    fontWeight: '600',
    color: '#2d3748',
    marginBottom: 3,
  },
  demoDesc: {
    fontSize: 14,
    color: '#718096',
  },
  usageSection: {
    backgroundColor: '#ffffff',
    marginHorizontal: 15,
    borderRadius: 15,
    padding: 20,
    marginBottom: 20,
    elevation: 3,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.08,
    shadowRadius: 4,
  },
  codeBlock: {
    backgroundColor: '#2d3748',
    borderRadius: 8,
    padding: 15,
    marginBottom: 15,
  },
  codeText: {
    fontFamily: 'monospace',
    color: '#e2e8f0',
    fontSize: 14,
    lineHeight: 22,
  },
  description: {
    fontSize: 15,
    color: '#4a5568',
    lineHeight: 22,
  },
  featuresSection: {
    backgroundColor: '#ffffff',
    marginHorizontal: 15,
    borderRadius: 15,
    padding: 20,
    marginBottom: 20,
    elevation: 3,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.08,
    shadowRadius: 4,
  },
  featuresList: {
    paddingLeft: 10,
  },
  featureItem: {
    flexDirection: 'row',
    alignItems: 'center',
    marginBottom: 15,
  },
  featureIcon: {
    marginRight: 15,
  },
  featureText: {
    fontSize: 16,
    color: '#2d3748',
  },
  footer: {
    paddingVertical: 20,
    alignItems: 'center',
  },
  footerText: {
    color: '#a0aec0',
    fontSize: 14,
  },
  // Calendar Styles
  calendarContainer: {
    backgroundColor: '#ffffff',
  },
  calendarHeader: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    paddingHorizontal: 10,
    paddingVertical: 15,
  },
  navButton: {
    padding: 10,
  },
  monthYearContainer: {
    alignItems: 'center',
  },
  monthText: {
    fontSize: 18,
    fontWeight: '700',
    color: '#2d3748',
  },
  yearText: {
    fontSize: 14,
    color: '#718096',
  },
  weekdaysContainer: {
    flexDirection: 'row',
    borderBottomWidth: 1,
    borderBottomColor: '#edf2f7',
    paddingBottom: 10,
    marginBottom: 5,
  },
  weekdayCell: {
    flex: 1,
    alignItems: 'center',
  },
  weekdayText: {
    fontSize: 14,
    fontWeight: '600',
    color: '#4a5568',
  },
  sundayText: {
    color: '#e53e3e',
  },
  daysContainer: {
    flexDirection: 'row',
    flexWrap: 'wrap',
  },
  dayCell: {
    width: '14.28%',
    aspectRatio: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  dayNumberContainer: {
    position: 'relative',
  },
  dayNumber: {
    fontSize: 16,
    color: '#4a5568',
    fontWeight: '500',
  },
  otherMonthDay: {
    fontSize: 16,
    color: '#cbd5e0',
  },
  todayCell: {
    backgroundColor: '#ebf8ff',
    borderRadius: 30,
  },
  todayText: {
    color: '#3182ce',
    fontWeight: '700',
  },
  selectedCell: {
    backgroundColor: '#3182ce',
    borderRadius: 30,
  },
  selectedText: {
    color: '#ffffff',
    fontWeight: '700',
  },
  eventIndicator: {
    position: 'absolute',
    top: -3,
    right: -3,
  },
  todayButton: {
    flexDirection: 'row',
    justifyContent: 'center',
    alignItems: 'center',
    paddingVertical: 15,
    borderTopWidth: 1,
    borderTopColor: '#edf2f7',
    marginTop: 5,
  },
  todayIcon: {
    marginRight: 8,
  },
  todayButtonText: {
    fontSize: 16,
    fontWeight: '600',
    color: '#4a5568',
  },
  msContainer: {
    marginHorizontal: 15,
  },
  msCard: {
    backgroundColor: '#0f141c',
    borderRadius: 16,
    borderWidth: 1,
    borderColor: '#1f2a36',
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 6 },
    shadowOpacity: 0.18,
    shadowRadius: 14,
    paddingVertical: 14,
    paddingHorizontal: 12,
  },
  msHeader: {
    marginBottom: 8,
  },
  msTitle: {
    fontSize: 18,
    fontWeight: '700',
    color: '#e6f1ff',
  },
  msSubtitle: {
    fontSize: 13,
    color: '#8aa0b8',
    marginTop: 4,
  },
  msControls: {
    flexDirection: 'row',
    alignItems: 'center',
    flexWrap: 'wrap',
    marginTop: 8,
    marginBottom: 12,
  },
  msBtn: {
    flexDirection: 'row',
    alignItems: 'center',
    backgroundColor: '#1a2433',
    borderWidth: 1,
    borderColor: '#28374d',
    borderRadius: 12,
    paddingVertical: 8,
    paddingHorizontal: 10,
    marginRight: 8,
  },
  msBtnActive: {
    backgroundColor: '#1f2d44',
    borderColor: '#34507a',
  },
  msIcon: {
    width: 20,
    height: 20,
    marginRight: 6,
  },
  msBtnText: {
    color: '#e6f1ff',
    fontSize: 13,
    fontWeight: '600',
  },
  msChips: {
    flexDirection: 'row',
    alignSelf: 'flex-end',
    marginLeft: 8,
  },
  msChip: {
    backgroundColor: '#182030',
    borderWidth: 1,
    borderColor: '#26364d',
    borderRadius: 10,
    paddingVertical: 6,
    paddingHorizontal: 10,
    marginLeft: 6,
  },
  msChipActive: {
    backgroundColor: '#213049',
    borderColor: '#385782',
  },
  msChipText: {
    color: '#cfe1ff',
    fontSize: 12,
    fontWeight: '600',
  },
  msBars: {
    flexDirection: 'row',
    alignItems: 'flex-end',
    justifyContent: 'space-between',
    paddingVertical: 12,
    paddingHorizontal: 6,
    minHeight: 220,
  },
  msBar: {
    width: 16,
    backgroundColor: '#2f7de1',
    borderTopLeftRadius: 8,
    borderTopRightRadius: 8,
    borderBottomLeftRadius: 2,
    borderBottomRightRadius: 2,
    justifyContent: 'flex-end',
    alignItems: 'center',
    marginHorizontal: 2,
  },
  msBarHL: {
    backgroundColor: '#49a2ff',
  },
  msBarText: {
    fontSize: 10,
    color: '#e6f1ff',
    marginBottom: 4,
    fontWeight: '600',
  },
  msFooterRow: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    paddingTop: 6,
  },
  msFootText: {
    color: '#8aa0b8',
    fontSize: 12,
  },
  msDone: {
    color: '#43d998',
    fontWeight: '700',
  },
  bsContainer: {
    marginHorizontal: 15,
  },
  bsCard: {
    backgroundColor: '#ffffff',
    borderRadius: 16,
    borderWidth: 1,
    borderColor: '#e7eef7',
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 6 },
    shadowOpacity: 0.08,
    shadowRadius: 12,
    paddingVertical: 14,
    paddingHorizontal: 12,
  },
  bsHeader: {
    marginBottom: 8,
  },
  bsTitle: {
    fontSize: 18,
    fontWeight: '700',
    color: '#2d3748',
  },
  bsSubtitle: {
    fontSize: 13,
    color: '#718096',
    marginTop: 4,
  },
  bsControls: {
    flexDirection: 'row',
    alignItems: 'center',
    flexWrap: 'wrap',
    marginTop: 8,
    marginBottom: 12,
  },
  bsBtn: {
    flexDirection: 'row',
    alignItems: 'center',
    backgroundColor: '#f1f5fb',
    borderWidth: 1,
    borderColor: '#e1eaf6',
    borderRadius: 12,
    paddingVertical: 8,
    paddingHorizontal: 10,
    marginRight: 8,
  },
  bsBtnActive: {
    backgroundColor: '#e7eef7',
    borderColor: '#d9e6f5',
  },
  bsIcon: {
    width: 20,
    height: 20,
    marginRight: 6,
  },
  bsBtnText: {
    color: '#2d3748',
    fontSize: 13,
    fontWeight: '600',
  },
  bsTargetRow: {
    flexDirection: 'row',
    alignItems: 'center',
    marginTop: 6,
  },
  bsChip: {
    backgroundColor: '#edf2f7',
    borderWidth: 1,
    borderColor: '#e2e8f0',
    borderRadius: 10,
    paddingVertical: 6,
    paddingHorizontal: 10,
    marginLeft: 6,
  },
  bsChipText: {
    color: '#2d3748',
    fontSize: 12,
    fontWeight: '600',
  },
  bsBars: {
    flexDirection: 'row',
    alignItems: 'flex-end',
    justifyContent: 'space-between',
    paddingVertical: 12,
    paddingHorizontal: 6,
    minHeight: 220,
  },
  bsBar: {
    width: 16,
    backgroundColor: '#90cdf4',
    borderTopLeftRadius: 8,
    borderTopRightRadius: 8,
    borderBottomLeftRadius: 2,
    borderBottomRightRadius: 2,
    justifyContent: 'flex-end',
    alignItems: 'center',
    marginHorizontal: 2,
  },
  bsBarDim: {
    backgroundColor: '#c5d8ee',
  },
  bsBarMid: {
    backgroundColor: '#63b3ed',
  },
  bsBarTarget: {
    backgroundColor: '#68d391',
  },
  bsBarText: {
    fontSize: 10,
    color: '#2d3748',
    marginBottom: 4,
    fontWeight: '600',
  },
  bsFooterRow: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    paddingTop: 6,
  },
  bsFootText: {
    color: '#718096',
    fontSize: 12,
  },
  bsDone: {
    color: '#2e7d32',
    fontWeight: '700',
  },
  lsContainer: {
    marginHorizontal: 15,
  },
  lsCard: {
    backgroundColor: '#fff7ed',
    borderRadius: 16,
    borderWidth: 1,
    borderColor: '#fde4d6',
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 6 },
    shadowOpacity: 0.08,
    shadowRadius: 12,
    paddingVertical: 14,
    paddingHorizontal: 12,
  },
  lsHeader: {
    marginBottom: 8,
  },
  lsTitle: {
    fontSize: 18,
    fontWeight: '700',
    color: '#7c2d12',
  },
  lsSubtitle: {
    fontSize: 13,
    color: '#9a3412',
    marginTop: 4,
  },
  lsControls: {
    flexDirection: 'row',
    alignItems: 'center',
    flexWrap: 'wrap',
    marginTop: 8,
    marginBottom: 12,
  },
  lsBtn: {
    flexDirection: 'row',
    alignItems: 'center',
    backgroundColor: '#fff1e6',
    borderWidth: 1,
    borderColor: '#ffe1cc',
    borderRadius: 12,
    paddingVertical: 8,
    paddingHorizontal: 10,
    marginRight: 8,
  },
  lsBtnActive: {
    backgroundColor: '#ffe8d6',
    borderColor: '#ffd9bf',
  },
  lsIcon: {
    width: 20,
    height: 20,
    marginRight: 6,
  },
  lsBtnText: {
    color: '#7c2d12',
    fontSize: 13,
    fontWeight: '600',
  },
  lsTargetRow: {
    flexDirection: 'row',
    alignItems: 'center',
    marginTop: 6,
  },
  lsChip: {
    backgroundColor: '#ffedd5',
    borderWidth: 1,
    borderColor: '#fed7aa',
    borderRadius: 10,
    paddingVertical: 6,
    paddingHorizontal: 10,
    marginLeft: 6,
  },
  lsChipText: {
    color: '#7c2d12',
    fontSize: 12,
    fontWeight: '600',
  },
  lsBars: {
    flexDirection: 'row',
    alignItems: 'flex-end',
    justifyContent: 'space-between',
    paddingVertical: 12,
    paddingHorizontal: 6,
    minHeight: 220,
  },
  lsBar: {
    width: 16,
    backgroundColor: '#fdba74',
    borderTopLeftRadius: 8,
    borderTopRightRadius: 8,
    borderBottomLeftRadius: 2,
    borderBottomRightRadius: 2,
    justifyContent: 'flex-end',
    alignItems: 'center',
    marginHorizontal: 2,
  },
  lsBarCur: {
    backgroundColor: '#fb923c',
  },
  lsBarTarget: {
    backgroundColor: '#22c55e',
  },
  lsBarText: {
    fontSize: 10,
    color: '#7c2d12',
    marginBottom: 4,
    fontWeight: '600',
  },
  lsFooterRow: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    paddingTop: 6,
  },
  lsFootText: {
    color: '#9a3412',
    fontSize: 12,
  },
  lsDone: {
    color: '#16a34a',
    fontWeight: '700',
  },
  bstContainer: {
    marginHorizontal: 15,
  },
  bstCard: {
    backgroundColor: '#0b1f16',
    borderRadius: 16,
    borderWidth: 1,
    borderColor: '#163529',
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 6 },
    shadowOpacity: 0.18,
    shadowRadius: 14,
    paddingVertical: 14,
    paddingHorizontal: 12,
  },
  bstHeader: {
    marginBottom: 8,
  },
  bstTitle: {
    fontSize: 18,
    fontWeight: '700',
    color: '#e7f6ef',
  },
  bstSubtitle: {
    fontSize: 13,
    color: '#a6c9bb',
    marginTop: 4,
  },
  bstControls: {
    flexDirection: 'row',
    alignItems: 'center',
    flexWrap: 'wrap',
    marginTop: 8,
    marginBottom: 12,
  },
  bstBtn: {
    flexDirection: 'row',
    alignItems: 'center',
    backgroundColor: '#123426',
    borderWidth: 1,
    borderColor: '#1a4a37',
    borderRadius: 12,
    paddingVertical: 8,
    paddingHorizontal: 10,
    marginRight: 8,
  },
  bstBtnActive: {
    backgroundColor: '#154234',
    borderColor: '#22624a',
  },
  bstIcon: {
    width: 20,
    height: 20,
    marginRight: 6,
  },
  bstBtnText: {
    color: '#e7f6ef',
    fontSize: 13,
    fontWeight: '600',
  },
  bstChip: {
    backgroundColor: '#0f2c20',
    borderWidth: 1,
    borderColor: '#1a4a37',
    borderRadius: 10,
    paddingVertical: 6,
    paddingHorizontal: 10,
    marginLeft: 6,
  },
  bstChipText: {
    color: '#cde7dc',
    fontSize: 12,
    fontWeight: '600',
  },
  bstTree: {
    position: 'relative',
    minHeight: 420,
    backgroundColor: '#0e251c',
    borderRadius: 12,
    borderWidth: 1,
    borderColor: '#153a2c',
    padding: 8,
  },
  bstNode: {
    position: 'absolute',
    width: 40,
    height: 40,
    borderRadius: 20,
    backgroundColor: '#1f6f4d',
    borderWidth: 2,
    borderColor: '#2e8a64',
    justifyContent: 'center',
    alignItems: 'center',
  },
  bstNodeHL: {
    backgroundColor: '#239f6b',
    borderColor: '#3ac18b',
  },
  bstNodeFound: {
    backgroundColor: '#1fbf72',
    borderColor: '#57d69b',
  },
  bstNodeText: {
    color: '#e7f6ef',
    fontSize: 12,
    fontWeight: '700',
  },
  bstLine: {
    position: 'absolute',
    backgroundColor: '#1a4a37',
  },
  bstFooterRow: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    paddingTop: 6,
  },
  bstFootText: {
    color: '#a6c9bb',
    fontSize: 12,
  },
  bstDone: {
    color: '#43d998',
    fontWeight: '700',
  },
});

const ICON_BASE64_MS = {
  play: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABJUlEQVR4nO2Uu0oDQRRGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  pause: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABK0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  step: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABMklEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  reset: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABMklEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  shuffle: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABO0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
};

const ICON_BASE64_BST = {
  play: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABJUlEQVR4nO2Uu0oDQRRGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  pause: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABK0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  step: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABMklEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  reset: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABMklEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  shuffle: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABO0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  insert: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABN0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  remove: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABN0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  find: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABN0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
};

const ICON_BASE64_BS = {
  play: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABJUlEQVR4nO2Uu0oDQRRGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  pause: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABK0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  step: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABMklEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  reset: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABMklEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  shuffle: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABO0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  search: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABN0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
};

const ICON_BASE64_LS = {
  play: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABJUlEQVR4nO2Uu0oDQRRGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  pause: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABK0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  step: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABMklEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  reset: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABMklEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  shuffle: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABO0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
};

const MergeSortVisualizer: React.FC = () => {
  const [arr, setArr] = useState<number[]>([]);
  const [steps, setSteps] = useState<{ arr: number[]; highlights: number[] }[]>([]);
  const [idx, setIdx] = useState(0);
  const [playing, setPlaying] = useState(false);
  const [speed, setSpeed] = useState(600);
  const timerRef = React.useRef<any>(null);
  const maxBars = 18;

  const genArray = () => {
    const a = Array.from({ length: maxBars }, () => Math.floor(20 + Math.random() * 120));
    setArr(a);
    const s = buildSteps(a);
    setSteps(s);
    setIdx(0);
    setPlaying(false);
  };

  const buildSteps = (a: number[]) => {
    const n = a.length;
    let src = a.slice();
    const out: { arr: number[]; highlights: number[] }[] = [{ arr: src.slice(), highlights: [] }];
    for (let size = 1; size < n; size *= 2) {
      for (let left = 0; left < n; left += 2 * size) {
        const mid = Math.min(left + size, n);
        const right = Math.min(left + 2 * size, n);
        let i = left, j = mid;
        const merged: number[] = [];
        while (i < mid && j < right) {
          if (src[i] <= src[j]) { merged.push(src[i]); i++; } else { merged.push(src[j]); j++; }
        }
        while (i < mid) { merged.push(src[i]); i++; }
        while (j < right) { merged.push(src[j]); j++; }
        for (let k = 0; k < merged.length; k++) src[left + k] = merged[k];
        out.push({ arr: src.slice(), highlights: Array.from({ length: merged.length }, (_, k) => left + k) });
      }
    }
    return out;
  };

  const stepOnce = () => {
    setIdx(prev => {
      const next = Math.min(prev + 1, steps.length - 1);
      setArr(steps[next].arr);
      if (next === steps.length - 1) setPlaying(false);
      return next;
    });
  };

  const togglePlay = () => setPlaying(p => !p);
  const resetSort = () => { const s = buildSteps(arr.slice()); setSteps(s); setIdx(0); setPlaying(false); };
  const shuffle = () => genArray();

  React.useEffect(() => { genArray(); }, []);
  React.useEffect(() => {
    if (playing) {
      if (timerRef.current) clearInterval(timerRef.current);
      timerRef.current = setInterval(stepOnce, speed);
    } else {
      if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; }
    }
    return () => { if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; } };
  }, [playing, speed, steps]);

  const maxVal = Math.max(...arr, 1);
  const isDone = idx === steps.length - 1 && steps.length > 1;

  return (
    <View style={styles.msCard}>
      <View style={styles.msHeader}> 
        <Text style={styles.msTitle}>归并排序 · 霓虹风格</Text>
        <Text style={styles.msSubtitle}>播放、步进、重置、随机</Text>
      </View>
      <View style={styles.msControls}>
        <TouchableOpacity style={[styles.msBtn, playing ? styles.msBtnActive : null]} onPress={togglePlay} activeOpacity={0.85}>
          <Image source={{ uri: playing ? ICON_BASE64_MS.pause : ICON_BASE64_MS.play }} style={styles.msIcon} />
          <Text style={styles.msBtnText}>{playing ? '暂停' : '播放'}</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.msBtn} onPress={stepOnce} activeOpacity={0.85}>
          <Image source={{ uri: ICON_BASE64_MS.step }} style={styles.msIcon} />
          <Text style={styles.msBtnText}>步进</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.msBtn} onPress={resetSort} activeOpacity={0.85}>
          <Image source={{ uri: ICON_BASE64_MS.reset }} style={styles.msIcon} />
          <Text style={styles.msBtnText}>重置</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.msBtn} onPress={shuffle} activeOpacity={0.85}>
          <Image source={{ uri: ICON_BASE64_MS.shuffle }} style={styles.msIcon} />
          <Text style={styles.msBtnText}>随机</Text>
        </TouchableOpacity>
        <View style={styles.msChips}>
          <TouchableOpacity style={styles.msChip} onPress={() => setSpeed(900)}><Text style={styles.msChipText}>0.5×</Text></TouchableOpacity>
          <TouchableOpacity style={[styles.msChip, styles.msChipActive]} onPress={() => setSpeed(600)}><Text style={styles.msChipText}>1×</Text></TouchableOpacity>
          <TouchableOpacity style={styles.msChip} onPress={() => setSpeed(300)}><Text style={styles.msChipText}>2×</Text></TouchableOpacity>
        </View>
      </View>
      <View style={styles.msBars}>
        {arr.map((v, i) => {
          const h = Math.round((v / maxVal) * 160) + 20;
          const hl = steps[idx]?.highlights.includes(i);
          return (
            <View key={i} style={[styles.msBar, { height: h }, hl ? styles.msBarHL : null]}>
              <Text style={styles.msBarText}>{v}</Text>
            </View>
          );
        })}
      </View>
      <View style={styles.msFooterRow}>
        <Text style={styles.msFootText}>步骤 {idx} / {Math.max(steps.length - 1, 0)}</Text>
        <Text style={[styles.msFootText, isDone ? styles.msDone : null]}>{isDone ? '已完成排序' : '进行中'}</Text>
      </View>
    </View>
  );
};
const BinarySearchVisualizer: React.FC = () => {
  const [arr, setArr] = useState<number[]>([]);
  const [target, setTarget] = useState<number>(0);
  const [steps, setSteps] = useState<{ low: number; high: number; mid: number; found: boolean }[]>([]);
  const [idx, setIdx] = useState(0);
  const [playing, setPlaying] = useState(false);
  const [speed, setSpeed] = useState(700);
  const timerRef = React.useRef<any>(null);
  const maxBars = 20;

  const genArray = () => {
    const base = Array.from({ length: maxBars }, () => Math.floor(10 + Math.random() * 150));
    const sorted = base.sort((a, b) => a - b);
    setArr(sorted);
    const t = sorted[Math.floor(Math.random() * sorted.length)];
    setTarget(t);
    const s = buildSteps(sorted, t);
    setSteps(s);
    setIdx(0);
    setPlaying(false);
  };

  const buildSteps = (a: number[], t: number) => {
    let low = 0, high = a.length - 1;
    const out: { low: number; high: number; mid: number; found: boolean }[] = [];
    while (low <= high) {
      const mid = Math.floor((low + high) / 2);
      const found = a[mid] === t;
      out.push({ low, high, mid, found });
      if (found) break;
      if (a[mid] < t) low = mid + 1; else high = mid - 1;
    }
    if (out.length === 0) out.push({ low: 0, high: a.length - 1, mid: -1, found: false });
    return out;
  };

  const stepOnce = () => {
    setIdx(prev => {
      const next = Math.min(prev + 1, steps.length - 1);
      if (next === steps.length - 1) setPlaying(false);
      return next;
    });
  };

  const togglePlay = () => setPlaying(p => !p);
  const resetSearch = () => { const s = buildSteps(arr.slice(), target); setSteps(s); setIdx(0); setPlaying(false); };
  const shuffle = () => genArray();
  const incTarget = () => setTarget(t => { const nt = t + 1; const s = buildSteps(arr.slice(), nt); setSteps(s); setIdx(0); return nt; });
  const decTarget = () => setTarget(t => { const nt = Math.max(0, t - 1); const s = buildSteps(arr.slice(), nt); setSteps(s); setIdx(0); return nt; });

  React.useEffect(() => { genArray(); }, []);
  React.useEffect(() => {
    if (playing) {
      if (timerRef.current) clearInterval(timerRef.current);
      timerRef.current = setInterval(stepOnce, speed);
    } else {
      if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; }
    }
    return () => { if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; } };
  }, [playing, speed, steps]);

  const maxVal = Math.max(...arr, 1);
  const isDone = idx === steps.length - 1 && steps.length > 0;
  const cur = steps[idx] || { low: 0, high: arr.length - 1, mid: -1, found: false };

  return (
    <View style={styles.bsCard}>
      <View style={styles.bsHeader}> 
        <Text style={styles.bsTitle}>二分查找 · 柔和风格</Text>
        <Text style={styles.bsSubtitle}>播放、步进、重置、随机、目标调整</Text>
      </View>
      <View style={styles.bsControls}>
        <TouchableOpacity style={[styles.bsBtn, playing ? styles.bsBtnActive : null]} onPress={togglePlay} activeOpacity={0.85}>
          <Image source={{ uri: playing ? ICON_BASE64_BS.pause : ICON_BASE64_BS.play }} style={styles.bsIcon} />
          <Text style={styles.bsBtnText}>{playing ? '暂停' : '播放'}</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.bsBtn} onPress={stepOnce} activeOpacity={0.85}>
          <Image source={{ uri: ICON_BASE64_BS.step }} style={styles.bsIcon} />
          <Text style={styles.bsBtnText}>步进</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.bsBtn} onPress={resetSearch} activeOpacity={0.85}>
          <Image source={{ uri: ICON_BASE64_BS.reset }} style={styles.bsIcon} />
          <Text style={styles.bsBtnText}>重置</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.bsBtn} onPress={shuffle} activeOpacity={0.85}>
          <Image source={{ uri: ICON_BASE64_BS.shuffle }} style={styles.bsIcon} />
          <Text style={styles.bsBtnText}>随机</Text>
        </TouchableOpacity>
      </View>
      <View style={styles.bsTargetRow}>
        <TouchableOpacity style={styles.bsChip} onPress={decTarget}><Text style={styles.bsChipText}>目标 -</Text></TouchableOpacity>
        <View style={styles.bsChip}><Text style={styles.bsChipText}>当前目标:{target}</Text></View>
        <TouchableOpacity style={styles.bsChip} onPress={incTarget}><Text style={styles.bsChipText}>目标 +</Text></TouchableOpacity>
        <View style={styles.bsChip}><Text style={styles.bsChipText}>范围:{cur.low} - {cur.high}</Text></View>
      </View>
      <View style={styles.bsBars}>
        {arr.map((v, i) => {
          const h = Math.round((v / maxVal) * 160) + 20;
          const inRange = i >= cur.low && i <= cur.high;
          const isMid = i === cur.mid;
          const isTarget = v === target && inRange;
          return (
            <View key={i} style={[styles.bsBar, { height: h }, !inRange ? styles.bsBarDim : null, isMid ? styles.bsBarMid : null, isTarget ? styles.bsBarTarget : null]}>
              <Text style={styles.bsBarText}>{v}</Text>
            </View>
          );
        })}
      </View>
      <View style={styles.bsFooterRow}>
        <Text style={styles.bsFootText}>步骤 {idx + 1} / {Math.max(steps.length, 0)}</Text>
        <Text style={[styles.bsFootText, isDone && cur.found ? styles.bsDone : null]}>{isDone ? (cur.found ? '已找到目标' : '未找到') : '进行中'}</Text>
      </View>
    </View>
  );
};
const LinearSearchVisualizer: React.FC = () => {
  const [arr, setArr] = useState<number[]>([]);
  const [target, setTarget] = useState<number>(0);
  const [steps, setSteps] = useState<{ idx: number; found: boolean }[]>([]);
  const [idx, setIdx] = useState(0);
  const [playing, setPlaying] = useState(false);
  const [speed, setSpeed] = useState(700);
  const timerRef = React.useRef<any>(null);
  const maxBars = 22;

  const genArray = () => {
    const base = Array.from({ length: maxBars }, () => Math.floor(10 + Math.random() * 150));
    setArr(base);
    const t = base[Math.floor(Math.random() * base.length)];
    setTarget(t);
    const s = buildSteps(base, t);
    setSteps(s);
    setIdx(0);
    setPlaying(false);
  };

  const buildSteps = (a: number[], t: number) => {
    const out: { idx: number; found: boolean }[] = [];
    for (let i = 0; i < a.length; i++) {
      const found = a[i] === t;
      out.push({ idx: i, found });
      if (found) break;
    }
    if (out.length === 0) out.push({ idx: -1, found: false });
    return out;
  };

  const stepOnce = () => {
    setIdx(prev => {
      const next = Math.min(prev + 1, steps.length - 1);
      if (next === steps.length - 1) setPlaying(false);
      return next;
    });
  };

  const togglePlay = () => setPlaying(p => !p);
  const resetSearch = () => { const s = buildSteps(arr.slice(), target); setSteps(s); setIdx(0); setPlaying(false); };
  const shuffle = () => genArray();
  const incTarget = () => setTarget(t => { const nt = t + 1; const s = buildSteps(arr.slice(), nt); setSteps(s); setIdx(0); return nt; });
  const decTarget = () => setTarget(t => { const nt = Math.max(0, t - 1); const s = buildSteps(arr.slice(), nt); setSteps(s); setIdx(0); return nt; });

  React.useEffect(() => { genArray(); }, []);
  React.useEffect(() => {
    if (playing) {
      if (timerRef.current) clearInterval(timerRef.current);
      timerRef.current = setInterval(stepOnce, speed);
    } else {
      if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; }
    }
    return () => { if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; } };
  }, [playing, speed, steps]);

  const maxVal = Math.max(...arr, 1);
  const isDone = idx === steps.length - 1 && steps.length > 0;
  const cur = steps[idx] || { idx: -1, found: false };

  return (
    <View style={styles.lsCard}>
      <View style={styles.lsHeader}> 
        <Text style={styles.lsTitle}>线性查找 · 暖色风格</Text>
        <Text style={styles.lsSubtitle}>播放、步进、重置、随机、目标调整</Text>
      </View>
      <View style={styles.lsControls}>
        <TouchableOpacity style={[styles.lsBtn, playing ? styles.lsBtnActive : null]} onPress={togglePlay} activeOpacity={0.85}>
          <Image source={{ uri: playing ? ICON_BASE64_LS.pause : ICON_BASE64_LS.play }} style={styles.lsIcon} />
          <Text style={styles.lsBtnText}>{playing ? '暂停' : '播放'}</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.lsBtn} onPress={stepOnce} activeOpacity={0.85}>
          <Image source={{ uri: ICON_BASE64_LS.step }} style={styles.lsIcon} />
          <Text style={styles.lsBtnText}>步进</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.lsBtn} onPress={resetSearch} activeOpacity={0.85}>
          <Image source={{ uri: ICON_BASE64_LS.reset }} style={styles.lsIcon} />
          <Text style={styles.lsBtnText}>重置</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.lsBtn} onPress={shuffle} activeOpacity={0.85}>
          <Image source={{ uri: ICON_BASE64_LS.shuffle }} style={styles.lsIcon} />
          <Text style={styles.lsBtnText}>随机</Text>
        </TouchableOpacity>
      </View>
      <View style={styles.lsTargetRow}>
        <TouchableOpacity style={styles.lsChip} onPress={decTarget}><Text style={styles.lsChipText}>目标 -</Text></TouchableOpacity>
        <View style={styles.lsChip}><Text style={styles.lsChipText}>当前目标:{target}</Text></View>
        <TouchableOpacity style={styles.lsChip} onPress={incTarget}><Text style={styles.lsChipText}>目标 +</Text></TouchableOpacity>
        <View style={styles.lsChip}><Text style={styles.lsChipText}>索引:{cur.idx >= 0 ? cur.idx : '-'}</Text></View>
      </View>
      <View style={styles.lsBars}>
        {arr.map((v, i) => {
          const h = Math.round((v / maxVal) * 160) + 20;
          const isCur = i === cur.idx;
          const isTarget = v === target;
          return (
            <View key={i} style={[styles.lsBar, { height: h }, isCur ? styles.lsBarCur : null, isTarget ? styles.lsBarTarget : null]}>
              <Text style={styles.lsBarText}>{v}</Text>
            </View>
          );
        })}
      </View>
      <View style={styles.lsFooterRow}>
        <Text style={styles.lsFootText}>步骤 {idx + 1} / {Math.max(steps.length, 0)}</Text>
        <Text style={[styles.lsFootText, isDone && cur.found ? styles.lsDone : null]}>{isDone ? (cur.found ? '已找到目标' : '未找到') : '进行中'}</Text>
      </View>
    </View>
  );
};
const BSTVisualizer: React.FC = () => {
  const [root, setRoot] = useState<any>(null);
  const [target, setTarget] = useState<number>(0);
  const [steps, setSteps] = useState<number[]>([]);
  const [idx, setIdx] = useState(0);
  const [playing, setPlaying] = useState(false);
  const [speed, setSpeed] = useState(700);
  const timerRef = React.useRef<any>(null);
  const levelGap = 90;
  const padding = 24;

  const insert = (k: number) => {
    const node = { key: k, left: null, right: null };
    if (!root) { setRoot(node); return; }
    const r = JSON.parse(JSON.stringify(root));
    let cur: any = r;
    while (true) {
      if (k < cur.key) { if (cur.left) cur = cur.left; else { cur.left = node; break; } }
      else if (k > cur.key) { if (cur.right) cur = cur.right; else { cur.right = node; break; } }
      else break;
    }
    setRoot(r);
  };

  const findPath = (k: number) => {
    const path: number[] = [];
    let cur: any = root;
    while (cur) {
      path.push(cur.key);
      if (k === cur.key) break;
      cur = k < cur.key ? cur.left : cur.right;
    }
    return path;
  };

  const minNode = (n: any) => { let c = n; while (c && c.left) c = c.left; return c; };
  const removeKey = (k: number) => {
    const del = (n: any, v: number): any => {
      if (!n) return null;
      if (v < n.key) { n.left = del(n.left, v); return n; }
      if (v > n.key) { n.right = del(n.right, v); return n; }
      if (!n.left) return n.right;
      if (!n.right) return n.left;
      const succ = minNode(n.right);
      n.key = succ.key;
      n.right = del(n.right, succ.key);
      return n;
    };
    const r = JSON.parse(JSON.stringify(root));
    setRoot(del(r, k));
  };

  const buildRandom = () => {
    let r: any = null;
    const values = Array.from({ length: 10 }, () => Math.floor(10 + Math.random() * 90));
    values.forEach(v => { r = insertInto(r, v); });
    setRoot(r);
    const t = values[Math.floor(Math.random() * values.length)];
    setTarget(t);
    const p = findPathOn(r, t);
    setSteps(p);
    setIdx(0);
    setPlaying(false);
  };

  const insertInto = (r: any, k: number) => {
    const node = { key: k, left: null, right: null };
    if (!r) return node;
    let cur = r;
    while (true) {
      if (k < cur.key) { if (cur.left) cur = cur.left; else { cur.left = node; break; } }
      else if (k > cur.key) { if (cur.right) cur = cur.right; else { cur.right = node; break; } }
      else break;
    }
    return r;
  };

  const findPathOn = (r: any, k: number) => {
    const path: number[] = [];
    let cur = r;
    while (cur) {
      path.push(cur.key);
      if (k === cur.key) break;
      cur = k < cur.key ? cur.left : cur.right;
    }
    return path;
  };

  const stepOnce = () => {
    setIdx(prev => {
      const next = Math.min(prev + 1, steps.length - 1);
      if (next === steps.length - 1) setPlaying(false);
      return next;
    });
  };

  const togglePlay = () => setPlaying(p => !p);
  const resetSearch = () => { const p = findPath(target); setSteps(p); setIdx(0); setPlaying(false); };
  const shuffle = () => buildRandom();
  const incTarget = () => setTarget(t => { const nt = t + 1; const p = findPath(nt); setSteps(p); setIdx(0); return nt; });
  const decTarget = () => setTarget(t => { const nt = Math.max(0, t - 1); const p = findPath(nt); setSteps(p); setIdx(0); return nt; });
  const doInsert = () => { const v = Math.floor(10 + Math.random() * 90); insert(v); const p = findPath(target); setSteps(p); setIdx(0); };
  const doRemove = () => { removeKey(target); const p = findPath(target); setSteps(p); setIdx(0); };
  const clearTree = () => { setRoot(null); setSteps([]); setIdx(0); setPlaying(false); };

  React.useEffect(() => { buildRandom(); }, []);
  React.useEffect(() => {
    if (playing) {
      if (timerRef.current) clearInterval(timerRef.current);
      timerRef.current = setInterval(stepOnce, speed);
    } else {
      if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; }
    }
    return () => { if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; } };
  }, [playing, speed, steps]);

  const layout = buildLayout(root);
  const curKey = steps[idx];
  const isDone = steps.length > 0 && idx === steps.length - 1 && curKey === target;

  return (
    <View style={styles.bstCard}>
      <View style={styles.bstHeader}> 
        <Text style={styles.bstTitle}>二叉搜索树 · 森林风格</Text>
        <Text style={styles.bstSubtitle}>播放、步进、插入、删除、目标调整</Text>
      </View>
      <View style={styles.bstControls}>
        <TouchableOpacity style={[styles.bstBtn, playing ? styles.bstBtnActive : null]} onPress={togglePlay} activeOpacity={0.85}>
          <Image source={{ uri: playing ? ICON_BASE64_BST.pause : ICON_BASE64_BST.play }} style={styles.bstIcon} />
          <Text style={styles.bstBtnText}>{playing ? '暂停' : '播放'}</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.bstBtn} onPress={stepOnce} activeOpacity={0.85}>
          <Image source={{ uri: ICON_BASE64_BST.step }} style={styles.bstIcon} />
          <Text style={styles.bstBtnText}>步进</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.bstBtn} onPress={resetSearch} activeOpacity={0.85}>
          <Image source={{ uri: ICON_BASE64_BST.reset }} style={styles.bstIcon} />
          <Text style={styles.bstBtnText}>重置</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.bstBtn} onPress={shuffle} activeOpacity={0.85}>
          <Image source={{ uri: ICON_BASE64_BST.shuffle }} style={styles.bstIcon} />
          <Text style={styles.bstBtnText}>随机</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.bstBtn} onPress={doInsert} activeOpacity={0.85}>
          <Image source={{ uri: ICON_BASE64_BST.insert }} style={styles.bstIcon} />
          <Text style={styles.bstBtnText}>插入</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.bstBtn} onPress={doRemove} activeOpacity={0.85}>
          <Image source={{ uri: ICON_BASE64_BST.remove }} style={styles.bstIcon} />
          <Text style={styles.bstBtnText}>删除</Text>
        </TouchableOpacity>
      </View>
      <View style={styles.bstControls}>
        <TouchableOpacity style={styles.bstChip} onPress={decTarget}><Text style={styles.bstChipText}>目标 -</Text></TouchableOpacity>
        <View style={styles.bstChip}><Text style={styles.bstChipText}>当前目标:{target}</Text></View>
        <TouchableOpacity style={styles.bstChip} onPress={incTarget}><Text style={styles.bstChipText}>目标 +</Text></TouchableOpacity>
      </View>
      <View style={styles.bstTree}>
        {layout.lines.map((l: any, i: number) => (
          <View key={`line-${i}`} style={[styles.bstLine, { left: l.vx, top: l.vy, width: 2, height: l.vh }]} />
        ))}
        {layout.lines.map((l: any, i: number) => (
          <View key={`hline-${i}`} style={[styles.bstLine, { left: l.hx, top: l.hy, width: l.hw, height: 2 }]} />
        ))}
        {layout.nodes.map((n: any) => (
          <View key={`node-${n.key}-${n.x}-${n.y}`} style={[styles.bstNode, { left: n.x - 20, top: n.y - 20 }, n.key === curKey ? styles.bstNodeHL : null, n.key === target && curKey === target ? styles.bstNodeFound : null]}>
            <Text style={styles.bstNodeText}>{n.key}</Text>
          </View>
        ))}
      </View>
      <View style={styles.bstFooterRow}>
        <Text style={styles.bstFootText}>步骤 {idx + 1} / {Math.max(steps.length, 0)}</Text>
        <Text style={[styles.bstFootText, isDone ? styles.bstDone : null]}>{isDone ? '已找到目标' : '进行中'}</Text>
      </View>
    </View>
  );

  function buildLayout(r: any) {
    const nodes: any[] = [];
    const lines: any[] = [];
    if (!r) return { nodes, lines };
    const queue: any[] = [{ n: r, level: 0, idx: 0 }];
    const levels: any = {};
    while (queue.length) {
      const item = queue.shift();
      const lvl = item.level;
      levels[lvl] = levels[lvl] || [];
      levels[lvl].push(item.n);
      if (item.n.left) queue.push({ n: item.n.left, level: lvl + 1, idx: 0 });
      if (item.n.right) queue.push({ n: item.n.right, level: lvl + 1, idx: 0 });
    }
    const contentWidth = width - padding * 2;
    const posMap: any = new Map();
    const maxLevel = Math.max(0, ...Object.keys(levels).map(v => parseInt(v, 10)));
    for (let l = 0; l <= maxLevel; l++) {
      const row = levels[l] || [];
      const count = row.length || 1;
      for (let i = 0; i < row.length; i++) {
        const x = padding + Math.round(((i + 1) * contentWidth) / (count + 1));
        const y = padding + l * levelGap + 20;
        posMap.set(row[i], { x, y });
        nodes.push({ key: row[i].key, x, y });
      }
    }
    const addLines = (n: any) => {
      const p = posMap.get(n);
      if (n.left) {
        const c = posMap.get(n.left);
        const vy = p.y + 20;
        const vh = c.y - p.y - 20;
        const vx = p.x;
        const hx = Math.min(p.x, c.x);
        const hy = c.y;
        const hw = Math.abs(c.x - p.x);
        lines.push({ vx, vy, vh, hx, hy, hw });
        addLines(n.left);
      }
      if (n.right) {
        const c = posMap.get(n.right);
        const vy = p.y + 20;
        const vh = c.y - p.y - 20;
        const vx = p.x;
        const hx = Math.min(p.x, c.x);
        const hy = c.y;
        const hw = Math.abs(c.x - p.x);
        lines.push({ vx, vy, vh, hx, hy, hw });
        addLines(n.right);
      }
    };
    addLines(r);
    return { nodes, lines };
  }
};
export default CalendarComponentApp;

这段React Native日历组件代码展现了现代化移动应用开发的典型架构模式,其设计理念与鸿蒙系统的分布式架构和声明式UI有着深层次的契合。

从组件化设计角度来看,Icon组件通过Unicode符号实现了轻量级图标系统,这种资源内聚的设计思想正呼应了鸿蒙原子化服务的核心理念。通过参数化配置图标名称、尺寸和颜色,实现了高度可复用的组件单元,这与鸿蒙Ability组件的设计哲学高度一致。组件内部的状态映射机制采用switch-case模式,将抽象名称转换为具体符号,体现了鸿蒙资源管理系统中的统一资源标识符思想。

日历主体采用状态驱动渲染机制,通过currentDate状态管理月份导航,这种单向数据流的设计模式正是鸿蒙ArkTS声明式UI的核心特征。组件内部维护的日期状态作为唯一真相源,驱动整个界面的重新渲染,实现了数据与UI的自动同步。这种响应式编程范式与鸿蒙的状态管理机制在本质上相通,都强调通过状态变化来触发界面更新。

在日期计算方面,代码通过JavaScript Date对象实现了完整的日历逻辑,包括月份天数计算、首日星期定位等功能模块。这种基于标准API的跨平台兼容性设计与鸿蒙的多设备适配能力相呼应。getDaysInMonth函数利用JavaScript Date对象的特性,通过设置月份为下个月的第0天来获取当月最后一天,这种巧妙的算法设计展现了与鸿蒙系统级API相似的抽象层次。

事件管理模块采用数据过滤机制,通过hasEvents函数实现日期与事件的关联映射,这种数据绑定模式与鸿蒙的状态管理机制高度相似。通过events数组与日期字符串的匹配,实现了业务数据与界面显示的松耦合关联,这种设计理念正是鸿蒙分布式数据管理能力的微观体现。

组件采用了分层渲染架构,将日历拆分为头部导航、星期栏和日期网格三个独立模块,这种组件解耦和复用性正是鸿蒙弹性部署能力的体现。renderHeader、renderWeekdays和renderDays三个渲染函数的分离,使得每个模块都可以独立开发和测试,这与鸿蒙的模块化开发理念不谋而合。

交互设计方面,TouchableOpacity组件提供了原生级的触控反馈,这与鸿蒙的统一交互体验设计原则相呼应。导航按钮的点击事件通过prevMonth和nextMonth函数处理,实现了时间的线性导航,这种交互模式与鸿蒙的分布式设备协同有着相似的逻辑结构。

整个组件的国际化处理也展现了与鸿蒙全球化支持相一致的设计思路。月份名称通过toLocaleString方法根据系统语言自动适配,这种动态本地化机制正是鸿蒙多语言架构的核心特征。


打包

接下来通过打包命令npn run harmony将reactNative的代码打包成为bundle,这样可以进行在开源鸿蒙OpenHarmony中进行使用。

打包之后再将打包后的鸿蒙OpenHarmony文件拷贝到鸿蒙的DevEco-Studio工程目录去:

最后运行效果图如下显示:

相关推荐
大雷神5 小时前
[鸿蒙2025领航者闯关]HarmonyOS中高德地图第一篇:高德地图SDK集成与初始化
华为·harmonyos
小p7 小时前
react学习13:几个简单的自定义hooks
react.js
m0_看见流浪猫请投喂7 小时前
Flutter鸿蒙化现有三方插件兼容适配鸿蒙平台
flutter·华为·harmonyos·flutterplugin·flutter鸿蒙化
cos11 小时前
React RCE 漏洞影响自建 Umami 服务 —— 记 CVE-2025-55182
前端·安全·react.js
嘴平伊之豬12 小时前
对照typescript学习鸿蒙ArkTS
前端·harmonyos
奋斗猿12 小时前
前端实测:RSC不是银弹,但它真的重构了我的技术栈
前端·react.js
HexCIer12 小时前
CVE-2025-55182 React Server Components "React2Shell" 深度调查与全链路响应报告
react.js·next.js
威哥爱编程13 小时前
【鸿蒙开发案例篇】NAPI 实现 ArkTS 与 C++ 间的复杂对象传递
c++·harmonyos·arkts
爱看书的小沐13 小时前
【小沐学WebGIS】基于Cesium.JS绘制雷达波束/几何体/传感器Sensor(Cesium / vue / react )
javascript·vue.js·react.js·雷达·cesium·传感器·波束